├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── assets │ └── images │ │ ├── bonsai.png │ │ ├── dynamic_scenes.png │ │ ├── garden.png │ │ ├── stump.png │ │ └── truck.png ├── bonsai.html ├── dropin.html ├── dynamic_dropin.html ├── dynamic_scenes.html ├── garden.html ├── index.html ├── js │ └── util.js ├── stump.html ├── truck.html └── vr.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── AbortablePromise.js ├── ArrowHelper.js ├── Constants.js ├── DropInViewer.js ├── LogLevel.js ├── OrbitControls.js ├── RenderMode.js ├── SceneHelper.js ├── SceneRevealMode.js ├── SplatRenderMode.js ├── Util.js ├── Viewer.js ├── index.js ├── loaders │ ├── Compression.js │ ├── DirectLoadError.js │ ├── InternalLoadType.js │ ├── LoaderStatus.js │ ├── SceneFormat.js │ ├── SplatBuffer.js │ ├── SplatBufferGenerator.js │ ├── SplatPartitioner.js │ ├── UncompressedSplatArray.js │ ├── Utils.js │ ├── ksplat │ │ └── KSplatLoader.js │ ├── ply │ │ ├── INRIAV1PlyParser.js │ │ ├── INRIAV2PlyParser.js │ │ ├── PlayCanvasCompressedPlyParser.js │ │ ├── PlyFormat.js │ │ ├── PlyLoader.js │ │ ├── PlyParser.js │ │ └── PlyParserUtils.js │ ├── splat │ │ ├── SplatLoader.js │ │ └── SplatParser.js │ └── spz │ │ └── SpzLoader.js ├── raycaster │ ├── Hit.js │ ├── Ray.js │ └── Raycaster.js ├── splatmesh │ ├── SplatGeometry.js │ ├── SplatMaterial.js │ ├── SplatMaterial2D.js │ ├── SplatMaterial3D.js │ ├── SplatMesh.js │ └── SplatScene.js ├── splattree │ └── SplatTree.js ├── three-shim │ ├── WebGLCapabilities.js │ └── WebGLExtensions.js ├── ui │ ├── InfoPanel.js │ ├── LoadingProgressBar.js │ ├── LoadingSpinner.js │ └── Util.js ├── webxr │ ├── ARButton.js │ ├── VRButton.js │ └── WebXRMode.js └── worker │ ├── SortWorker.js │ ├── compile_wasm.sh │ ├── compile_wasm_no_simd.sh │ ├── compile_wasm_no_simd_non_shared.sh │ ├── compile_wasm_non_shared.sh │ ├── sorter.cpp │ ├── sorter.wasm │ ├── sorter_no_simd.cpp │ ├── sorter_no_simd.wasm │ ├── sorter_no_simd_non_shared.wasm │ └── sorter_non_shared.wasm ├── stylelintrc.json └── util ├── create-ksplat.js ├── import-base-64.js └── server.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es2021': true, 5 | }, 6 | 'extends': 'google', 7 | 'overrides': [ 8 | { 9 | 'env': { 10 | 'node': true, 11 | }, 12 | 'files': [ 13 | '.eslintrc.{js,cjs}', 14 | ], 15 | 'parserOptions': { 16 | 'sourceType': 'script', 17 | }, 18 | }, 19 | ], 20 | 'parserOptions': { 21 | 'ecmaVersion': 'latest', 22 | 'sourceType': 'module', 23 | }, 24 | 'rules': { 25 | "indent": ["error", 4], 26 | "max-len": ["error", 140], 27 | "object-curly-spacing": ["off"], 28 | "comma-dangle": ["off"], 29 | "prefer-const": ["off"], 30 | 'require-jsdoc': ["off"], 31 | "padded-blocks": ["off"], 32 | "indent": ["off"], 33 | "arrow-parens": ["off"], 34 | "no-unused-vars": ["error"], 35 | "valid-jsdoc" : ["off"] 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | node_modules 4 | **/*.DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Mark Kellogg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /demo/assets/images/bonsai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/demo/assets/images/bonsai.png -------------------------------------------------------------------------------- /demo/assets/images/dynamic_scenes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/demo/assets/images/dynamic_scenes.png -------------------------------------------------------------------------------- /demo/assets/images/garden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/demo/assets/images/garden.png -------------------------------------------------------------------------------- /demo/assets/images/stump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/demo/assets/images/stump.png -------------------------------------------------------------------------------- /demo/assets/images/truck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/demo/assets/images/truck.png -------------------------------------------------------------------------------- /demo/bonsai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splat Demo - Truck 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/dropin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splats - Drop-in example 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /demo/dynamic_dropin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splats - Drop-in example 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /demo/dynamic_scenes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splats - Dynamic scenes example 9 | 17 | 26 | 27 | 28 | 29 | 30 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /demo/garden.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splat Demo - Garden 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /demo/js/util.js: -------------------------------------------------------------------------------- 1 | function isMobile() { 2 | return navigator.userAgent.includes("Mobi"); 3 | } -------------------------------------------------------------------------------- /demo/stump.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splat Demo - Stump 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/truck.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splat Demo - Truck 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/vr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D Gaussian Splat Demo - VR Garden 9 | 10 | 18 | 27 | 28 | 29 | 30 | 31 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mkkellogg/gaussian-splats-3d", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/mkkellogg/GaussianSplats3D" 6 | }, 7 | "version": "0.4.7", 8 | "description": "Three.js-based 3D Gaussian splat viewer", 9 | "module": "build/gaussian-splats-3d.module.js", 10 | "main": "build/gaussian-splats-3d.umd.cjs", 11 | "author": "Mark Kellogg", 12 | "license": "MIT", 13 | "type": "module", 14 | "scripts": { 15 | "build-demo": "mkdir -p ./build/demo && cp -r ./demo ./build/ && cp ./node_modules/three/build/three.module.js ./build/demo/lib/three.module.js", 16 | "build-library": "npx rollup -c && mkdir -p ./build/demo/lib && cp ./build/gaussian-splats-3d.module.* ./build/demo/lib/", 17 | "build": "npm run build-library && npm run build-demo", 18 | "build-demo-windows": "(if not exist \".\\build\\demo\" mkdir .\\build\\demo) && xcopy /E .\\demo .\\build\\demo && xcopy .\\node_modules\\three\\build\\three.module.js .\\build\\demo\\lib\\", 19 | "build-library-windows": "npx rollup -c && (if not exist \".\\build\\demo\\lib\" mkdir .\\build\\demo\\lib) && copy .\\build\\gaussian-splats-3d* .\\build\\demo\\lib\\", 20 | "build-windows": "npm run build-library-windows && npm run build-demo-windows", 21 | "watch": "npx npm-watch ", 22 | "demo": "node util/server.js -d ./build/demo", 23 | "fix-styling": "npx stylelint **/*.scss --fix", 24 | "fix-js": "npx eslint src --fix", 25 | "lint": "npx eslint 'src/**/*.js' || true", 26 | "prettify": "npx prettier --write 'src/**/*.js'" 27 | }, 28 | "watch": { 29 | "build-library": { 30 | "patterns": [ 31 | "src/**/*.js" 32 | ] 33 | }, 34 | "build-demo": { 35 | "patterns": [ 36 | "demo/**/*.*" 37 | ] 38 | } 39 | }, 40 | "babel": {}, 41 | "keywords": [ 42 | "three", 43 | "threejs", 44 | "three.js", 45 | "splatting", 46 | "3D", 47 | "gaussian", 48 | "webgl", 49 | "javascript" 50 | ], 51 | "devDependencies": { 52 | "@babel/core": "7.22.0", 53 | "@babel/eslint-parser": "7.22.11", 54 | "@babel/plugin-proposal-class-properties": "7.18.6", 55 | "@babel/preset-env": "7.22.10", 56 | "babel-loader": "9.1.3", 57 | "@rollup/plugin-terser": "0.4.4", 58 | "@rollup/pluginutils": "5.0.5", 59 | "eslint": "8.47.0", 60 | "eslint-config-google": "0.14.0", 61 | "file-loader": "6.2.0", 62 | "http-server": "14.1.1", 63 | "npm-watch": "0.11.0", 64 | "prettier": "3.0.2", 65 | "prettier-eslint": "15.0.1", 66 | "rollup": "3.28.1", 67 | "url-loader": "4.1.1" 68 | }, 69 | "peerDependencies": { 70 | "three": ">=0.160.0" 71 | }, 72 | "files": [ 73 | "build/gaussian-splats-3d.umd.cjs", 74 | "build/gaussian-splats-3d.umd.cjs.map", 75 | "build/gaussian-splats-3d.module.js", 76 | "build/gaussian-splats-3d.module.js.map" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { base64 } from "./util/import-base-64.js"; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | const globals = { 5 | 'three': 'THREE' 6 | }; 7 | 8 | export default [ 9 | { 10 | input: './src/index.js', 11 | treeshake: false, 12 | external: [ 13 | 'three' 14 | ], 15 | output: [ 16 | { 17 | name: 'Gaussian Splats 3D', 18 | extend: true, 19 | format: 'umd', 20 | file: './build/gaussian-splats-3d.umd.cjs', 21 | globals: globals, 22 | sourcemap: true 23 | }, 24 | { 25 | name: 'Gaussian Splats 3D', 26 | extend: true, 27 | format: 'umd', 28 | file: './build/gaussian-splats-3d.umd.min.cjs', 29 | globals: globals, 30 | sourcemap: true, 31 | plugins: [terser()] 32 | } 33 | ], 34 | plugins: [ 35 | base64({ include: "**/*.wasm" }) 36 | ] 37 | }, 38 | { 39 | input: './src/index.js', 40 | treeshake: false, 41 | external: [ 42 | 'three' 43 | ], 44 | output: [ 45 | { 46 | name: 'Gaussian Splats 3D', 47 | format: 'esm', 48 | file: './build/gaussian-splats-3d.module.js', 49 | sourcemap: true 50 | }, 51 | { 52 | name: 'Gaussian Splats 3D', 53 | format: 'esm', 54 | file: './build/gaussian-splats-3d.module.min.js', 55 | sourcemap: true, 56 | plugins: [terser()] 57 | } 58 | ], 59 | plugins: [ 60 | base64({ 61 | include: "**/*.wasm", 62 | sourceMap: false 63 | }) 64 | ] 65 | } 66 | ]; -------------------------------------------------------------------------------- /src/AbortablePromise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AbortablePromise: A quick & dirty wrapper for JavaScript's Promise class that allows the underlying 3 | * asynchronous operation to be cancelled. It is only meant for simple situations where no complex promise 4 | * chaining or merging occurs. It needs a significant amount of work to truly replicate the full 5 | * functionality of JavaScript's Promise class. Look at Util.fetchWithProgress() for example usage. 6 | * 7 | * This class was primarily added to allow splat scene downloads to be cancelled. It has not been tested 8 | * very thoroughly and the implementation is kinda janky. If you can at all help it, please avoid using it :) 9 | */ 10 | export class AbortablePromise { 11 | 12 | static idGen = 0; 13 | 14 | constructor(promiseFunc, abortHandler) { 15 | 16 | let resolver; 17 | let rejecter; 18 | this.promise = new Promise((resolve, reject) => { 19 | resolver = resolve; 20 | rejecter = reject; 21 | }); 22 | 23 | const promiseResolve = resolver.bind(this); 24 | const promiseReject = rejecter.bind(this); 25 | 26 | const resolve = (...args) => { 27 | promiseResolve(...args); 28 | }; 29 | 30 | const reject = (error) => { 31 | promiseReject(error); 32 | }; 33 | 34 | promiseFunc(resolve.bind(this), reject.bind(this)); 35 | this.abortHandler = abortHandler; 36 | this.id = AbortablePromise.idGen++; 37 | } 38 | 39 | then(onResolve) { 40 | return new AbortablePromise((resolve, reject) => { 41 | this.promise = this.promise 42 | .then((...args) => { 43 | const onResolveResult = onResolve(...args); 44 | if (onResolveResult instanceof Promise || onResolveResult instanceof AbortablePromise) { 45 | onResolveResult.then((...args2) => { 46 | resolve(...args2); 47 | }); 48 | } else { 49 | resolve(onResolveResult); 50 | } 51 | }) 52 | .catch((error) => { 53 | reject(error); 54 | }); 55 | }, this.abortHandler); 56 | } 57 | 58 | catch(onFail) { 59 | return new AbortablePromise((resolve) => { 60 | this.promise = this.promise.then((...args) => { 61 | resolve(...args); 62 | }) 63 | .catch(onFail); 64 | }, this.abortHandler); 65 | } 66 | 67 | abort(reason) { 68 | if (this.abortHandler) this.abortHandler(reason); 69 | } 70 | 71 | } 72 | 73 | export class AbortedPromiseError extends Error { 74 | 75 | constructor(msg) { 76 | super(msg); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/ArrowHelper.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const _axis = new THREE.Vector3(); 4 | 5 | export class ArrowHelper extends THREE.Object3D { 6 | 7 | constructor(dir = new THREE.Vector3(0, 0, 1), origin = new THREE.Vector3(0, 0, 0), length = 1, 8 | radius = 0.1, color = 0xffff00, headLength = length * 0.2, headRadius = headLength * 0.2) { 9 | super(); 10 | 11 | this.type = 'ArrowHelper'; 12 | 13 | const lineGeometry = new THREE.CylinderGeometry(radius, radius, length, 32); 14 | lineGeometry.translate(0, length / 2.0, 0); 15 | const coneGeometry = new THREE.CylinderGeometry( 0, headRadius, headLength, 32); 16 | coneGeometry.translate(0, length, 0); 17 | 18 | this.position.copy( origin ); 19 | 20 | this.line = new THREE.Mesh(lineGeometry, new THREE.MeshBasicMaterial({color: color, toneMapped: false})); 21 | this.line.matrixAutoUpdate = false; 22 | this.add(this.line); 23 | 24 | this.cone = new THREE.Mesh(coneGeometry, new THREE.MeshBasicMaterial({color: color, toneMapped: false})); 25 | this.cone.matrixAutoUpdate = false; 26 | this.add(this.cone); 27 | 28 | this.setDirection(dir); 29 | } 30 | 31 | setDirection( dir ) { 32 | if (dir.y > 0.99999) { 33 | this.quaternion.set(0, 0, 0, 1); 34 | } else if (dir.y < - 0.99999) { 35 | this.quaternion.set(1, 0, 0, 0); 36 | } else { 37 | _axis.set(dir.z, 0, -dir.x).normalize(); 38 | const radians = Math.acos(dir.y); 39 | this.quaternion.setFromAxisAngle(_axis, radians); 40 | } 41 | } 42 | 43 | setColor( color ) { 44 | this.line.material.color.set(color); 45 | this.cone.material.color.set(color); 46 | } 47 | 48 | copy(source) { 49 | super.copy(source, false); 50 | this.line.copy(source.line); 51 | this.cone.copy(source.cone); 52 | return this; 53 | } 54 | 55 | dispose() { 56 | this.line.geometry.dispose(); 57 | this.line.material.dispose(); 58 | this.cone.geometry.dispose(); 59 | this.cone.material.dispose(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Constants.js: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | 3 | static DefaultSplatSortDistanceMapPrecision = 16; 4 | static MemoryPageSize = 65536; 5 | static BytesPerFloat = 4; 6 | static BytesPerInt = 4; 7 | static MaxScenes = 32; 8 | static ProgressiveLoadSectionSize = 262144; 9 | static ProgressiveLoadSectionDelayDuration = 15; 10 | static SphericalHarmonics8BitCompressionRange = 3; 11 | } 12 | -------------------------------------------------------------------------------- /src/DropInViewer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Viewer } from './Viewer.js'; 3 | 4 | /** 5 | * DropInViewer: Wrapper for a Viewer instance that enables it to be added to a Three.js scene like 6 | * any other Three.js scene object (Mesh, Object3D, etc.) 7 | */ 8 | export class DropInViewer extends THREE.Group { 9 | 10 | constructor(options = {}) { 11 | super(); 12 | 13 | options.selfDrivenMode = false; 14 | options.useBuiltInControls = false; 15 | options.rootElement = null; 16 | options.dropInMode = true; 17 | options.camera = undefined; 18 | options.renderer = undefined; 19 | 20 | this.viewer = new Viewer(options); 21 | this.splatMesh = null; 22 | this.updateSplatMesh(); 23 | 24 | this.callbackMesh = DropInViewer.createCallbackMesh(); 25 | this.add(this.callbackMesh); 26 | this.callbackMesh.onBeforeRender = DropInViewer.onBeforeRender.bind(this, this.viewer); 27 | 28 | this.viewer.onSplatMeshChanged(() => { 29 | this.updateSplatMesh(); 30 | }); 31 | 32 | } 33 | 34 | updateSplatMesh() { 35 | if (this.splatMesh !== this.viewer.splatMesh) { 36 | if (this.splatMesh) { 37 | this.remove(this.splatMesh); 38 | } 39 | this.splatMesh = this.viewer.splatMesh; 40 | this.add(this.viewer.splatMesh); 41 | } 42 | } 43 | 44 | /** 45 | * Add a single splat scene to the viewer. 46 | * @param {string} path Path to splat scene to be loaded 47 | * @param {object} options { 48 | * 49 | * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified 50 | * value (valid range: 0 - 255), defaults to 1 51 | * 52 | * showLoadingUI: Display a loading spinner while the scene is loading, defaults to true 53 | * 54 | * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] 55 | * 56 | * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] 57 | * 58 | * scale (Array): Scene's scale, defaults to [1, 1, 1] 59 | * 60 | * onProgress: Function to be called as file data are received 61 | * 62 | * } 63 | * @return {AbortablePromise} 64 | */ 65 | addSplatScene(path, options = {}) { 66 | if (options.showLoadingUI !== false) options.showLoadingUI = true; 67 | return this.viewer.addSplatScene(path, options); 68 | } 69 | 70 | /** 71 | * Add multiple splat scenes to the viewer. 72 | * @param {Array} sceneOptions Array of per-scene options: { 73 | * 74 | * path: Path to splat scene to be loaded 75 | * 76 | * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified 77 | * value (valid range: 0 - 255), defaults to 1 78 | * 79 | * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] 80 | * 81 | * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] 82 | * 83 | * scale (Array): Scene's scale, defaults to [1, 1, 1] 84 | * } 85 | * @param {boolean} showLoadingUI Display a loading spinner while the scene is loading, defaults to true 86 | * @return {AbortablePromise} 87 | */ 88 | addSplatScenes(sceneOptions, showLoadingUI) { 89 | if (showLoadingUI !== false) showLoadingUI = true; 90 | return this.viewer.addSplatScenes(sceneOptions, showLoadingUI); 91 | } 92 | 93 | /** 94 | * Get a reference to a splat scene. 95 | * @param {number} sceneIndex The index of the scene to which the reference will be returned 96 | * @return {SplatScene} 97 | */ 98 | getSplatScene(sceneIndex) { 99 | return this.viewer.getSplatScene(sceneIndex); 100 | } 101 | 102 | removeSplatScene(index, showLoadingUI = true) { 103 | return this.viewer.removeSplatScene(index, showLoadingUI); 104 | } 105 | 106 | removeSplatScenes(indexes, showLoadingUI = true) { 107 | return this.viewer.removeSplatScenes(indexes, showLoadingUI); 108 | } 109 | 110 | getSceneCount() { 111 | return this.viewer.getSceneCount(); 112 | } 113 | 114 | setActiveSphericalHarmonicsDegrees(activeSphericalHarmonicsDegrees) { 115 | this.viewer.setActiveSphericalHarmonicsDegrees(activeSphericalHarmonicsDegrees); 116 | } 117 | 118 | async dispose() { 119 | return await this.viewer.dispose(); 120 | } 121 | 122 | static onBeforeRender(viewer, renderer, threeScene, camera) { 123 | viewer.update(renderer, camera); 124 | } 125 | 126 | static createCallbackMesh() { 127 | const geometry = new THREE.SphereGeometry(1, 8, 8); 128 | const material = new THREE.MeshBasicMaterial(); 129 | material.colorWrite = false; 130 | material.depthWrite = false; 131 | const mesh = new THREE.Mesh(geometry, material); 132 | mesh.frustumCulled = false; 133 | return mesh; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/LogLevel.js: -------------------------------------------------------------------------------- 1 | export const LogLevel = { 2 | None: 0, 3 | Error: 1, 4 | Warning: 2, 5 | Info: 3, 6 | Debug: 4 7 | }; 8 | -------------------------------------------------------------------------------- /src/RenderMode.js: -------------------------------------------------------------------------------- 1 | export const RenderMode = { 2 | Always: 0, 3 | OnChange: 1, 4 | Never: 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/SceneRevealMode.js: -------------------------------------------------------------------------------- 1 | export const SceneRevealMode = { 2 | Default: 0, 3 | Gradual: 1, 4 | Instant: 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/SplatRenderMode.js: -------------------------------------------------------------------------------- 1 | export const SplatRenderMode = { 2 | ThreeD: 0, 3 | TwoD: 1 4 | }; 5 | -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | import { AbortablePromise, AbortedPromiseError } from './AbortablePromise.js'; 2 | 3 | export const floatToHalf = function() { 4 | 5 | const floatView = new Float32Array(1); 6 | const int32View = new Int32Array(floatView.buffer); 7 | 8 | return function(val) { 9 | floatView[0] = val; 10 | const x = int32View[0]; 11 | 12 | let bits = (x >> 16) & 0x8000; 13 | let m = (x >> 12) & 0x07ff; 14 | const e = (x >> 23) & 0xff; 15 | 16 | if (e < 103) return bits; 17 | 18 | if (e > 142) { 19 | bits |= 0x7c00; 20 | bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff); 21 | return bits; 22 | } 23 | 24 | if (e < 113) { 25 | m |= 0x0800; 26 | bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1); 27 | return bits; 28 | } 29 | 30 | bits |= (( e - 112) << 10) | (m >> 1); 31 | bits += m & 1; 32 | return bits; 33 | }; 34 | 35 | }(); 36 | 37 | export const uintEncodedFloat = function() { 38 | 39 | const floatView = new Float32Array(1); 40 | const int32View = new Int32Array(floatView.buffer); 41 | 42 | return function(f) { 43 | floatView[0] = f; 44 | return int32View[0]; 45 | }; 46 | 47 | }(); 48 | 49 | export const rgbaToInteger = function(r, g, b, a) { 50 | return r + (g << 8) + (b << 16) + (a << 24); 51 | }; 52 | 53 | export const rgbaArrayToInteger = function(arr, offset) { 54 | return arr[offset] + (arr[offset + 1] << 8) + (arr[offset + 2] << 16) + (arr[offset + 3] << 24); 55 | }; 56 | 57 | export const fetchWithProgress = function(path, onProgress, saveChunks = true, headers) { 58 | 59 | const abortController = new AbortController(); 60 | const signal = abortController.signal; 61 | let aborted = false; 62 | const abortHandler = (reason) => { 63 | abortController.abort(reason); 64 | aborted = true; 65 | }; 66 | 67 | let onProgressCalledAtComplete = false; 68 | const localOnProgress = (percent, percentLabel, chunk, fileSize) => { 69 | if (onProgress && !onProgressCalledAtComplete) { 70 | onProgress(percent, percentLabel, chunk, fileSize); 71 | if (percent === 100) { 72 | onProgressCalledAtComplete = true; 73 | } 74 | } 75 | }; 76 | 77 | return new AbortablePromise((resolve, reject) => { 78 | const fetchOptions = { signal }; 79 | if (headers) fetchOptions.headers = headers; 80 | fetch(path, fetchOptions) 81 | .then(async (data) => { 82 | // Handle error conditions where data is still returned 83 | if (!data.ok) { 84 | const errorText = await data.text(); 85 | reject(new Error(`Fetch failed: ${data.status} ${data.statusText} ${errorText}`)); 86 | return; 87 | } 88 | 89 | const reader = data.body.getReader(); 90 | let bytesDownloaded = 0; 91 | let _fileSize = data.headers.get('Content-Length'); 92 | let fileSize = _fileSize ? parseInt(_fileSize) : undefined; 93 | 94 | const chunks = []; 95 | 96 | while (!aborted) { 97 | try { 98 | const { value: chunk, done } = await reader.read(); 99 | if (done) { 100 | localOnProgress(100, '100%', chunk, fileSize); 101 | if (saveChunks) { 102 | const buffer = new Blob(chunks).arrayBuffer(); 103 | resolve(buffer); 104 | } else { 105 | resolve(); 106 | } 107 | break; 108 | } 109 | bytesDownloaded += chunk.length; 110 | let percent; 111 | let percentLabel; 112 | if (fileSize !== undefined) { 113 | percent = bytesDownloaded / fileSize * 100; 114 | percentLabel = `${percent.toFixed(2)}%`; 115 | } 116 | if (saveChunks) { 117 | chunks.push(chunk); 118 | } 119 | localOnProgress(percent, percentLabel, chunk, fileSize); 120 | } catch (error) { 121 | reject(error); 122 | return; 123 | } 124 | } 125 | }) 126 | .catch((error) => { 127 | reject(new AbortedPromiseError(error)); 128 | }); 129 | }, abortHandler); 130 | 131 | }; 132 | 133 | export const clamp = function(val, min, max) { 134 | return Math.max(Math.min(val, max), min); 135 | }; 136 | 137 | export const getCurrentTime = function() { 138 | return performance.now() / 1000; 139 | }; 140 | 141 | export const disposeAllMeshes = (object3D) => { 142 | if (object3D.geometry) { 143 | object3D.geometry.dispose(); 144 | object3D.geometry = null; 145 | } 146 | if (object3D.material) { 147 | object3D.material.dispose(); 148 | object3D.material = null; 149 | } 150 | if (object3D.children) { 151 | for (let child of object3D.children) { 152 | disposeAllMeshes(child); 153 | } 154 | } 155 | }; 156 | 157 | export const delayedExecute = (func, fast) => { 158 | return new Promise((resolve) => { 159 | window.setTimeout(() => { 160 | resolve(func ? func() : undefined); 161 | }, fast ? 1 : 50); 162 | }); 163 | }; 164 | 165 | 166 | export const getSphericalHarmonicsComponentCountForDegree = (sphericalHarmonicsDegree = 0) => { 167 | let shCoeffPerSplat = 0; 168 | if (sphericalHarmonicsDegree === 1) { 169 | shCoeffPerSplat = 9; 170 | } else if (sphericalHarmonicsDegree === 2) { 171 | shCoeffPerSplat = 24; 172 | } else if (sphericalHarmonicsDegree === 3) { 173 | shCoeffPerSplat = 45; 174 | } else if (sphericalHarmonicsDegree > 3) { 175 | throw new Error('getSphericalHarmonicsComponentCountForDegree() -> Invalid spherical harmonics degree'); 176 | } 177 | return shCoeffPerSplat; 178 | }; 179 | 180 | export const nativePromiseWithExtractedComponents = () => { 181 | let resolver; 182 | let rejecter; 183 | const promise = new Promise((resolve, reject) => { 184 | resolver = resolve; 185 | rejecter = reject; 186 | }); 187 | return { 188 | 'promise': promise, 189 | 'resolve': resolver, 190 | 'reject': rejecter 191 | }; 192 | }; 193 | 194 | export const abortablePromiseWithExtractedComponents = (abortHandler) => { 195 | let resolver; 196 | let rejecter; 197 | if (!abortHandler) { 198 | abortHandler = () => {}; 199 | } 200 | const promise = new AbortablePromise((resolve, reject) => { 201 | resolver = resolve; 202 | rejecter = reject; 203 | }, abortHandler); 204 | return { 205 | 'promise': promise, 206 | 'resolve': resolver, 207 | 'reject': rejecter 208 | }; 209 | }; 210 | 211 | class Semver { 212 | constructor(major, minor, patch) { 213 | this.major = major; 214 | this.minor = minor; 215 | this.patch = patch; 216 | } 217 | 218 | toString() { 219 | return `${this.major}_${this.minor}_${this.patch}`; 220 | } 221 | } 222 | 223 | export function isIOS() { 224 | const ua = navigator.userAgent; 225 | return ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0; 226 | } 227 | 228 | export function getIOSSemever() { 229 | if (isIOS()) { 230 | const extract = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); 231 | return new Semver( 232 | parseInt(extract[1] || 0, 10), 233 | parseInt(extract[2] || 0, 10), 234 | parseInt(extract[3] || 0, 10) 235 | ); 236 | } else { 237 | return null; // or [0,0,0] 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { PlyParser } from './loaders/ply/PlyParser.js'; 2 | import { PlayCanvasCompressedPlyParser } from './loaders/ply/PlayCanvasCompressedPlyParser.js'; 3 | import { PlyLoader } from './loaders/ply/PlyLoader.js'; 4 | import { SpzLoader } from './loaders/spz/SpzLoader.js'; 5 | import { SplatLoader } from './loaders/splat/SplatLoader.js'; 6 | import { KSplatLoader } from './loaders/ksplat/KSplatLoader.js'; 7 | import * as LoaderUtils from './loaders/Utils.js'; 8 | import { SplatBuffer } from './loaders/SplatBuffer.js'; 9 | import { SplatParser } from './loaders/splat/SplatParser.js'; 10 | import { SplatPartitioner } from './loaders/SplatPartitioner.js'; 11 | import { SplatBufferGenerator } from './loaders/SplatBufferGenerator.js'; 12 | import { Viewer } from './Viewer.js'; 13 | import { DropInViewer } from './DropInViewer.js'; 14 | import { OrbitControls } from './OrbitControls.js'; 15 | import { AbortablePromise } from './AbortablePromise.js'; 16 | import { SceneFormat } from './loaders/SceneFormat.js'; 17 | import { WebXRMode } from './webxr/WebXRMode.js'; 18 | import { RenderMode } from './RenderMode.js'; 19 | import { LogLevel } from './LogLevel.js'; 20 | import { SceneRevealMode } from './SceneRevealMode.js'; 21 | import { SplatRenderMode } from './SplatRenderMode.js'; 22 | 23 | export { 24 | PlyParser, 25 | PlayCanvasCompressedPlyParser, 26 | PlyLoader, 27 | SpzLoader, 28 | SplatLoader, 29 | KSplatLoader, 30 | LoaderUtils, 31 | SplatBuffer, 32 | SplatParser, 33 | SplatPartitioner, 34 | SplatBufferGenerator, 35 | Viewer, 36 | DropInViewer, 37 | OrbitControls, 38 | AbortablePromise, 39 | SceneFormat, 40 | WebXRMode, 41 | RenderMode, 42 | LogLevel, 43 | SceneRevealMode, 44 | SplatRenderMode 45 | }; 46 | -------------------------------------------------------------------------------- /src/loaders/Compression.js: -------------------------------------------------------------------------------- 1 | const createStream = (data)=> { 2 | return new ReadableStream({ 3 | async start(controller) { 4 | controller.enqueue(data); 5 | controller.close(); 6 | }, 7 | }); 8 | }; 9 | 10 | export async function decompressGzipped(data) { 11 | try { 12 | const stream = createStream(data); 13 | if (!stream) throw new Error('Failed to create stream from data'); 14 | 15 | return await decompressGzipStream(stream); 16 | } catch (error) { 17 | console.error('Error decompressing gzipped data:', error); 18 | throw error; 19 | } 20 | } 21 | 22 | export async function decompressGzipStream(stream) { 23 | const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')); 24 | const response = new Response(decompressedStream); 25 | const buffer = await response.arrayBuffer(); 26 | 27 | return new Uint8Array(buffer); 28 | } 29 | 30 | export async function compressGzipped(data) { 31 | try { 32 | const stream = createStream(data); 33 | const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); 34 | const response = new Response(compressedStream); 35 | const buffer = await response.arrayBuffer(); 36 | 37 | return new Uint8Array(buffer); 38 | } catch (error) { 39 | console.error('Error compressing gzipped data:', error); 40 | throw error; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/loaders/DirectLoadError.js: -------------------------------------------------------------------------------- 1 | export class DirectLoadError extends Error { 2 | 3 | constructor(msg) { 4 | super(msg); 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/loaders/InternalLoadType.js: -------------------------------------------------------------------------------- 1 | export const InternalLoadType = { 2 | ProgressiveToSplatBuffer: 0, 3 | ProgressiveToSplatArray: 1, 4 | DownloadBeforeProcessing: 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/loaders/LoaderStatus.js: -------------------------------------------------------------------------------- 1 | export const LoaderStatus = { 2 | 'Downloading': 0, 3 | 'Processing': 1, 4 | 'Done': 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/loaders/SceneFormat.js: -------------------------------------------------------------------------------- 1 | export const SceneFormat = { 2 | 'Splat': 0, 3 | 'KSplat': 1, 4 | 'Ply': 2, 5 | 'Spz': 3 6 | }; 7 | -------------------------------------------------------------------------------- /src/loaders/SplatBufferGenerator.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { SplatPartitioner } from './SplatPartitioner.js'; 3 | import { SplatBuffer } from './SplatBuffer.js'; 4 | 5 | export class SplatBufferGenerator { 6 | 7 | constructor(splatPartitioner, alphaRemovalThreshold, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) { 8 | this.splatPartitioner = splatPartitioner; 9 | this.alphaRemovalThreshold = alphaRemovalThreshold; 10 | this.compressionLevel = compressionLevel; 11 | this.sectionSize = sectionSize; 12 | this.sceneCenter = sceneCenter ? new THREE.Vector3().copy(sceneCenter) : undefined; 13 | this.blockSize = blockSize; 14 | this.bucketSize = bucketSize; 15 | } 16 | 17 | generateFromUncompressedSplatArray(splatArray) { 18 | const partitionResults = this.splatPartitioner.partitionUncompressedSplatArray(splatArray); 19 | return SplatBuffer.generateFromUncompressedSplatArrays(partitionResults.splatArrays, 20 | this.alphaRemovalThreshold, this.compressionLevel, 21 | this.sceneCenter, this.blockSize, this.bucketSize, 22 | partitionResults.parameters); 23 | } 24 | 25 | static getStandardGenerator(alphaRemovalThreshold = 1, compressionLevel = 1, sectionSize = 0, sceneCenter = new THREE.Vector3(), 26 | blockSize = SplatBuffer.BucketBlockSize, bucketSize = SplatBuffer.BucketSize) { 27 | const splatPartitioner = SplatPartitioner.getStandardPartitioner(sectionSize, sceneCenter, blockSize, bucketSize); 28 | return new SplatBufferGenerator(splatPartitioner, alphaRemovalThreshold, compressionLevel, 29 | sectionSize, sceneCenter, blockSize, bucketSize); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/loaders/SplatPartitioner.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { UncompressedSplatArray } from './UncompressedSplatArray.js'; 3 | import { SplatBuffer } from './SplatBuffer.js'; 4 | 5 | export class SplatPartitioner { 6 | 7 | constructor(sectionCount, sectionFilters, groupingParameters, partitionGenerator) { 8 | this.sectionCount = sectionCount; 9 | this.sectionFilters = sectionFilters; 10 | this.groupingParameters = groupingParameters; 11 | this.partitionGenerator = partitionGenerator; 12 | } 13 | 14 | partitionUncompressedSplatArray(splatArray) { 15 | let groupingParameters; 16 | let sectionCount; 17 | let sectionFilters; 18 | if (this.partitionGenerator) { 19 | const results = this.partitionGenerator(splatArray); 20 | groupingParameters = results.groupingParameters; 21 | sectionCount = results.sectionCount; 22 | sectionFilters = results.sectionFilters; 23 | } else { 24 | groupingParameters = this.groupingParameters; 25 | sectionCount = this.sectionCount; 26 | sectionFilters = this.sectionFilters; 27 | } 28 | 29 | const newArrays = []; 30 | for (let s = 0; s < sectionCount; s++) { 31 | const sectionSplats = new UncompressedSplatArray(splatArray.sphericalHarmonicsDegree); 32 | const sectionFilter = sectionFilters[s]; 33 | for (let i = 0; i < splatArray.splatCount; i++) { 34 | if (sectionFilter(i)) { 35 | sectionSplats.addSplat(splatArray.splats[i]); 36 | } 37 | } 38 | newArrays.push(sectionSplats); 39 | } 40 | return { 41 | splatArrays: newArrays, 42 | parameters: groupingParameters 43 | }; 44 | } 45 | 46 | static getStandardPartitioner(partitionSize = 0, sceneCenter = new THREE.Vector3(), 47 | blockSize = SplatBuffer.BucketBlockSize, bucketSize = SplatBuffer.BucketSize) { 48 | 49 | const partitionGenerator = (splatArray) => { 50 | 51 | const OFFSET_X = UncompressedSplatArray.OFFSET.X; 52 | const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; 53 | const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; 54 | 55 | if (partitionSize <= 0) partitionSize = splatArray.splatCount; 56 | 57 | const center = new THREE.Vector3(); 58 | const clampDistance = 0.5; 59 | const clampPoint = (point) => { 60 | point.x = Math.floor(point.x / clampDistance) * clampDistance; 61 | point.y = Math.floor(point.y / clampDistance) * clampDistance; 62 | point.z = Math.floor(point.z / clampDistance) * clampDistance; 63 | }; 64 | splatArray.splats.forEach((splat) => { 65 | center.set(splat[OFFSET_X], splat[OFFSET_Y], splat[OFFSET_Z]).sub(sceneCenter); 66 | clampPoint(center); 67 | splat.centerDist = center.lengthSq(); 68 | }); 69 | splatArray.splats.sort((a, b) => { 70 | let centerADist = a.centerDist; 71 | let centerBDist = b.centerDist; 72 | if (centerADist > centerBDist) return 1; 73 | else return -1; 74 | }); 75 | 76 | const sectionFilters = []; 77 | const groupingParameters = []; 78 | partitionSize = Math.min(splatArray.splatCount, partitionSize); 79 | const patitionCount = Math.ceil(splatArray.splatCount / partitionSize); 80 | let currentStartSplat = 0; 81 | for (let i = 0; i < patitionCount; i ++) { 82 | let startSplat = currentStartSplat; 83 | sectionFilters.push((splatIndex) => { 84 | return splatIndex >= startSplat && splatIndex < startSplat + partitionSize; 85 | }); 86 | groupingParameters.push({ 87 | 'blocksSize': blockSize, 88 | 'bucketSize': bucketSize, 89 | }); 90 | currentStartSplat += partitionSize; 91 | } 92 | return { 93 | 'sectionCount': sectionFilters.length, 94 | sectionFilters, 95 | groupingParameters 96 | }; 97 | }; 98 | return new SplatPartitioner(undefined, undefined, undefined, partitionGenerator); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/loaders/UncompressedSplatArray.js: -------------------------------------------------------------------------------- 1 | import { getSphericalHarmonicsComponentCountForDegree } from '../Util.js'; 2 | 3 | const BASE_COMPONENT_COUNT = 14; 4 | 5 | export class UncompressedSplatArray { 6 | 7 | static OFFSET = { 8 | X: 0, 9 | Y: 1, 10 | Z: 2, 11 | SCALE0: 3, 12 | SCALE1: 4, 13 | SCALE2: 5, 14 | ROTATION0: 6, 15 | ROTATION1: 7, 16 | ROTATION2: 8, 17 | ROTATION3: 9, 18 | FDC0: 10, 19 | FDC1: 11, 20 | FDC2: 12, 21 | OPACITY: 13, 22 | FRC0: 14, 23 | FRC1: 15, 24 | FRC2: 16, 25 | FRC3: 17, 26 | FRC4: 18, 27 | FRC5: 19, 28 | FRC6: 20, 29 | FRC7: 21, 30 | FRC8: 22, 31 | FRC9: 23, 32 | FRC10: 24, 33 | FRC11: 25, 34 | FRC12: 26, 35 | FRC13: 27, 36 | FRC14: 28, 37 | FRC15: 29, 38 | FRC16: 30, 39 | FRC17: 31, 40 | FRC18: 32, 41 | FRC19: 33, 42 | FRC20: 34, 43 | FRC21: 35, 44 | FRC22: 36, 45 | FRC23: 37 46 | }; 47 | 48 | constructor(sphericalHarmonicsDegree = 0) { 49 | this.sphericalHarmonicsDegree = sphericalHarmonicsDegree; 50 | this.sphericalHarmonicsCount = getSphericalHarmonicsComponentCountForDegree(this.sphericalHarmonicsDegree); 51 | this.componentCount = this.sphericalHarmonicsCount + BASE_COMPONENT_COUNT; 52 | this.defaultSphericalHarmonics = new Array(this.sphericalHarmonicsCount).fill(0); 53 | this.splats = []; 54 | this.splatCount = 0; 55 | } 56 | 57 | static createSplat(sphericalHarmonicsDegree = 0) { 58 | const baseSplat = [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; 59 | let shEntries = getSphericalHarmonicsComponentCountForDegree(sphericalHarmonicsDegree); 60 | for (let i = 0; i < shEntries; i++) baseSplat.push(0); 61 | return baseSplat; 62 | } 63 | 64 | addSplat(splat) { 65 | this.splats.push(splat); 66 | this.splatCount++; 67 | } 68 | 69 | getSplat(index) { 70 | return this.splats[index]; 71 | } 72 | 73 | addDefaultSplat() { 74 | const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree); 75 | this.addSplat(newSplat); 76 | return newSplat; 77 | } 78 | 79 | addSplatFromComonents(x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...rest) { 80 | const newSplat = [x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...this.defaultSphericalHarmonics]; 81 | for (let i = 0; i < rest.length && i < this.sphericalHarmonicsCount; i++) { 82 | newSplat[i] = rest[i]; 83 | } 84 | this.addSplat(newSplat); 85 | return newSplat; 86 | } 87 | 88 | addSplatFromArray(src, srcIndex) { 89 | const srcSplat = src.splats[srcIndex]; 90 | const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree); 91 | for (let i = 0; i < this.componentCount && i < srcSplat.length; i++) { 92 | newSplat[i] = srcSplat[i]; 93 | } 94 | this.addSplat(newSplat); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/loaders/Utils.js: -------------------------------------------------------------------------------- 1 | import { SceneFormat } from './SceneFormat.js'; 2 | 3 | export const sceneFormatFromPath = (path) => { 4 | if (path.endsWith('.ply')) return SceneFormat.Ply; 5 | else if (path.endsWith('.splat')) return SceneFormat.Splat; 6 | else if (path.endsWith('.ksplat')) return SceneFormat.KSplat; 7 | else if (path.endsWith('.spz')) return SceneFormat.Spz; 8 | return null; 9 | }; 10 | -------------------------------------------------------------------------------- /src/loaders/ksplat/KSplatLoader.js: -------------------------------------------------------------------------------- 1 | import { SplatBuffer } from '../SplatBuffer.js'; 2 | import { fetchWithProgress, delayedExecute, nativePromiseWithExtractedComponents } from '../../Util.js'; 3 | import { LoaderStatus } from '../LoaderStatus.js'; 4 | import { Constants } from '../../Constants.js'; 5 | 6 | export class KSplatLoader { 7 | 8 | static checkVersion(buffer) { 9 | const minVersionMajor = SplatBuffer.CurrentMajorVersion; 10 | const minVersionMinor = SplatBuffer.CurrentMinorVersion; 11 | const header = SplatBuffer.parseHeader(buffer); 12 | if (header.versionMajor === minVersionMajor && 13 | header.versionMinor >= minVersionMinor || 14 | header.versionMajor > minVersionMajor) { 15 | return true; 16 | } else { 17 | throw new Error(`KSplat version not supported: v${header.versionMajor}.${header.versionMinor}. ` + 18 | `Minimum required: v${minVersionMajor}.${minVersionMinor}`); 19 | } 20 | }; 21 | 22 | static loadFromURL(fileName, externalOnProgress, progressiveLoadToSplatBuffer, onSectionBuilt, headers) { 23 | let directLoadBuffer; 24 | let directLoadSplatBuffer; 25 | 26 | let headerBuffer; 27 | let header; 28 | let headerLoaded = false; 29 | let headerLoading = false; 30 | 31 | let sectionHeadersBuffer; 32 | let sectionHeaders = []; 33 | let sectionHeadersLoaded = false; 34 | let sectionHeadersLoading = false; 35 | 36 | let numBytesLoaded = 0; 37 | let numBytesProgressivelyLoaded = 0; 38 | let totalBytesToDownload = 0; 39 | 40 | let downloadComplete = false; 41 | let loadComplete = false; 42 | let loadSectionQueued = false; 43 | 44 | let chunks = []; 45 | 46 | const directLoadPromise = nativePromiseWithExtractedComponents(); 47 | 48 | const checkAndLoadHeader = () => { 49 | if (!headerLoaded && !headerLoading && numBytesLoaded >= SplatBuffer.HeaderSizeBytes) { 50 | headerLoading = true; 51 | const headerAssemblyPromise = new Blob(chunks).arrayBuffer(); 52 | headerAssemblyPromise.then((bufferData) => { 53 | headerBuffer = new ArrayBuffer(SplatBuffer.HeaderSizeBytes); 54 | new Uint8Array(headerBuffer).set(new Uint8Array(bufferData, 0, SplatBuffer.HeaderSizeBytes)); 55 | KSplatLoader.checkVersion(headerBuffer); 56 | headerLoading = false; 57 | headerLoaded = true; 58 | header = SplatBuffer.parseHeader(headerBuffer); 59 | window.setTimeout(() => { 60 | checkAndLoadSectionHeaders(); 61 | }, 1); 62 | }); 63 | } 64 | }; 65 | 66 | let queuedCheckAndLoadSectionsCount = 0; 67 | const queueCheckAndLoadSections = () => { 68 | if (queuedCheckAndLoadSectionsCount === 0) { 69 | queuedCheckAndLoadSectionsCount++; 70 | window.setTimeout(() => { 71 | queuedCheckAndLoadSectionsCount--; 72 | checkAndLoadSections(); 73 | }, 1); 74 | } 75 | }; 76 | 77 | const checkAndLoadSectionHeaders = () => { 78 | const performLoad = () => { 79 | sectionHeadersLoading = true; 80 | const sectionHeadersAssemblyPromise = new Blob(chunks).arrayBuffer(); 81 | sectionHeadersAssemblyPromise.then((bufferData) => { 82 | sectionHeadersLoading = false; 83 | sectionHeadersLoaded = true; 84 | sectionHeadersBuffer = new ArrayBuffer(header.maxSectionCount * SplatBuffer.SectionHeaderSizeBytes); 85 | new Uint8Array(sectionHeadersBuffer).set(new Uint8Array(bufferData, SplatBuffer.HeaderSizeBytes, 86 | header.maxSectionCount * SplatBuffer.SectionHeaderSizeBytes)); 87 | sectionHeaders = SplatBuffer.parseSectionHeaders(header, sectionHeadersBuffer, 0, false); 88 | let totalSectionStorageStorageByes = 0; 89 | for (let i = 0; i < header.maxSectionCount; i++) { 90 | totalSectionStorageStorageByes += sectionHeaders[i].storageSizeBytes; 91 | } 92 | const totalStorageSizeBytes = SplatBuffer.HeaderSizeBytes + header.maxSectionCount * 93 | SplatBuffer.SectionHeaderSizeBytes + totalSectionStorageStorageByes; 94 | if (!directLoadBuffer) { 95 | directLoadBuffer = new ArrayBuffer(totalStorageSizeBytes); 96 | let offset = 0; 97 | for (let i = 0; i < chunks.length; i++) { 98 | const chunk = chunks[i]; 99 | new Uint8Array(directLoadBuffer, offset, chunk.byteLength).set(new Uint8Array(chunk)); 100 | offset += chunk.byteLength; 101 | } 102 | } 103 | 104 | totalBytesToDownload = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount; 105 | for (let i = 0; i <= sectionHeaders.length && i < header.maxSectionCount; i++) { 106 | totalBytesToDownload += sectionHeaders[i].storageSizeBytes; 107 | } 108 | 109 | queueCheckAndLoadSections(); 110 | }); 111 | }; 112 | 113 | if (!sectionHeadersLoading && !sectionHeadersLoaded && headerLoaded && 114 | numBytesLoaded >= SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount) { 115 | performLoad(); 116 | } 117 | }; 118 | 119 | const checkAndLoadSections = () => { 120 | if (loadSectionQueued) return; 121 | loadSectionQueued = true; 122 | const checkAndLoadFunc = () => { 123 | loadSectionQueued = false; 124 | if (sectionHeadersLoaded) { 125 | 126 | if (loadComplete) return; 127 | 128 | downloadComplete = numBytesLoaded >= totalBytesToDownload; 129 | 130 | let bytesLoadedSinceLastSection = numBytesLoaded - numBytesProgressivelyLoaded; 131 | if (bytesLoadedSinceLastSection > Constants.ProgressiveLoadSectionSize || downloadComplete) { 132 | 133 | numBytesProgressivelyLoaded += Constants.ProgressiveLoadSectionSize; 134 | loadComplete = numBytesProgressivelyLoaded >= totalBytesToDownload; 135 | 136 | if (!directLoadSplatBuffer) directLoadSplatBuffer = new SplatBuffer(directLoadBuffer, false); 137 | 138 | const baseDataOffset = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes * header.maxSectionCount; 139 | let sectionBase = 0; 140 | let reachedSections = 0; 141 | let loadedSplatCount = 0; 142 | for (let i = 0; i < header.maxSectionCount; i++) { 143 | const sectionHeader = sectionHeaders[i]; 144 | const bucketsDataOffset = sectionBase + sectionHeader.partiallyFilledBucketCount * 4 + 145 | sectionHeader.bucketStorageSizeBytes * sectionHeader.bucketCount; 146 | const bytesRequiredToReachSectionSplatData = baseDataOffset + bucketsDataOffset; 147 | if (numBytesProgressivelyLoaded >= bytesRequiredToReachSectionSplatData) { 148 | reachedSections++; 149 | const bytesPastSSectionSplatDataStart = numBytesProgressivelyLoaded - bytesRequiredToReachSectionSplatData; 150 | const baseDescriptor = SplatBuffer.CompressionLevels[header.compressionLevel]; 151 | const shDesc = baseDescriptor.SphericalHarmonicsDegrees[sectionHeader.sphericalHarmonicsDegree]; 152 | const bytesPerSplat = shDesc.BytesPerSplat; 153 | let loadedSplatsForSection = Math.floor(bytesPastSSectionSplatDataStart / bytesPerSplat); 154 | loadedSplatsForSection = Math.min(loadedSplatsForSection, sectionHeader.maxSplatCount); 155 | loadedSplatCount += loadedSplatsForSection; 156 | directLoadSplatBuffer.updateLoadedCounts(reachedSections, loadedSplatCount); 157 | directLoadSplatBuffer.updateSectionLoadedCounts(i, loadedSplatsForSection); 158 | } else { 159 | break; 160 | } 161 | sectionBase += sectionHeader.storageSizeBytes; 162 | } 163 | 164 | onSectionBuilt(directLoadSplatBuffer, loadComplete); 165 | 166 | const percentComplete = numBytesProgressivelyLoaded / totalBytesToDownload * 100; 167 | const percentLabel = (percentComplete).toFixed(2) + '%'; 168 | 169 | if (externalOnProgress) externalOnProgress(percentComplete, percentLabel, LoaderStatus.Downloading); 170 | 171 | if (loadComplete) { 172 | directLoadPromise.resolve(directLoadSplatBuffer); 173 | } else { 174 | checkAndLoadSections(); 175 | } 176 | } 177 | } 178 | }; 179 | window.setTimeout(checkAndLoadFunc, Constants.ProgressiveLoadSectionDelayDuration); 180 | }; 181 | 182 | const localOnProgress = (percent, percentStr, chunk) => { 183 | if (chunk) { 184 | chunks.push(chunk); 185 | if (directLoadBuffer) { 186 | new Uint8Array(directLoadBuffer, numBytesLoaded, chunk.byteLength).set(new Uint8Array(chunk)); 187 | } 188 | numBytesLoaded += chunk.byteLength; 189 | } 190 | if (progressiveLoadToSplatBuffer) { 191 | checkAndLoadHeader(); 192 | checkAndLoadSectionHeaders(); 193 | checkAndLoadSections(); 194 | } else { 195 | if (externalOnProgress) externalOnProgress(percent, percentStr, LoaderStatus.Downloading); 196 | } 197 | }; 198 | 199 | return fetchWithProgress(fileName, localOnProgress, !progressiveLoadToSplatBuffer, headers).then((fullBuffer) => { 200 | if (externalOnProgress) externalOnProgress(0, '0%', LoaderStatus.Processing); 201 | const loadPromise = progressiveLoadToSplatBuffer ? directLoadPromise.promise : KSplatLoader.loadFromFileData(fullBuffer); 202 | return loadPromise.then((splatBuffer) => { 203 | if (externalOnProgress) externalOnProgress(100, '100%', LoaderStatus.Done); 204 | return splatBuffer; 205 | }); 206 | }); 207 | } 208 | 209 | static loadFromFileData(fileData) { 210 | return delayedExecute(() => { 211 | KSplatLoader.checkVersion(fileData); 212 | return new SplatBuffer(fileData); 213 | }); 214 | } 215 | 216 | static downloadFile = function() { 217 | 218 | let downLoadLink; 219 | 220 | return function(splatBuffer, fileName) { 221 | const blob = new Blob([splatBuffer.bufferData], { 222 | type: 'application/octet-stream', 223 | }); 224 | 225 | if (!downLoadLink) { 226 | downLoadLink = document.createElement('a'); 227 | document.body.appendChild(downLoadLink); 228 | } 229 | downLoadLink.download = fileName; 230 | downLoadLink.href = URL.createObjectURL(blob); 231 | downLoadLink.click(); 232 | }; 233 | 234 | }(); 235 | 236 | } 237 | -------------------------------------------------------------------------------- /src/loaders/ply/INRIAV1PlyParser.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { clamp } from '../../Util.js'; 3 | import { UncompressedSplatArray } from '../UncompressedSplatArray.js'; 4 | import { SplatBuffer } from '../SplatBuffer.js'; 5 | import { PlyParserUtils } from './PlyParserUtils.js'; 6 | 7 | const BaseFieldNamesToRead = ['scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3', 'x', 'y', 'z', 8 | 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'red', 'green', 'blue', 'f_rest_0']; 9 | 10 | const BaseFieldsToReadIndexes = BaseFieldNamesToRead.map((e, i) => i); 11 | 12 | const [ 13 | SCALE_0, SCALE_1, SCALE_2, ROT_0, ROT_1, ROT_2, ROT_3, X, Y, Z, F_DC_0, F_DC_1, F_DC_2, OPACITY, RED, GREEN, BLUE, F_REST_0 14 | ] = BaseFieldsToReadIndexes; 15 | 16 | export class INRIAV1PlyParser { 17 | 18 | static decodeHeaderLines(headerLines) { 19 | 20 | let shLineCount = 0; 21 | headerLines.forEach((line) => { 22 | if (line.includes('f_rest_')) shLineCount++; 23 | }); 24 | 25 | let shFieldsToReadCount = 0; 26 | if (shLineCount >= 45) { 27 | shFieldsToReadCount = 45; 28 | } else if (shLineCount >= 24) { 29 | shFieldsToReadCount = 24; 30 | } else if (shLineCount >= 9) { 31 | shFieldsToReadCount = 9; 32 | } 33 | 34 | const shFieldIndexesToMap = Array.from(Array(Math.max(shFieldsToReadCount - 1, 0))); 35 | let shRemainingFieldNamesToRead = shFieldIndexesToMap.map((element, index) => `f_rest_${index + 1}`); 36 | 37 | const fieldNamesToRead = [...BaseFieldNamesToRead, ...shRemainingFieldNamesToRead]; 38 | const fieldsToReadIndexes = fieldNamesToRead.map((e, i) => i); 39 | 40 | const fieldNameIdMap = fieldsToReadIndexes.reduce((acc, element) => { 41 | acc[fieldNamesToRead[element]] = element; 42 | return acc; 43 | }, {}); 44 | const header = PlyParserUtils.decodeSectionHeader(headerLines, fieldNameIdMap, 0); 45 | header.splatCount = header.vertexCount; 46 | header.bytesPerSplat = header.bytesPerVertex; 47 | header.fieldsToReadIndexes = fieldsToReadIndexes; 48 | return header; 49 | } 50 | 51 | static decodeHeaderText(headerText) { 52 | const headerLines = PlyParserUtils.convertHeaderTextToLines(headerText); 53 | const header = INRIAV1PlyParser.decodeHeaderLines(headerLines); 54 | header.headerText = headerText; 55 | header.headerSizeBytes = headerText.indexOf(PlyParserUtils.HeaderEndToken) + PlyParserUtils.HeaderEndToken.length + 1; 56 | return header; 57 | } 58 | 59 | static decodeHeaderFromBuffer(plyBuffer) { 60 | const headerText = PlyParserUtils.readHeaderFromBuffer(plyBuffer); 61 | return INRIAV1PlyParser.decodeHeaderText(headerText); 62 | } 63 | 64 | static findSplatData(plyBuffer, header) { 65 | return new DataView(plyBuffer, header.headerSizeBytes); 66 | } 67 | 68 | static parseToUncompressedSplatBufferSection(header, fromSplat, toSplat, splatData, splatDataOffset, 69 | toBuffer, toOffset, outSphericalHarmonicsDegree = 0) { 70 | outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); 71 | const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[outSphericalHarmonicsDegree].BytesPerSplat; 72 | 73 | for (let i = fromSplat; i <= toSplat; i++) { 74 | const parsedSplat = INRIAV1PlyParser.parseToUncompressedSplat(splatData, i, header, 75 | splatDataOffset, outSphericalHarmonicsDegree); 76 | const outBase = i * outBytesPerSplat + toOffset; 77 | SplatBuffer.writeSplatDataToSectionBuffer(parsedSplat, toBuffer, outBase, 0, outSphericalHarmonicsDegree); 78 | } 79 | } 80 | 81 | static parseToUncompressedSplatArraySection(header, fromSplat, toSplat, splatData, splatDataOffset, 82 | splatArray, outSphericalHarmonicsDegree = 0) { 83 | outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); 84 | for (let i = fromSplat; i <= toSplat; i++) { 85 | const parsedSplat = INRIAV1PlyParser.parseToUncompressedSplat(splatData, i, header, 86 | splatDataOffset, outSphericalHarmonicsDegree); 87 | splatArray.addSplat(parsedSplat); 88 | } 89 | } 90 | 91 | static decodeSectionSplatData(sectionSplatData, splatCount, sectionHeader, outSphericalHarmonicsDegree, toSplatArray = true) { 92 | outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, sectionHeader.sphericalHarmonicsDegree); 93 | if (toSplatArray) { 94 | const splatArray = new UncompressedSplatArray(outSphericalHarmonicsDegree); 95 | for (let row = 0; row < splatCount; row++) { 96 | const newSplat = INRIAV1PlyParser.parseToUncompressedSplat(sectionSplatData, row, sectionHeader, 97 | 0, outSphericalHarmonicsDegree); 98 | splatArray.addSplat(newSplat); 99 | } 100 | return splatArray; 101 | } else { 102 | const { 103 | splatBuffer, 104 | splatBufferDataOffsetBytes 105 | } = SplatBuffer.preallocateUncompressed(splatCount, outSphericalHarmonicsDegree); 106 | INRIAV1PlyParser.parseToUncompressedSplatBufferSection( 107 | sectionHeader, 0, splatCount - 1, sectionSplatData, 0, 108 | splatBuffer.bufferData, splatBufferDataOffsetBytes, outSphericalHarmonicsDegree 109 | ); 110 | return splatBuffer; 111 | } 112 | } 113 | 114 | static parseToUncompressedSplat = function() { 115 | 116 | let rawSplat = []; 117 | const tempRotation = new THREE.Quaternion(); 118 | 119 | const OFFSET_X = UncompressedSplatArray.OFFSET.X; 120 | const OFFSET_Y = UncompressedSplatArray.OFFSET.Y; 121 | const OFFSET_Z = UncompressedSplatArray.OFFSET.Z; 122 | 123 | const OFFSET_SCALE0 = UncompressedSplatArray.OFFSET.SCALE0; 124 | const OFFSET_SCALE1 = UncompressedSplatArray.OFFSET.SCALE1; 125 | const OFFSET_SCALE2 = UncompressedSplatArray.OFFSET.SCALE2; 126 | 127 | const OFFSET_ROTATION0 = UncompressedSplatArray.OFFSET.ROTATION0; 128 | const OFFSET_ROTATION1 = UncompressedSplatArray.OFFSET.ROTATION1; 129 | const OFFSET_ROTATION2 = UncompressedSplatArray.OFFSET.ROTATION2; 130 | const OFFSET_ROTATION3 = UncompressedSplatArray.OFFSET.ROTATION3; 131 | 132 | const OFFSET_FDC0 = UncompressedSplatArray.OFFSET.FDC0; 133 | const OFFSET_FDC1 = UncompressedSplatArray.OFFSET.FDC1; 134 | const OFFSET_FDC2 = UncompressedSplatArray.OFFSET.FDC2; 135 | const OFFSET_OPACITY = UncompressedSplatArray.OFFSET.OPACITY; 136 | 137 | const OFFSET_FRC = []; 138 | 139 | for (let i = 0; i < 45; i++) { 140 | OFFSET_FRC[i] = UncompressedSplatArray.OFFSET.FRC0 + i; 141 | } 142 | 143 | return function(splatData, row, header, splatDataOffset = 0, outSphericalHarmonicsDegree = 0) { 144 | outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, header.sphericalHarmonicsDegree); 145 | INRIAV1PlyParser.readSplat(splatData, header, row, splatDataOffset, rawSplat); 146 | const newSplat = UncompressedSplatArray.createSplat(outSphericalHarmonicsDegree); 147 | if (rawSplat[SCALE_0] !== undefined) { 148 | newSplat[OFFSET_SCALE0] = Math.exp(rawSplat[SCALE_0]); 149 | newSplat[OFFSET_SCALE1] = Math.exp(rawSplat[SCALE_1]); 150 | newSplat[OFFSET_SCALE2] = Math.exp(rawSplat[SCALE_2]); 151 | } else { 152 | newSplat[OFFSET_SCALE0] = 0.01; 153 | newSplat[OFFSET_SCALE1] = 0.01; 154 | newSplat[OFFSET_SCALE2] = 0.01; 155 | } 156 | 157 | if (rawSplat[F_DC_0] !== undefined) { 158 | const SH_C0 = 0.28209479177387814; 159 | newSplat[OFFSET_FDC0] = (0.5 + SH_C0 * rawSplat[F_DC_0]) * 255; 160 | newSplat[OFFSET_FDC1] = (0.5 + SH_C0 * rawSplat[F_DC_1]) * 255; 161 | newSplat[OFFSET_FDC2] = (0.5 + SH_C0 * rawSplat[F_DC_2]) * 255; 162 | } else if (rawSplat[RED] !== undefined) { 163 | newSplat[OFFSET_FDC0] = rawSplat[RED] * 255; 164 | newSplat[OFFSET_FDC1] = rawSplat[GREEN] * 255; 165 | newSplat[OFFSET_FDC2] = rawSplat[BLUE] * 255; 166 | } else { 167 | newSplat[OFFSET_FDC0] = 0; 168 | newSplat[OFFSET_FDC1] = 0; 169 | newSplat[OFFSET_FDC2] = 0; 170 | } 171 | 172 | if (rawSplat[OPACITY] !== undefined) { 173 | newSplat[OFFSET_OPACITY] = (1 / (1 + Math.exp(-rawSplat[OPACITY]))) * 255; 174 | } 175 | 176 | newSplat[OFFSET_FDC0] = clamp(Math.floor(newSplat[OFFSET_FDC0]), 0, 255); 177 | newSplat[OFFSET_FDC1] = clamp(Math.floor(newSplat[OFFSET_FDC1]), 0, 255); 178 | newSplat[OFFSET_FDC2] = clamp(Math.floor(newSplat[OFFSET_FDC2]), 0, 255); 179 | newSplat[OFFSET_OPACITY] = clamp(Math.floor(newSplat[OFFSET_OPACITY]), 0, 255); 180 | 181 | if (outSphericalHarmonicsDegree >= 1) { 182 | if (rawSplat[F_REST_0] !== undefined) { 183 | for (let i = 0; i < 9; i++) { 184 | newSplat[OFFSET_FRC[i]] = rawSplat[header.sphericalHarmonicsDegree1Fields[i]]; 185 | } 186 | if (outSphericalHarmonicsDegree >= 2) { 187 | for (let i = 0; i < 15; i++) { 188 | newSplat[OFFSET_FRC[9 + i]] = rawSplat[header.sphericalHarmonicsDegree2Fields[i]]; 189 | } 190 | } 191 | } 192 | } 193 | 194 | tempRotation.set(rawSplat[ROT_0], rawSplat[ROT_1], rawSplat[ROT_2], rawSplat[ROT_3]); 195 | tempRotation.normalize(); 196 | 197 | newSplat[OFFSET_ROTATION0] = tempRotation.x; 198 | newSplat[OFFSET_ROTATION1] = tempRotation.y; 199 | newSplat[OFFSET_ROTATION2] = tempRotation.z; 200 | newSplat[OFFSET_ROTATION3] = tempRotation.w; 201 | 202 | newSplat[OFFSET_X] = rawSplat[X]; 203 | newSplat[OFFSET_Y] = rawSplat[Y]; 204 | newSplat[OFFSET_Z] = rawSplat[Z]; 205 | 206 | return newSplat; 207 | }; 208 | 209 | }(); 210 | 211 | static readSplat(splatData, header, row, dataOffset, rawSplat) { 212 | return PlyParserUtils.readVertex(splatData, header, row, dataOffset, header.fieldsToReadIndexes, rawSplat, true); 213 | } 214 | 215 | static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) { 216 | const { header, splatCount, splatData } = separatePlyHeaderAndData(plyBuffer); 217 | return INRIAV1PlyParser.decodeSectionSplatData(splatData, splatCount, header, outSphericalHarmonicsDegree, true); 218 | } 219 | 220 | static parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree = 0) { 221 | const { header, splatCount, splatData } = separatePlyHeaderAndData(plyBuffer); 222 | return INRIAV1PlyParser.decodeSectionSplatData(splatData, splatCount, header, outSphericalHarmonicsDegree, false); 223 | } 224 | } 225 | 226 | function separatePlyHeaderAndData(plyBuffer) { 227 | const header = INRIAV1PlyParser.decodeHeaderFromBuffer(plyBuffer); 228 | const splatCount = header.splatCount; 229 | const splatData = INRIAV1PlyParser.findSplatData(plyBuffer, header); 230 | return { 231 | header, 232 | splatCount, 233 | splatData 234 | }; 235 | } 236 | -------------------------------------------------------------------------------- /src/loaders/ply/PlyFormat.js: -------------------------------------------------------------------------------- 1 | export const PlyFormat = { 2 | 'INRIAV1': 0, 3 | 'INRIAV2': 1, 4 | 'PlayCanvasCompressed': 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/loaders/ply/PlyParser.js: -------------------------------------------------------------------------------- 1 | import { PlayCanvasCompressedPlyParser } from './PlayCanvasCompressedPlyParser.js'; 2 | import { INRIAV1PlyParser } from './INRIAV1PlyParser.js'; 3 | import { INRIAV2PlyParser } from './INRIAV2PlyParser.js'; 4 | import { PlyParserUtils } from './PlyParserUtils.js'; 5 | import { PlyFormat } from './PlyFormat.js'; 6 | 7 | export class PlyParser { 8 | 9 | static parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree = 0) { 10 | const plyFormat = PlyParserUtils.determineHeaderFormatFromPlyBuffer(plyBuffer); 11 | if (plyFormat === PlyFormat.PlayCanvasCompressed) { 12 | return PlayCanvasCompressedPlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); 13 | } else if (plyFormat === PlyFormat.INRIAV1) { 14 | return INRIAV1PlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); 15 | } else if (plyFormat === PlyFormat.INRIAV2) { 16 | return INRIAV2PlyParser.parseToUncompressedSplatArray(plyBuffer, outSphericalHarmonicsDegree); 17 | } 18 | } 19 | 20 | static parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree = 0) { 21 | const plyFormat = PlyParserUtils.determineHeaderFormatFromPlyBuffer(plyBuffer); 22 | if (plyFormat === PlyFormat.PlayCanvasCompressed) { 23 | return PlayCanvasCompressedPlyParser.parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree); 24 | } else if (plyFormat === PlyFormat.INRIAV1) { 25 | return INRIAV1PlyParser.parseToUncompressedSplatBuffer(plyBuffer, outSphericalHarmonicsDegree); 26 | } else if (plyFormat === PlyFormat.INRIAV2) { 27 | // TODO: Implement! 28 | throw new Error('parseToUncompressedSplatBuffer() is not implemented for INRIA V2 PLY files'); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/loaders/ply/PlyParserUtils.js: -------------------------------------------------------------------------------- 1 | import { PlyFormat } from './PlyFormat.js'; 2 | 3 | const [ 4 | FieldSizeIdDouble, FieldSizeIdInt, FieldSizeIdUInt, FieldSizeIdFloat, FieldSizeIdShort, FieldSizeIdUShort, FieldSizeIdUChar 5 | ] = [0, 1, 2, 3, 4, 5, 6]; 6 | 7 | const FieldSizeStringMap = { 8 | 'double': FieldSizeIdDouble, 9 | 'int': FieldSizeIdInt, 10 | 'uint': FieldSizeIdUInt, 11 | 'float': FieldSizeIdFloat, 12 | 'short': FieldSizeIdShort, 13 | 'ushort': FieldSizeIdUShort, 14 | 'uchar': FieldSizeIdUChar, 15 | }; 16 | 17 | const FieldSize = { 18 | [FieldSizeIdDouble]: 8, 19 | [FieldSizeIdInt]: 4, 20 | [FieldSizeIdUInt]: 4, 21 | [FieldSizeIdFloat]: 4, 22 | [FieldSizeIdShort]: 2, 23 | [FieldSizeIdUShort]: 2, 24 | [FieldSizeIdUChar]: 1, 25 | }; 26 | 27 | export class PlyParserUtils { 28 | 29 | static HeaderEndToken = 'end_header'; 30 | 31 | static decodeSectionHeader(headerLines, fieldNameIdMap, headerStartLine = 0) { 32 | 33 | const extractedLines = []; 34 | 35 | let processingSection = false; 36 | let headerEndLine = -1; 37 | let vertexCount = 0; 38 | let endOfHeader = false; 39 | let sectionName = null; 40 | 41 | const fieldIds = []; 42 | const fieldTypes = []; 43 | const allFieldNames = []; 44 | const usedFieldNames = []; 45 | const fieldTypesByName = {}; 46 | 47 | for (let i = headerStartLine; i < headerLines.length; i++) { 48 | const line = headerLines[i].trim(); 49 | if (line.startsWith('element')) { 50 | if (processingSection) { 51 | headerEndLine--; 52 | break; 53 | } else { 54 | processingSection = true; 55 | headerStartLine = i; 56 | headerEndLine = i; 57 | const lineComponents = line.split(' '); 58 | let validComponents = 0; 59 | for (let lineComponent of lineComponents) { 60 | const trimmedComponent = lineComponent.trim(); 61 | if (trimmedComponent.length > 0) { 62 | validComponents++; 63 | if (validComponents === 2) { 64 | sectionName = trimmedComponent; 65 | } else if (validComponents === 3) { 66 | vertexCount = parseInt(trimmedComponent); 67 | } 68 | } 69 | } 70 | } 71 | } else if (line.startsWith('property')) { 72 | const fieldMatch = line.match(/(\w+)\s+(\w+)\s+(\w+)/); 73 | if (fieldMatch) { 74 | const fieldTypeStr = fieldMatch[2]; 75 | const fieldName = fieldMatch[3]; 76 | allFieldNames.push(fieldName); 77 | const fieldId = fieldNameIdMap[fieldName]; 78 | fieldTypesByName[fieldName] = fieldTypeStr; 79 | const fieldType = FieldSizeStringMap[fieldTypeStr]; 80 | if (fieldId !== undefined) { 81 | usedFieldNames.push(fieldName); 82 | fieldIds.push(fieldId); 83 | fieldTypes[fieldId] = fieldType; 84 | } 85 | } 86 | } 87 | if (line === PlyParserUtils.HeaderEndToken) { 88 | endOfHeader = true; 89 | break; 90 | } 91 | if (processingSection) { 92 | extractedLines.push(line); 93 | headerEndLine++; 94 | } 95 | } 96 | 97 | const fieldOffsets = []; 98 | let bytesPerVertex = 0; 99 | for (let fieldName of allFieldNames) { 100 | const fieldType = fieldTypesByName[fieldName]; 101 | if (fieldTypesByName.hasOwnProperty(fieldName)) { 102 | const fieldId = fieldNameIdMap[fieldName]; 103 | if (fieldId !== undefined) { 104 | fieldOffsets[fieldId] = bytesPerVertex; 105 | } 106 | } 107 | bytesPerVertex += FieldSize[FieldSizeStringMap[fieldType]]; 108 | } 109 | 110 | const sphericalHarmonics = PlyParserUtils.decodeSphericalHarmonicsFromSectionHeader(allFieldNames, fieldNameIdMap); 111 | 112 | return { 113 | 'headerLines': extractedLines, 114 | 'headerStartLine': headerStartLine, 115 | 'headerEndLine': headerEndLine, 116 | 'fieldTypes': fieldTypes, 117 | 'fieldIds': fieldIds, 118 | 'fieldOffsets': fieldOffsets, 119 | 'bytesPerVertex': bytesPerVertex, 120 | 'vertexCount': vertexCount, 121 | 'dataSizeBytes': bytesPerVertex * vertexCount, 122 | 'endOfHeader': endOfHeader, 123 | 'sectionName': sectionName, 124 | 'sphericalHarmonicsDegree': sphericalHarmonics.degree, 125 | 'sphericalHarmonicsCoefficientsPerChannel': sphericalHarmonics.coefficientsPerChannel, 126 | 'sphericalHarmonicsDegree1Fields': sphericalHarmonics.degree1Fields, 127 | 'sphericalHarmonicsDegree2Fields': sphericalHarmonics.degree2Fields 128 | }; 129 | 130 | } 131 | 132 | static decodeSphericalHarmonicsFromSectionHeader(fieldNames, fieldNameIdMap) { 133 | let sphericalHarmonicsFieldCount = 0; 134 | let coefficientsPerChannel = 0; 135 | for (let fieldName of fieldNames) { 136 | if (fieldName.startsWith('f_rest')) sphericalHarmonicsFieldCount++; 137 | } 138 | coefficientsPerChannel = sphericalHarmonicsFieldCount / 3; 139 | let degree = 0; 140 | if (coefficientsPerChannel >= 3) degree = 1; 141 | if (coefficientsPerChannel >= 8) degree = 2; 142 | 143 | let degree1Fields = []; 144 | let degree2Fields = []; 145 | 146 | for (let rgb = 0; rgb < 3; rgb++) { 147 | if (degree >= 1) { 148 | for (let i = 0; i < 3; i++) { 149 | degree1Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb)]); 150 | } 151 | } 152 | if (degree >= 2) { 153 | for (let i = 0; i < 5; i++) { 154 | degree2Fields.push(fieldNameIdMap['f_rest_' + (i + coefficientsPerChannel * rgb + 3)]); 155 | } 156 | } 157 | } 158 | 159 | return { 160 | 'degree': degree, 161 | 'coefficientsPerChannel': coefficientsPerChannel, 162 | 'degree1Fields': degree1Fields, 163 | 'degree2Fields': degree2Fields 164 | }; 165 | } 166 | 167 | static getHeaderSectionNames(headerLines) { 168 | const sectionNames = []; 169 | for (let headerLine of headerLines) { 170 | if (headerLine.startsWith('element')) { 171 | const lineComponents = headerLine.split(' '); 172 | let validComponents = 0; 173 | for (let lineComponent of lineComponents) { 174 | const trimmedComponent = lineComponent.trim(); 175 | if (trimmedComponent.length > 0) { 176 | validComponents++; 177 | if (validComponents === 2) { 178 | sectionNames.push(trimmedComponent); 179 | } 180 | } 181 | } 182 | } 183 | } 184 | return sectionNames; 185 | } 186 | 187 | static checkTextForEndHeader(endHeaderTestText) { 188 | if (endHeaderTestText.includes(PlyParserUtils.HeaderEndToken)) { 189 | return true; 190 | } 191 | return false; 192 | } 193 | 194 | static checkBufferForEndHeader(buffer, searchOfset, chunkSize, decoder) { 195 | const endHeaderTestChunk = new Uint8Array(buffer, Math.max(0, searchOfset - chunkSize), chunkSize); 196 | const endHeaderTestText = decoder.decode(endHeaderTestChunk); 197 | return PlyParserUtils.checkTextForEndHeader(endHeaderTestText); 198 | } 199 | 200 | static extractHeaderFromBufferToText(plyBuffer) { 201 | const decoder = new TextDecoder(); 202 | let headerOffset = 0; 203 | let headerText = ''; 204 | const readChunkSize = 100; 205 | 206 | while (true) { 207 | if (headerOffset + readChunkSize >= plyBuffer.byteLength) { 208 | throw new Error('End of file reached while searching for end of header'); 209 | } 210 | const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize); 211 | headerText += decoder.decode(headerChunk); 212 | headerOffset += readChunkSize; 213 | 214 | if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) { 215 | break; 216 | } 217 | } 218 | 219 | return headerText; 220 | } 221 | 222 | static readHeaderFromBuffer(plyBuffer) { 223 | const decoder = new TextDecoder(); 224 | let headerOffset = 0; 225 | let headerText = ''; 226 | const readChunkSize = 100; 227 | 228 | while (true) { 229 | if (headerOffset + readChunkSize >= plyBuffer.byteLength) { 230 | throw new Error('End of file reached while searching for end of header'); 231 | } 232 | const headerChunk = new Uint8Array(plyBuffer, headerOffset, readChunkSize); 233 | headerText += decoder.decode(headerChunk); 234 | headerOffset += readChunkSize; 235 | 236 | if (PlyParserUtils.checkBufferForEndHeader(plyBuffer, headerOffset, readChunkSize * 2, decoder)) { 237 | break; 238 | } 239 | } 240 | 241 | return headerText; 242 | } 243 | 244 | static convertHeaderTextToLines(headerText) { 245 | const headerLines = headerText.split('\n'); 246 | const prunedLines = []; 247 | for (let i = 0; i < headerLines.length; i++) { 248 | const line = headerLines[i].trim(); 249 | prunedLines.push(line); 250 | if (line === PlyParserUtils.HeaderEndToken) { 251 | break; 252 | } 253 | } 254 | return prunedLines; 255 | } 256 | 257 | static determineHeaderFormatFromHeaderText(headertText) { 258 | const headerLines = PlyParserUtils.convertHeaderTextToLines(headertText); 259 | let format = PlyFormat.INRIAV1; 260 | for (let i = 0; i < headerLines.length; i++) { 261 | const line = headerLines[i].trim(); 262 | if (line.startsWith('element chunk') || line.match(/[A-Za-z]*packed_[A-Za-z]*/)) { 263 | format = PlyFormat.PlayCanvasCompressed; 264 | } else if (line.startsWith('element codebook_centers')) { 265 | format = PlyFormat.INRIAV2; 266 | } else if (line === PlyParserUtils.HeaderEndToken) { 267 | break; 268 | } 269 | } 270 | return format; 271 | } 272 | 273 | static determineHeaderFormatFromPlyBuffer(plyBuffer) { 274 | const headertText = PlyParserUtils.extractHeaderFromBufferToText(plyBuffer); 275 | return PlyParserUtils.determineHeaderFormatFromHeaderText(headertText); 276 | } 277 | 278 | static readVertex(vertexData, header, row, dataOffset, fieldsToRead, rawVertex, normalize = true) { 279 | const offset = row * header.bytesPerVertex + dataOffset; 280 | const fieldOffsets = header.fieldOffsets; 281 | const fieldTypes = header.fieldTypes; 282 | for (let fieldId of fieldsToRead) { 283 | const fieldType = fieldTypes[fieldId]; 284 | if (fieldType === FieldSizeIdFloat) { 285 | rawVertex[fieldId] = vertexData.getFloat32(offset + fieldOffsets[fieldId], true); 286 | } else if (fieldType === FieldSizeIdShort) { 287 | rawVertex[fieldId] = vertexData.getInt16(offset + fieldOffsets[fieldId], true); 288 | } else if (fieldType === FieldSizeIdUShort) { 289 | rawVertex[fieldId] = vertexData.getUint16(offset + fieldOffsets[fieldId], true); 290 | } else if (fieldType === FieldSizeIdInt) { 291 | rawVertex[fieldId] = vertexData.getInt32(offset + fieldOffsets[fieldId], true); 292 | } else if (fieldType === FieldSizeIdUInt) { 293 | rawVertex[fieldId] = vertexData.getUint32(offset + fieldOffsets[fieldId], true); 294 | } else if (fieldType === FieldSizeIdUChar) { 295 | if (normalize) { 296 | rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]) / 255.0; 297 | } else { 298 | rawVertex[fieldId] = vertexData.getUint8(offset + fieldOffsets[fieldId]); 299 | } 300 | } 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/loaders/splat/SplatLoader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { SplatBuffer } from '../SplatBuffer.js'; 3 | import { SplatBufferGenerator } from '../SplatBufferGenerator.js'; 4 | import { SplatParser } from './SplatParser.js'; 5 | import { fetchWithProgress, delayedExecute, nativePromiseWithExtractedComponents } from '../../Util.js'; 6 | import { UncompressedSplatArray } from '../UncompressedSplatArray.js'; 7 | import { LoaderStatus } from '../LoaderStatus.js'; 8 | import { DirectLoadError } from '../DirectLoadError.js'; 9 | import { Constants } from '../../Constants.js'; 10 | import { InternalLoadType } from '../InternalLoadType.js'; 11 | 12 | function finalize(splatData, optimizeSplatData, minimumAlpha, compressionLevel, sectionSize, sceneCenter, blockSize, bucketSize) { 13 | if (optimizeSplatData) { 14 | const splatBufferGenerator = SplatBufferGenerator.getStandardGenerator(minimumAlpha, compressionLevel, 15 | sectionSize, sceneCenter, 16 | blockSize, bucketSize); 17 | return splatBufferGenerator.generateFromUncompressedSplatArray(splatData); 18 | } else { 19 | // TODO: Implement direct-to-SplatBuffer when not optimizing splat data 20 | return SplatBuffer.generateFromUncompressedSplatArrays([splatData], minimumAlpha, 0, new THREE.Vector3()); 21 | } 22 | } 23 | 24 | export class SplatLoader { 25 | 26 | static loadFromURL(fileName, onProgress, progressiveLoadToSplatBuffer, onProgressiveLoadSectionProgress, minimumAlpha, compressionLevel, 27 | optimizeSplatData = true, headers, sectionSize, sceneCenter, blockSize, bucketSize) { 28 | 29 | let internalLoadType = progressiveLoadToSplatBuffer ? InternalLoadType.ProgressiveToSplatBuffer : 30 | InternalLoadType.ProgressiveToSplatArray; 31 | if (optimizeSplatData) internalLoadType = InternalLoadType.ProgressiveToSplatArray; 32 | 33 | const splatDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes; 34 | const directLoadSectionSizeBytes = Constants.ProgressiveLoadSectionSize; 35 | const sectionCount = 1; 36 | 37 | let directLoadBufferIn; 38 | let directLoadBufferOut; 39 | let directLoadSplatBuffer; 40 | let maxSplatCount = 0; 41 | let splatCount = 0; 42 | 43 | let standardLoadUncompressedSplatArray; 44 | 45 | const loadPromise = nativePromiseWithExtractedComponents(); 46 | 47 | let numBytesStreamed = 0; 48 | let numBytesLoaded = 0; 49 | let chunks = []; 50 | 51 | const localOnProgress = (percent, percentStr, chunk, fileSize) => { 52 | const loadComplete = percent >= 100; 53 | 54 | if (chunk) { 55 | chunks.push(chunk); 56 | } 57 | 58 | if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { 59 | if (loadComplete) { 60 | loadPromise.resolve(chunks); 61 | } 62 | return; 63 | } 64 | 65 | if (!fileSize) { 66 | if (progressiveLoadToSplatBuffer) { 67 | throw new DirectLoadError('Cannon directly load .splat because no file size info is available.'); 68 | } else { 69 | internalLoadType = InternalLoadType.DownloadBeforeProcessing; 70 | return; 71 | } 72 | } 73 | 74 | if (!directLoadBufferIn) { 75 | maxSplatCount = fileSize / SplatParser.RowSizeBytes; 76 | directLoadBufferIn = new ArrayBuffer(fileSize); 77 | const bytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[0].BytesPerSplat; 78 | const splatBufferSizeBytes = splatDataOffsetBytes + bytesPerSplat * maxSplatCount; 79 | 80 | if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { 81 | directLoadBufferOut = new ArrayBuffer(splatBufferSizeBytes); 82 | SplatBuffer.writeHeaderToBuffer({ 83 | versionMajor: SplatBuffer.CurrentMajorVersion, 84 | versionMinor: SplatBuffer.CurrentMinorVersion, 85 | maxSectionCount: sectionCount, 86 | sectionCount: sectionCount, 87 | maxSplatCount: maxSplatCount, 88 | splatCount: splatCount, 89 | compressionLevel: 0, 90 | sceneCenter: new THREE.Vector3() 91 | }, directLoadBufferOut); 92 | } else { 93 | standardLoadUncompressedSplatArray = new UncompressedSplatArray(0); 94 | } 95 | } 96 | 97 | if (chunk) { 98 | new Uint8Array(directLoadBufferIn, numBytesLoaded, chunk.byteLength).set(new Uint8Array(chunk)); 99 | numBytesLoaded += chunk.byteLength; 100 | 101 | const bytesLoadedSinceLastSection = numBytesLoaded - numBytesStreamed; 102 | if (bytesLoadedSinceLastSection > directLoadSectionSizeBytes || loadComplete) { 103 | const bytesToUpdate = loadComplete ? bytesLoadedSinceLastSection : directLoadSectionSizeBytes; 104 | const addedSplatCount = bytesToUpdate / SplatParser.RowSizeBytes; 105 | const newSplatCount = splatCount + addedSplatCount; 106 | 107 | if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { 108 | SplatParser.parseToUncompressedSplatBufferSection(splatCount, newSplatCount - 1, directLoadBufferIn, 0, 109 | directLoadBufferOut, splatDataOffsetBytes); 110 | } else { 111 | SplatParser.parseToUncompressedSplatArraySection(splatCount, newSplatCount - 1, directLoadBufferIn, 0, 112 | standardLoadUncompressedSplatArray); 113 | } 114 | 115 | splatCount = newSplatCount; 116 | 117 | if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { 118 | if (!directLoadSplatBuffer) { 119 | SplatBuffer.writeSectionHeaderToBuffer({ 120 | maxSplatCount: maxSplatCount, 121 | splatCount: splatCount, 122 | bucketSize: 0, 123 | bucketCount: 0, 124 | bucketBlockSize: 0, 125 | compressionScaleRange: 0, 126 | storageSizeBytes: 0, 127 | fullBucketCount: 0, 128 | partiallyFilledBucketCount: 0 129 | }, 0, directLoadBufferOut, SplatBuffer.HeaderSizeBytes); 130 | directLoadSplatBuffer = new SplatBuffer(directLoadBufferOut, false); 131 | } 132 | directLoadSplatBuffer.updateLoadedCounts(1, splatCount); 133 | if (onProgressiveLoadSectionProgress) { 134 | onProgressiveLoadSectionProgress(directLoadSplatBuffer, loadComplete); 135 | } 136 | } 137 | 138 | numBytesStreamed += directLoadSectionSizeBytes; 139 | } 140 | } 141 | 142 | if (loadComplete) { 143 | if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { 144 | loadPromise.resolve(directLoadSplatBuffer); 145 | } else { 146 | loadPromise.resolve(standardLoadUncompressedSplatArray); 147 | } 148 | } 149 | 150 | if (onProgress) onProgress(percent, percentStr, LoaderStatus.Downloading); 151 | }; 152 | 153 | if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading); 154 | return fetchWithProgress(fileName, localOnProgress, false, headers).then(() => { 155 | if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); 156 | return loadPromise.promise.then((splatData) => { 157 | if (onProgress) onProgress(100, '100%', LoaderStatus.Done); 158 | if (internalLoadType === InternalLoadType.DownloadBeforeProcessing) { 159 | return new Blob(chunks).arrayBuffer().then((splatData) => { 160 | return SplatLoader.loadFromFileData(splatData, minimumAlpha, compressionLevel, optimizeSplatData, 161 | sectionSize, sceneCenter, blockSize, bucketSize); 162 | }); 163 | } else if (internalLoadType === InternalLoadType.ProgressiveToSplatBuffer) { 164 | return splatData; 165 | } else { 166 | return delayedExecute(() => { 167 | return finalize(splatData, optimizeSplatData, minimumAlpha, compressionLevel, 168 | sectionSize, sceneCenter, blockSize, bucketSize); 169 | }); 170 | } 171 | }); 172 | }); 173 | } 174 | 175 | static loadFromFileData(splatFileData, minimumAlpha, compressionLevel, optimizeSplatData, 176 | sectionSize, sceneCenter, blockSize, bucketSize) { 177 | return delayedExecute(() => { 178 | const splatArray = SplatParser.parseStandardSplatToUncompressedSplatArray(splatFileData); 179 | return finalize(splatArray, optimizeSplatData, minimumAlpha, compressionLevel, 180 | sectionSize, sceneCenter, blockSize, bucketSize); 181 | }); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/loaders/splat/SplatParser.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { SplatBuffer } from '../SplatBuffer.js'; 3 | import { UncompressedSplatArray } from '../UncompressedSplatArray.js'; 4 | 5 | export class SplatParser { 6 | 7 | static RowSizeBytes = 32; 8 | static CenterSizeBytes = 12; 9 | static ScaleSizeBytes = 12; 10 | static RotationSizeBytes = 4; 11 | static ColorSizeBytes = 4; 12 | 13 | static parseToUncompressedSplatBufferSection(fromSplat, toSplat, fromBuffer, fromOffset, toBuffer, toOffset) { 14 | 15 | const outBytesPerCenter = SplatBuffer.CompressionLevels[0].BytesPerCenter; 16 | const outBytesPerScale = SplatBuffer.CompressionLevels[0].BytesPerScale; 17 | const outBytesPerRotation = SplatBuffer.CompressionLevels[0].BytesPerRotation; 18 | const outBytesPerSplat = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[0].BytesPerSplat; 19 | 20 | for (let i = fromSplat; i <= toSplat; i++) { 21 | const inBase = i * SplatParser.RowSizeBytes + fromOffset; 22 | const inCenter = new Float32Array(fromBuffer, inBase, 3); 23 | const inScale = new Float32Array(fromBuffer, inBase + SplatParser.CenterSizeBytes, 3); 24 | const inColor = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); 25 | const inRotation = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes + 26 | SplatParser.RotationSizeBytes, 4); 27 | 28 | const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, 29 | (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); 30 | quat.normalize(); 31 | 32 | const outBase = i * outBytesPerSplat + toOffset; 33 | const outCenter = new Float32Array(toBuffer, outBase, 3); 34 | const outScale = new Float32Array(toBuffer, outBase + outBytesPerCenter, 3); 35 | const outRotation = new Float32Array(toBuffer, outBase + outBytesPerCenter + outBytesPerScale, 4); 36 | const outColor = new Uint8Array(toBuffer, outBase + outBytesPerCenter + outBytesPerScale + outBytesPerRotation, 4); 37 | 38 | outCenter[0] = inCenter[0]; 39 | outCenter[1] = inCenter[1]; 40 | outCenter[2] = inCenter[2]; 41 | 42 | outScale[0] = inScale[0]; 43 | outScale[1] = inScale[1]; 44 | outScale[2] = inScale[2]; 45 | 46 | outRotation[0] = quat.w; 47 | outRotation[1] = quat.x; 48 | outRotation[2] = quat.y; 49 | outRotation[3] = quat.z; 50 | 51 | outColor[0] = inColor[0]; 52 | outColor[1] = inColor[1]; 53 | outColor[2] = inColor[2]; 54 | outColor[3] = inColor[3]; 55 | } 56 | } 57 | 58 | static parseToUncompressedSplatArraySection(fromSplat, toSplat, fromBuffer, fromOffset, splatArray) { 59 | 60 | for (let i = fromSplat; i <= toSplat; i++) { 61 | const inBase = i * SplatParser.RowSizeBytes + fromOffset; 62 | const inCenter = new Float32Array(fromBuffer, inBase, 3); 63 | const inScale = new Float32Array(fromBuffer, inBase + SplatParser.CenterSizeBytes, 3); 64 | const inColor = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); 65 | const inRotation = new Uint8Array(fromBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes + 66 | SplatParser.RotationSizeBytes, 4); 67 | 68 | const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, 69 | (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); 70 | quat.normalize(); 71 | 72 | splatArray.addSplatFromComonents(inCenter[0], inCenter[1], inCenter[2], inScale[0], inScale[1], inScale[2], 73 | quat.w, quat.x, quat.y, quat.z, inColor[0], inColor[1], inColor[2], inColor[3]); 74 | } 75 | } 76 | 77 | static parseStandardSplatToUncompressedSplatArray(inBuffer) { 78 | // Standard .splat row layout: 79 | // XYZ - Position (Float32) 80 | // XYZ - Scale (Float32) 81 | // RGBA - colors (uint8) 82 | // IJKL - quaternion/rot (uint8) 83 | 84 | const splatCount = inBuffer.byteLength / SplatParser.RowSizeBytes; 85 | 86 | const splatArray = new UncompressedSplatArray(); 87 | 88 | for (let i = 0; i < splatCount; i++) { 89 | const inBase = i * SplatParser.RowSizeBytes; 90 | const inCenter = new Float32Array(inBuffer, inBase, 3); 91 | const inScale = new Float32Array(inBuffer, inBase + SplatParser.CenterSizeBytes, 3); 92 | const inColor = new Uint8Array(inBuffer, inBase + SplatParser.CenterSizeBytes + SplatParser.ScaleSizeBytes, 4); 93 | const inRotation = new Uint8Array(inBuffer, inBase + SplatParser.CenterSizeBytes + 94 | SplatParser.ScaleSizeBytes + SplatParser.ColorSizeBytes, 4); 95 | 96 | const quat = new THREE.Quaternion((inRotation[1] - 128) / 128, (inRotation[2] - 128) / 128, 97 | (inRotation[3] - 128) / 128, (inRotation[0] - 128) / 128); 98 | quat.normalize(); 99 | 100 | splatArray.addSplatFromComonents(inCenter[0], inCenter[1], inCenter[2], inScale[0], inScale[1], inScale[2], 101 | quat.w, quat.x, quat.y, quat.z, inColor[0], inColor[1], inColor[2], inColor[3]); 102 | } 103 | 104 | return splatArray; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/raycaster/Hit.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export class Hit { 4 | 5 | constructor() { 6 | this.origin = new THREE.Vector3(); 7 | this.normal = new THREE.Vector3(); 8 | this.distance = 0; 9 | this.splatIndex = 0; 10 | } 11 | 12 | set(origin, normal, distance, splatIndex) { 13 | this.origin.copy(origin); 14 | this.normal.copy(normal); 15 | this.distance = distance; 16 | this.splatIndex = splatIndex; 17 | } 18 | 19 | clone() { 20 | const hitClone = new Hit(); 21 | hitClone.origin.copy(this.origin); 22 | hitClone.normal.copy(this.normal); 23 | hitClone.distance = this.distance; 24 | hitClone.splatIndex = this.splatIndex; 25 | return hitClone; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/raycaster/Ray.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | const VectorRight = new THREE.Vector3(1, 0, 0); 4 | const VectorUp = new THREE.Vector3(0, 1, 0); 5 | const VectorBackward = new THREE.Vector3(0, 0, 1); 6 | 7 | export class Ray { 8 | 9 | constructor(origin = new THREE.Vector3(), direction = new THREE.Vector3()) { 10 | this.origin = new THREE.Vector3(); 11 | this.direction = new THREE.Vector3(); 12 | this.setParameters(origin, direction); 13 | } 14 | 15 | setParameters(origin, direction) { 16 | this.origin.copy(origin); 17 | this.direction.copy(direction).normalize(); 18 | } 19 | 20 | boxContainsPoint(box, point, epsilon) { 21 | return point.x < box.min.x - epsilon || point.x > box.max.x + epsilon || 22 | point.y < box.min.y - epsilon || point.y > box.max.y + epsilon || 23 | point.z < box.min.z - epsilon || point.z > box.max.z + epsilon ? false : true; 24 | } 25 | 26 | intersectBox = function() { 27 | 28 | const planeIntersectionPoint = new THREE.Vector3(); 29 | const planeIntersectionPointArray = []; 30 | const originArray = []; 31 | const directionArray = []; 32 | 33 | return function(box, outHit) { 34 | 35 | originArray[0] = this.origin.x; 36 | originArray[1] = this.origin.y; 37 | originArray[2] = this.origin.z; 38 | directionArray[0] = this.direction.x; 39 | directionArray[1] = this.direction.y; 40 | directionArray[2] = this.direction.z; 41 | 42 | if (this.boxContainsPoint(box, this.origin, 0.0001)) { 43 | if (outHit) { 44 | outHit.origin.copy(this.origin); 45 | outHit.normal.set(0, 0, 0); 46 | outHit.distance = -1; 47 | } 48 | return true; 49 | } 50 | 51 | for (let i = 0; i < 3; i++) { 52 | if (directionArray[i] == 0.0) continue; 53 | 54 | const hitNormal = i == 0 ? VectorRight : i == 1 ? VectorUp : VectorBackward; 55 | const extremeVec = directionArray[i] < 0 ? box.max : box.min; 56 | let multiplier = -Math.sign(directionArray[i]); 57 | planeIntersectionPointArray[0] = i == 0 ? extremeVec.x : i == 1 ? extremeVec.y : extremeVec.z; 58 | let toSide = planeIntersectionPointArray[0] - originArray[i]; 59 | 60 | if (toSide * multiplier < 0) { 61 | const idx1 = (i + 1) % 3; 62 | const idx2 = (i + 2) % 3; 63 | planeIntersectionPointArray[2] = directionArray[idx1] / directionArray[i] * toSide + originArray[idx1]; 64 | planeIntersectionPointArray[1] = directionArray[idx2] / directionArray[i] * toSide + originArray[idx2]; 65 | planeIntersectionPoint.set(planeIntersectionPointArray[i], 66 | planeIntersectionPointArray[idx2], 67 | planeIntersectionPointArray[idx1]); 68 | if (this.boxContainsPoint(box, planeIntersectionPoint, 0.0001)) { 69 | if (outHit) { 70 | outHit.origin.copy(planeIntersectionPoint); 71 | outHit.normal.copy(hitNormal).multiplyScalar(multiplier); 72 | outHit.distance = planeIntersectionPoint.sub(this.origin).length(); 73 | } 74 | return true; 75 | } 76 | } 77 | } 78 | 79 | return false; 80 | }; 81 | 82 | }(); 83 | 84 | intersectSphere = function() { 85 | 86 | const toSphereCenterVec = new THREE.Vector3(); 87 | 88 | return function(center, radius, outHit) { 89 | toSphereCenterVec.copy(center).sub(this.origin); 90 | const toClosestApproach = toSphereCenterVec.dot(this.direction); 91 | const toClosestApproachSq = toClosestApproach * toClosestApproach; 92 | const toSphereCenterSq = toSphereCenterVec.dot(toSphereCenterVec); 93 | const diffSq = toSphereCenterSq - toClosestApproachSq; 94 | const radiusSq = radius * radius; 95 | 96 | if (diffSq > radiusSq) return false; 97 | 98 | const thc = Math.sqrt(radiusSq - diffSq); 99 | const t0 = toClosestApproach - thc; 100 | const t1 = toClosestApproach + thc; 101 | 102 | if (t1 < 0) return false; 103 | let t = t0 < 0 ? t1 : t0; 104 | 105 | if (outHit) { 106 | outHit.origin.copy(this.origin).addScaledVector(this.direction, t); 107 | outHit.normal.copy(outHit.origin).sub(center).normalize(); 108 | outHit.distance = t; 109 | } 110 | return true; 111 | }; 112 | 113 | }(); 114 | } 115 | -------------------------------------------------------------------------------- /src/raycaster/Raycaster.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Ray } from './Ray.js'; 3 | import { Hit } from './Hit.js'; 4 | import { SplatRenderMode } from '../SplatRenderMode.js'; 5 | 6 | export class Raycaster { 7 | 8 | constructor(origin, direction, raycastAgainstTrueSplatEllipsoid = false) { 9 | this.ray = new Ray(origin, direction); 10 | this.raycastAgainstTrueSplatEllipsoid = raycastAgainstTrueSplatEllipsoid; 11 | } 12 | 13 | setFromCameraAndScreenPosition = function() { 14 | 15 | const ndcCoords = new THREE.Vector2(); 16 | 17 | return function(camera, screenPosition, screenDimensions) { 18 | ndcCoords.x = screenPosition.x / screenDimensions.x * 2.0 - 1.0; 19 | ndcCoords.y = (screenDimensions.y - screenPosition.y) / screenDimensions.y * 2.0 - 1.0; 20 | if (camera.isPerspectiveCamera) { 21 | this.ray.origin.setFromMatrixPosition(camera.matrixWorld); 22 | this.ray.direction.set(ndcCoords.x, ndcCoords.y, 0.5 ).unproject(camera).sub(this.ray.origin).normalize(); 23 | this.camera = camera; 24 | } else if (camera.isOrthographicCamera) { 25 | this.ray.origin.set(ndcCoords.x, ndcCoords.y, 26 | (camera.near + camera.far) / (camera.near - camera.far)).unproject(camera); 27 | this.ray.direction.set(0, 0, -1).transformDirection(camera.matrixWorld); 28 | this.camera = camera; 29 | } else { 30 | throw new Error('Raycaster::setFromCameraAndScreenPosition() -> Unsupported camera type'); 31 | } 32 | }; 33 | 34 | }(); 35 | 36 | intersectSplatMesh = function() { 37 | 38 | const toLocal = new THREE.Matrix4(); 39 | const fromLocal = new THREE.Matrix4(); 40 | const sceneTransform = new THREE.Matrix4(); 41 | const localRay = new Ray(); 42 | const tempPoint = new THREE.Vector3(); 43 | 44 | return function(splatMesh, outHits = []) { 45 | const splatTree = splatMesh.getSplatTree(); 46 | 47 | if (!splatTree) return; 48 | 49 | for (let s = 0; s < splatTree.subTrees.length; s++) { 50 | const subTree = splatTree.subTrees[s]; 51 | 52 | fromLocal.copy(splatMesh.matrixWorld); 53 | if (splatMesh.dynamicMode) { 54 | splatMesh.getSceneTransform(s, sceneTransform); 55 | fromLocal.multiply(sceneTransform); 56 | } 57 | toLocal.copy(fromLocal).invert(); 58 | 59 | localRay.origin.copy(this.ray.origin).applyMatrix4(toLocal); 60 | localRay.direction.copy(this.ray.origin).add(this.ray.direction); 61 | localRay.direction.applyMatrix4(toLocal).sub(localRay.origin).normalize(); 62 | 63 | const outHitsForSubTree = []; 64 | if (subTree.rootNode) { 65 | this.castRayAtSplatTreeNode(localRay, splatTree, subTree.rootNode, outHitsForSubTree); 66 | } 67 | 68 | outHitsForSubTree.forEach((hit) => { 69 | hit.origin.applyMatrix4(fromLocal); 70 | hit.normal.applyMatrix4(fromLocal).normalize(); 71 | hit.distance = tempPoint.copy(hit.origin).sub(this.ray.origin).length(); 72 | }); 73 | 74 | outHits.push(...outHitsForSubTree); 75 | } 76 | 77 | outHits.sort((a, b) => { 78 | if (a.distance > b.distance) return 1; 79 | else return -1; 80 | }); 81 | 82 | return outHits; 83 | }; 84 | 85 | }(); 86 | 87 | castRayAtSplatTreeNode = function() { 88 | 89 | const tempColor = new THREE.Vector4(); 90 | const tempCenter = new THREE.Vector3(); 91 | const tempScale = new THREE.Vector3(); 92 | const tempRotation = new THREE.Quaternion(); 93 | const tempHit = new Hit(); 94 | const scaleEpsilon = 0.0000001; 95 | 96 | const origin = new THREE.Vector3(0, 0, 0); 97 | const uniformScaleMatrix = new THREE.Matrix4(); 98 | const scaleMatrix = new THREE.Matrix4(); 99 | const rotationMatrix = new THREE.Matrix4(); 100 | const toSphereSpace = new THREE.Matrix4(); 101 | const fromSphereSpace = new THREE.Matrix4(); 102 | const tempRay = new Ray(); 103 | 104 | return function(ray, splatTree, node, outHits = []) { 105 | if (!ray.intersectBox(node.boundingBox)) { 106 | return; 107 | } 108 | if (node.data && node.data.indexes && node.data.indexes.length > 0) { 109 | for (let i = 0; i < node.data.indexes.length; i++) { 110 | 111 | const splatGlobalIndex = node.data.indexes[i]; 112 | const splatSceneIndex = splatTree.splatMesh.getSceneIndexForSplat(splatGlobalIndex); 113 | const splatScene = splatTree.splatMesh.getScene(splatSceneIndex); 114 | if (!splatScene.visible) continue; 115 | 116 | splatTree.splatMesh.getSplatColor(splatGlobalIndex, tempColor); 117 | splatTree.splatMesh.getSplatCenter(splatGlobalIndex, tempCenter); 118 | splatTree.splatMesh.getSplatScaleAndRotation(splatGlobalIndex, tempScale, tempRotation); 119 | 120 | if (tempScale.x <= scaleEpsilon || tempScale.y <= scaleEpsilon || 121 | splatTree.splatMesh.splatRenderMode === SplatRenderMode.ThreeD && tempScale.z <= scaleEpsilon) { 122 | continue; 123 | } 124 | 125 | if (!this.raycastAgainstTrueSplatEllipsoid) { 126 | let radius = (tempScale.x + tempScale.y); 127 | let componentCount = 2; 128 | if (splatTree.splatMesh.splatRenderMode === SplatRenderMode.ThreeD) { 129 | radius += tempScale.z; 130 | componentCount = 3; 131 | } 132 | radius = radius / componentCount; 133 | if (ray.intersectSphere(tempCenter, radius, tempHit)) { 134 | const hitClone = tempHit.clone(); 135 | hitClone.splatIndex = splatGlobalIndex; 136 | outHits.push(hitClone); 137 | } 138 | } else { 139 | scaleMatrix.makeScale(tempScale.x, tempScale.y, tempScale.z); 140 | rotationMatrix.makeRotationFromQuaternion(tempRotation); 141 | const uniformScale = Math.log10(tempColor.w) * 2.0; 142 | uniformScaleMatrix.makeScale(uniformScale, uniformScale, uniformScale); 143 | fromSphereSpace.copy(uniformScaleMatrix).multiply(rotationMatrix).multiply(scaleMatrix); 144 | toSphereSpace.copy(fromSphereSpace).invert(); 145 | tempRay.origin.copy(ray.origin).sub(tempCenter).applyMatrix4(toSphereSpace); 146 | tempRay.direction.copy(ray.origin).add(ray.direction).sub(tempCenter); 147 | tempRay.direction.applyMatrix4(toSphereSpace).sub(tempRay.origin).normalize(); 148 | if (tempRay.intersectSphere(origin, 1.0, tempHit)) { 149 | const hitClone = tempHit.clone(); 150 | hitClone.splatIndex = splatGlobalIndex; 151 | hitClone.origin.applyMatrix4(fromSphereSpace).add(tempCenter); 152 | outHits.push(hitClone); 153 | } 154 | } 155 | } 156 | } 157 | if (node.children && node.children.length > 0) { 158 | for (let child of node.children) { 159 | this.castRayAtSplatTreeNode(ray, splatTree, child, outHits); 160 | } 161 | } 162 | return outHits; 163 | }; 164 | 165 | }(); 166 | } 167 | -------------------------------------------------------------------------------- /src/splatmesh/SplatGeometry.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export class SplatGeometry { 4 | 5 | /** 6 | * Build the Three.js geometry that will be used to render the splats. The geometry is instanced and is made up of 7 | * vertices for a single quad as well as an attribute buffer for the splat indexes. 8 | * @param {number} maxSplatCount The maximum number of splats that the geometry will need to accomodate 9 | * @return {THREE.InstancedBufferGeometry} 10 | */ 11 | static build(maxSplatCount) { 12 | 13 | const baseGeometry = new THREE.BufferGeometry(); 14 | baseGeometry.setIndex([0, 1, 2, 0, 2, 3]); 15 | 16 | // Vertices for the instanced quad 17 | const positionsArray = new Float32Array(4 * 3); 18 | const positions = new THREE.BufferAttribute(positionsArray, 3); 19 | baseGeometry.setAttribute('position', positions); 20 | positions.setXYZ(0, -1.0, -1.0, 0.0); 21 | positions.setXYZ(1, -1.0, 1.0, 0.0); 22 | positions.setXYZ(2, 1.0, 1.0, 0.0); 23 | positions.setXYZ(3, 1.0, -1.0, 0.0); 24 | positions.needsUpdate = true; 25 | 26 | const geometry = new THREE.InstancedBufferGeometry().copy(baseGeometry); 27 | 28 | // Splat index buffer 29 | const splatIndexArray = new Uint32Array(maxSplatCount); 30 | const splatIndexes = new THREE.InstancedBufferAttribute(splatIndexArray, 1, false); 31 | splatIndexes.setUsage(THREE.DynamicDrawUsage); 32 | geometry.setAttribute('splatIndex', splatIndexes); 33 | 34 | geometry.instanceCount = 0; 35 | 36 | return geometry; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/splatmesh/SplatScene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | /** 4 | * SplatScene: Descriptor for a single splat scene managed by an instance of SplatMesh. 5 | */ 6 | export class SplatScene extends THREE.Object3D { 7 | 8 | constructor(splatBuffer, position = new THREE.Vector3(), quaternion = new THREE.Quaternion(), 9 | scale = new THREE.Vector3(1, 1, 1), minimumAlpha = 1, opacity = 1.0, visible = true) { 10 | super(); 11 | this.splatBuffer = splatBuffer; 12 | this.position.copy(position); 13 | this.quaternion.copy(quaternion); 14 | this.scale.copy(scale); 15 | this.transform = new THREE.Matrix4(); 16 | this.minimumAlpha = minimumAlpha; 17 | this.opacity = opacity; 18 | this.visible = visible; 19 | } 20 | 21 | copyTransformData(otherScene) { 22 | this.position.copy(otherScene.position); 23 | this.quaternion.copy(otherScene.quaternion); 24 | this.scale.copy(otherScene.scale); 25 | this.transform.copy(otherScene.transform); 26 | } 27 | 28 | updateTransform(dynamicMode) { 29 | if (dynamicMode) { 30 | if (this.matrixWorldAutoUpdate) this.updateWorldMatrix(true, false); 31 | this.transform.copy(this.matrixWorld); 32 | } else { 33 | if (this.matrixAutoUpdate) this.updateMatrix(); 34 | this.transform.copy(this.matrix); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/three-shim/WebGLCapabilities.js: -------------------------------------------------------------------------------- 1 | function WebGLCapabilities( gl, extensions, parameters ) { 2 | 3 | let maxAnisotropy; 4 | 5 | function getMaxAnisotropy() { 6 | 7 | if ( maxAnisotropy !== undefined ) return maxAnisotropy; 8 | 9 | if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) { 10 | 11 | const extension = extensions.get( 'EXT_texture_filter_anisotropic' ); 12 | 13 | maxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT ); 14 | 15 | } else { 16 | 17 | maxAnisotropy = 0; 18 | 19 | } 20 | 21 | return maxAnisotropy; 22 | 23 | } 24 | 25 | function getMaxPrecision( precision ) { 26 | 27 | if ( precision === 'highp' ) { 28 | 29 | if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.HIGH_FLOAT ).precision > 0 && 30 | gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.HIGH_FLOAT ).precision > 0 ) { 31 | 32 | return 'highp'; 33 | 34 | } 35 | 36 | precision = 'mediump'; 37 | 38 | } 39 | 40 | if ( precision === 'mediump' ) { 41 | 42 | if ( gl.getShaderPrecisionFormat( gl.VERTEX_SHADER, gl.MEDIUM_FLOAT ).precision > 0 && 43 | gl.getShaderPrecisionFormat( gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT ).precision > 0 ) { 44 | 45 | return 'mediump'; 46 | 47 | } 48 | 49 | } 50 | 51 | return 'lowp'; 52 | 53 | } 54 | 55 | const isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl.constructor.name === 'WebGL2RenderingContext'; 56 | 57 | let precision = parameters.precision !== undefined ? parameters.precision : 'highp'; 58 | const maxPrecision = getMaxPrecision( precision ); 59 | 60 | if ( maxPrecision !== precision ) { 61 | 62 | console.warn( 'THREE.WebGLRenderer:', precision, 'not supported, using', maxPrecision, 'instead.' ); 63 | precision = maxPrecision; 64 | 65 | } 66 | 67 | const drawBuffers = isWebGL2 || extensions.has( 'WEBGL_draw_buffers' ); 68 | 69 | const logarithmicDepthBuffer = parameters.logarithmicDepthBuffer === true; 70 | 71 | const maxTextures = gl.getParameter( gl.MAX_TEXTURE_IMAGE_UNITS ); 72 | const maxVertexTextures = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS ); 73 | const maxTextureSize = gl.getParameter( gl.MAX_TEXTURE_SIZE ); 74 | const maxCubemapSize = gl.getParameter( gl.MAX_CUBE_MAP_TEXTURE_SIZE ); 75 | 76 | const maxAttributes = gl.getParameter( gl.MAX_VERTEX_ATTRIBS ); 77 | const maxVertexUniforms = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS ); 78 | const maxVaryings = gl.getParameter( gl.MAX_VARYING_VECTORS ); 79 | const maxFragmentUniforms = gl.getParameter( gl.MAX_FRAGMENT_UNIFORM_VECTORS ); 80 | 81 | const vertexTextures = maxVertexTextures > 0; 82 | const floatFragmentTextures = isWebGL2 || extensions.has( 'OES_texture_float' ); 83 | const floatVertexTextures = vertexTextures && floatFragmentTextures; 84 | 85 | const maxSamples = isWebGL2 ? gl.getParameter( gl.MAX_SAMPLES ) : 0; 86 | 87 | return { 88 | 89 | isWebGL2: isWebGL2, 90 | 91 | drawBuffers: drawBuffers, 92 | 93 | getMaxAnisotropy: getMaxAnisotropy, 94 | getMaxPrecision: getMaxPrecision, 95 | 96 | precision: precision, 97 | logarithmicDepthBuffer: logarithmicDepthBuffer, 98 | 99 | maxTextures: maxTextures, 100 | maxVertexTextures: maxVertexTextures, 101 | maxTextureSize: maxTextureSize, 102 | maxCubemapSize: maxCubemapSize, 103 | 104 | maxAttributes: maxAttributes, 105 | maxVertexUniforms: maxVertexUniforms, 106 | maxVaryings: maxVaryings, 107 | maxFragmentUniforms: maxFragmentUniforms, 108 | 109 | vertexTextures: vertexTextures, 110 | floatFragmentTextures: floatFragmentTextures, 111 | floatVertexTextures: floatVertexTextures, 112 | 113 | maxSamples: maxSamples 114 | 115 | }; 116 | 117 | } 118 | 119 | 120 | export { WebGLCapabilities }; 121 | -------------------------------------------------------------------------------- /src/three-shim/WebGLExtensions.js: -------------------------------------------------------------------------------- 1 | function WebGLExtensions( gl ) { 2 | 3 | const extensions = {}; 4 | 5 | function getExtension( name ) { 6 | 7 | if ( extensions[name] !== undefined ) { 8 | 9 | return extensions[name]; 10 | 11 | } 12 | 13 | let extension; 14 | 15 | switch ( name ) { 16 | 17 | case 'WEBGL_depth_texture': 18 | extension = gl.getExtension( 'WEBGL_depth_texture' ) || gl.getExtension( 'MOZ_WEBGL_depth_texture' ) || 19 | gl.getExtension( 'WEBKIT_WEBGL_depth_texture' ); 20 | break; 21 | 22 | case 'EXT_texture_filter_anisotropic': 23 | extension = gl.getExtension( 'EXT_texture_filter_anisotropic' ) || 24 | gl.getExtension( 'MOZ_EXT_texture_filter_anisotropic' ) || 25 | gl.getExtension( 'WEBKIT_EXT_texture_filter_anisotropic' ); 26 | break; 27 | 28 | case 'WEBGL_compressed_texture_s3tc': 29 | extension = gl.getExtension( 'WEBGL_compressed_texture_s3tc' ) || 30 | gl.getExtension( 'MOZ_WEBGL_compressed_texture_s3tc' ) || 31 | gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_s3tc' ); 32 | break; 33 | 34 | case 'WEBGL_compressed_texture_pvrtc': 35 | extension = gl.getExtension( 'WEBGL_compressed_texture_pvrtc' ) || 36 | gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_pvrtc' ); 37 | break; 38 | 39 | default: 40 | extension = gl.getExtension( name ); 41 | 42 | } 43 | 44 | extensions[name] = extension; 45 | 46 | return extension; 47 | 48 | } 49 | 50 | return { 51 | 52 | has: function( name ) { 53 | 54 | return getExtension( name ) !== null; 55 | 56 | }, 57 | 58 | init: function( capabilities ) { 59 | 60 | if ( capabilities.isWebGL2 ) { 61 | 62 | getExtension( 'EXT_color_buffer_float' ); 63 | getExtension( 'WEBGL_clip_cull_distance' ); 64 | 65 | } else { 66 | 67 | getExtension( 'WEBGL_depth_texture' ); 68 | getExtension( 'OES_texture_float' ); 69 | getExtension( 'OES_texture_half_float' ); 70 | getExtension( 'OES_texture_half_float_linear' ); 71 | getExtension( 'OES_standard_derivatives' ); 72 | getExtension( 'OES_element_index_uint' ); 73 | getExtension( 'OES_vertex_array_object' ); 74 | getExtension( 'ANGLE_instanced_arrays' ); 75 | 76 | } 77 | 78 | getExtension( 'OES_texture_float_linear' ); 79 | getExtension( 'EXT_color_buffer_half_float' ); 80 | getExtension( 'WEBGL_multisampled_render_to_texture' ); 81 | 82 | }, 83 | 84 | get: function( name ) { 85 | 86 | const extension = getExtension( name ); 87 | 88 | if ( extension === null ) { 89 | 90 | console.warn( 'THREE.WebGLRenderer: ' + name + ' extension not supported.' ); 91 | 92 | } 93 | 94 | return extension; 95 | 96 | } 97 | 98 | }; 99 | 100 | } 101 | 102 | export { WebGLExtensions }; 103 | -------------------------------------------------------------------------------- /src/ui/InfoPanel.js: -------------------------------------------------------------------------------- 1 | export class InfoPanel { 2 | 3 | constructor(container) { 4 | 5 | this.container = container || document.body; 6 | 7 | this.infoCells = {}; 8 | 9 | const layout = [ 10 | ['Camera position', 'cameraPosition'], 11 | ['Camera look-at', 'cameraLookAt'], 12 | ['Camera up', 'cameraUp'], 13 | ['Camera mode', 'orthographicCamera'], 14 | ['Cursor position', 'cursorPosition'], 15 | ['FPS', 'fps'], 16 | ['Rendering:', 'renderSplatCount'], 17 | ['Sort time', 'sortTime'], 18 | ['Render window', 'renderWindow'], 19 | ['Focal adjustment', 'focalAdjustment'], 20 | ['Splat scale', 'splatScale'], 21 | ['Point cloud mode', 'pointCloudMode'] 22 | ]; 23 | 24 | this.infoPanelContainer = document.createElement('div'); 25 | const style = document.createElement('style'); 26 | style.innerHTML = ` 27 | 28 | .infoPanel { 29 | width: 430px; 30 | padding: 10px; 31 | background-color: rgba(50, 50, 50, 0.85); 32 | border: #555555 2px solid; 33 | color: #dddddd; 34 | border-radius: 10px; 35 | z-index: 9999; 36 | font-family: arial; 37 | font-size: 11pt; 38 | text-align: left; 39 | margin: 0; 40 | top: 10px; 41 | left:10px; 42 | position: absolute; 43 | pointer-events: auto; 44 | } 45 | 46 | .info-panel-cell { 47 | margin-bottom: 5px; 48 | padding-bottom: 2px; 49 | } 50 | 51 | .label-cell { 52 | font-weight: bold; 53 | font-size: 12pt; 54 | width: 140px; 55 | } 56 | 57 | `; 58 | this.infoPanelContainer.append(style); 59 | 60 | this.infoPanel = document.createElement('div'); 61 | this.infoPanel.className = 'infoPanel'; 62 | 63 | const infoTable = document.createElement('div'); 64 | infoTable.style.display = 'table'; 65 | 66 | for (let layoutEntry of layout) { 67 | const row = document.createElement('div'); 68 | row.style.display = 'table-row'; 69 | row.className = 'info-panel-row'; 70 | 71 | const labelCell = document.createElement('div'); 72 | labelCell.style.display = 'table-cell'; 73 | labelCell.innerHTML = `${layoutEntry[0]}: `; 74 | labelCell.classList.add('info-panel-cell', 'label-cell'); 75 | 76 | const spacerCell = document.createElement('div'); 77 | spacerCell.style.display = 'table-cell'; 78 | spacerCell.style.width = '10px'; 79 | spacerCell.innerHTML = ' '; 80 | spacerCell.className = 'info-panel-cell'; 81 | 82 | const infoCell = document.createElement('div'); 83 | infoCell.style.display = 'table-cell'; 84 | infoCell.innerHTML = ''; 85 | infoCell.className = 'info-panel-cell'; 86 | 87 | this.infoCells[layoutEntry[1]] = infoCell; 88 | 89 | row.appendChild(labelCell); 90 | row.appendChild(spacerCell); 91 | row.appendChild(infoCell); 92 | 93 | infoTable.appendChild(row); 94 | } 95 | 96 | this.infoPanel.appendChild(infoTable); 97 | this.infoPanelContainer.append(this.infoPanel); 98 | this.infoPanelContainer.style.display = 'none'; 99 | this.container.appendChild(this.infoPanelContainer); 100 | 101 | this.visible = false; 102 | } 103 | 104 | update = function(renderDimensions, cameraPosition, cameraLookAtPosition, cameraUp, orthographicCamera, 105 | meshCursorPosition, currentFPS, splatCount, splatRenderCount, 106 | splatRenderCountPct, lastSortTime, focalAdjustment, splatScale, pointCloudMode) { 107 | 108 | const cameraPosString = `${cameraPosition.x.toFixed(5)}, ${cameraPosition.y.toFixed(5)}, ${cameraPosition.z.toFixed(5)}`; 109 | if (this.infoCells.cameraPosition.innerHTML !== cameraPosString) { 110 | this.infoCells.cameraPosition.innerHTML = cameraPosString; 111 | } 112 | 113 | if (cameraLookAtPosition) { 114 | const cla = cameraLookAtPosition; 115 | const cameraLookAtString = `${cla.x.toFixed(5)}, ${cla.y.toFixed(5)}, ${cla.z.toFixed(5)}`; 116 | if (this.infoCells.cameraLookAt.innerHTML !== cameraLookAtString) { 117 | this.infoCells.cameraLookAt.innerHTML = cameraLookAtString; 118 | } 119 | } 120 | 121 | const cameraUpString = `${cameraUp.x.toFixed(5)}, ${cameraUp.y.toFixed(5)}, ${cameraUp.z.toFixed(5)}`; 122 | if (this.infoCells.cameraUp.innerHTML !== cameraUpString) { 123 | this.infoCells.cameraUp.innerHTML = cameraUpString; 124 | } 125 | 126 | this.infoCells.orthographicCamera.innerHTML = orthographicCamera ? 'Orthographic' : 'Perspective'; 127 | 128 | if (meshCursorPosition) { 129 | const cursPos = meshCursorPosition; 130 | const cursorPosString = `${cursPos.x.toFixed(5)}, ${cursPos.y.toFixed(5)}, ${cursPos.z.toFixed(5)}`; 131 | this.infoCells.cursorPosition.innerHTML = cursorPosString; 132 | } else { 133 | this.infoCells.cursorPosition.innerHTML = 'N/A'; 134 | } 135 | 136 | this.infoCells.fps.innerHTML = currentFPS; 137 | this.infoCells.renderWindow.innerHTML = `${renderDimensions.x} x ${renderDimensions.y}`; 138 | 139 | this.infoCells.renderSplatCount.innerHTML = 140 | `${splatRenderCount} splats out of ${splatCount} (${splatRenderCountPct.toFixed(2)}%)`; 141 | 142 | this.infoCells.sortTime.innerHTML = `${lastSortTime.toFixed(3)} ms`; 143 | this.infoCells.focalAdjustment.innerHTML = `${focalAdjustment.toFixed(3)}`; 144 | this.infoCells.splatScale.innerHTML = `${splatScale.toFixed(3)}`; 145 | this.infoCells.pointCloudMode.innerHTML = `${pointCloudMode}`; 146 | }; 147 | 148 | setContainer(container) { 149 | if (this.container && this.infoPanelContainer.parentElement === this.container) { 150 | this.container.removeChild(this.infoPanelContainer); 151 | } 152 | if (container) { 153 | this.container = container; 154 | this.container.appendChild(this.infoPanelContainer); 155 | this.infoPanelContainer.style.zIndex = this.container.style.zIndex + 1; 156 | } 157 | } 158 | 159 | show() { 160 | this.infoPanelContainer.style.display = 'block'; 161 | this.visible = true; 162 | } 163 | 164 | hide() { 165 | this.infoPanelContainer.style.display = 'none'; 166 | this.visible = false; 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/ui/LoadingProgressBar.js: -------------------------------------------------------------------------------- 1 | export class LoadingProgressBar { 2 | 3 | constructor(container) { 4 | 5 | this.idGen = 0; 6 | 7 | this.tasks = []; 8 | 9 | this.container = container || document.body; 10 | 11 | this.progressBarContainerOuter = document.createElement('div'); 12 | this.progressBarContainerOuter.className = 'progressBarOuterContainer'; 13 | this.progressBarContainerOuter.style.display = 'none'; 14 | 15 | this.progressBarBox = document.createElement('div'); 16 | this.progressBarBox.className = 'progressBarBox'; 17 | 18 | this.progressBarBackground = document.createElement('div'); 19 | this.progressBarBackground.className = 'progressBarBackground'; 20 | 21 | this.progressBar = document.createElement('div'); 22 | this.progressBar.className = 'progressBar'; 23 | 24 | this.progressBarBackground.appendChild(this.progressBar); 25 | this.progressBarBox.appendChild(this.progressBarBackground); 26 | this.progressBarContainerOuter.appendChild(this.progressBarBox); 27 | 28 | const style = document.createElement('style'); 29 | style.innerHTML = ` 30 | 31 | .progressBarOuterContainer { 32 | width: 100%; 33 | height: 100%; 34 | margin: 0; 35 | top: 0; 36 | left: 0; 37 | position: absolute; 38 | pointer-events: none; 39 | } 40 | 41 | .progressBarBox { 42 | z-index:99999; 43 | padding: 7px 9px 5px 7px; 44 | background-color: rgba(190, 190, 190, 0.75); 45 | border: #555555 1px solid; 46 | border-radius: 15px; 47 | margin: 0; 48 | position: absolute; 49 | bottom: 50px; 50 | left: 50%; 51 | transform: translate(-50%, 0); 52 | width: 180px; 53 | height: 30px; 54 | pointer-events: auto; 55 | } 56 | 57 | .progressBarBackground { 58 | width: 100%; 59 | height: 25px; 60 | border-radius:10px; 61 | background-color: rgba(128, 128, 128, 0.75); 62 | border: #444444 1px solid; 63 | box-shadow: inset 0 0 10px #333333; 64 | } 65 | 66 | .progressBar { 67 | height: 25px; 68 | width: 0px; 69 | border-radius:10px; 70 | background-color: rgba(0, 200, 0, 0.75); 71 | box-shadow: inset 0 0 10px #003300; 72 | } 73 | 74 | `; 75 | this.progressBarContainerOuter.appendChild(style); 76 | this.container.appendChild(this.progressBarContainerOuter); 77 | } 78 | 79 | show() { 80 | this.progressBarContainerOuter.style.display = 'block'; 81 | } 82 | 83 | hide() { 84 | this.progressBarContainerOuter.style.display = 'none'; 85 | } 86 | 87 | setProgress(progress) { 88 | this.progressBar.style.width = progress + '%'; 89 | } 90 | 91 | setContainer(container) { 92 | if (this.container && this.progressBarContainerOuter.parentElement === this.container) { 93 | this.container.removeChild(this.progressBarContainerOuter); 94 | } 95 | if (container) { 96 | this.container = container; 97 | this.container.appendChild(this.progressBarContainerOuter); 98 | this.progressBarContainerOuter.style.zIndex = this.container.style.zIndex + 1; 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/ui/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | import { fadeElement } from './Util.js'; 2 | 3 | const STANDARD_FADE_DURATION = 500; 4 | 5 | export class LoadingSpinner { 6 | 7 | static elementIDGen = 0; 8 | 9 | constructor(message, container) { 10 | 11 | this.taskIDGen = 0; 12 | this.elementID = LoadingSpinner.elementIDGen++; 13 | 14 | this.tasks = []; 15 | 16 | this.message = message || 'Loading...'; 17 | this.container = container || document.body; 18 | 19 | this.spinnerContainerOuter = document.createElement('div'); 20 | this.spinnerContainerOuter.className = `spinnerOuterContainer${this.elementID}`; 21 | this.spinnerContainerOuter.style.display = 'none'; 22 | 23 | this.spinnerContainerPrimary = document.createElement('div'); 24 | this.spinnerContainerPrimary.className = `spinnerContainerPrimary${this.elementID}`; 25 | this.spinnerPrimary = document.createElement('div'); 26 | this.spinnerPrimary.classList.add(`spinner${this.elementID}`, `spinnerPrimary${this.elementID}`); 27 | this.messageContainerPrimary = document.createElement('div'); 28 | this.messageContainerPrimary.classList.add(`messageContainer${this.elementID}`, `messageContainerPrimary${this.elementID}`); 29 | this.messageContainerPrimary.innerHTML = this.message; 30 | 31 | this.spinnerContainerMin = document.createElement('div'); 32 | this.spinnerContainerMin.className = `spinnerContainerMin${this.elementID}`; 33 | this.spinnerMin = document.createElement('div'); 34 | this.spinnerMin.classList.add(`spinner${this.elementID}`, `spinnerMin${this.elementID}`); 35 | this.messageContainerMin = document.createElement('div'); 36 | this.messageContainerMin.classList.add(`messageContainer${this.elementID}`, `messageContainerMin${this.elementID}`); 37 | this.messageContainerMin.innerHTML = this.message; 38 | 39 | this.spinnerContainerPrimary.appendChild(this.spinnerPrimary); 40 | this.spinnerContainerPrimary.appendChild(this.messageContainerPrimary); 41 | this.spinnerContainerOuter.appendChild(this.spinnerContainerPrimary); 42 | 43 | this.spinnerContainerMin.appendChild(this.spinnerMin); 44 | this.spinnerContainerMin.appendChild(this.messageContainerMin); 45 | this.spinnerContainerOuter.appendChild(this.spinnerContainerMin); 46 | 47 | const style = document.createElement('style'); 48 | style.innerHTML = ` 49 | 50 | .spinnerOuterContainer${this.elementID} { 51 | width: 100%; 52 | height: 100%; 53 | margin: 0; 54 | top: 0; 55 | left: 0; 56 | position: absolute; 57 | pointer-events: none; 58 | } 59 | 60 | .messageContainer${this.elementID} { 61 | height: 20px; 62 | font-family: arial; 63 | font-size: 12pt; 64 | color: #ffffff; 65 | text-align: center; 66 | vertical-align: middle; 67 | } 68 | 69 | .spinner${this.elementID} { 70 | padding: 15px; 71 | background: #07e8d6; 72 | z-index:99999; 73 | 74 | aspect-ratio: 1; 75 | border-radius: 50%; 76 | --_m: 77 | conic-gradient(#0000,#000), 78 | linear-gradient(#000 0 0) content-box; 79 | -webkit-mask: var(--_m); 80 | mask: var(--_m); 81 | -webkit-mask-composite: source-out; 82 | mask-composite: subtract; 83 | box-sizing: border-box; 84 | animation: load 1s linear infinite; 85 | } 86 | 87 | .spinnerContainerPrimary${this.elementID} { 88 | z-index:99999; 89 | background-color: rgba(128, 128, 128, 0.75); 90 | border: #666666 1px solid; 91 | border-radius: 5px; 92 | padding-top: 20px; 93 | padding-bottom: 10px; 94 | margin: 0; 95 | position: absolute; 96 | top: 50%; 97 | left: 50%; 98 | transform: translate(-80px, -80px); 99 | width: 180px; 100 | pointer-events: auto; 101 | } 102 | 103 | .spinnerPrimary${this.elementID} { 104 | width: 120px; 105 | margin-left: 30px; 106 | } 107 | 108 | .messageContainerPrimary${this.elementID} { 109 | padding-top: 15px; 110 | } 111 | 112 | .spinnerContainerMin${this.elementID} { 113 | z-index:99999; 114 | background-color: rgba(128, 128, 128, 0.75); 115 | border: #666666 1px solid; 116 | border-radius: 5px; 117 | padding-top: 20px; 118 | padding-bottom: 15px; 119 | margin: 0; 120 | position: absolute; 121 | bottom: 50px; 122 | left: 50%; 123 | transform: translate(-50%, 0); 124 | display: flex; 125 | flex-direction: left; 126 | pointer-events: auto; 127 | min-width: 250px; 128 | } 129 | 130 | .messageContainerMin${this.elementID} { 131 | margin-right: 15px; 132 | } 133 | 134 | .spinnerMin${this.elementID} { 135 | width: 50px; 136 | height: 50px; 137 | margin-left: 15px; 138 | margin-right: 25px; 139 | } 140 | 141 | .messageContainerMin${this.elementID} { 142 | padding-top: 15px; 143 | } 144 | 145 | @keyframes load { 146 | to{transform: rotate(1turn)} 147 | } 148 | 149 | `; 150 | this.spinnerContainerOuter.appendChild(style); 151 | this.container.appendChild(this.spinnerContainerOuter); 152 | 153 | this.setMinimized(false, true); 154 | 155 | this.fadeTransitions = []; 156 | } 157 | 158 | addTask(message) { 159 | const newTask = { 160 | 'message': message, 161 | 'id': this.taskIDGen++ 162 | }; 163 | this.tasks.push(newTask); 164 | this.update(); 165 | return newTask.id; 166 | } 167 | 168 | removeTask(id) { 169 | let index = 0; 170 | for (let task of this.tasks) { 171 | if (task.id === id) { 172 | this.tasks.splice(index, 1); 173 | break; 174 | } 175 | index++; 176 | } 177 | this.update(); 178 | } 179 | 180 | removeAllTasks() { 181 | this.tasks = []; 182 | this.update(); 183 | } 184 | 185 | setMessageForTask(id, message) { 186 | for (let task of this.tasks) { 187 | if (task.id === id) { 188 | task.message = message; 189 | break; 190 | } 191 | } 192 | this.update(); 193 | } 194 | 195 | update() { 196 | if (this.tasks.length > 0) { 197 | this.show(); 198 | this.setMessage(this.tasks[this.tasks.length - 1].message); 199 | } else { 200 | this.hide(); 201 | } 202 | } 203 | 204 | show() { 205 | this.spinnerContainerOuter.style.display = 'block'; 206 | this.visible = true; 207 | } 208 | 209 | hide() { 210 | this.spinnerContainerOuter.style.display = 'none'; 211 | this.visible = false; 212 | } 213 | 214 | setContainer(container) { 215 | if (this.container && this.spinnerContainerOuter.parentElement === this.container) { 216 | this.container.removeChild(this.spinnerContainerOuter); 217 | } 218 | if (container) { 219 | this.container = container; 220 | this.container.appendChild(this.spinnerContainerOuter); 221 | this.spinnerContainerOuter.style.zIndex = this.container.style.zIndex + 1; 222 | } 223 | } 224 | 225 | setMinimized(minimized, instant) { 226 | const showHideSpinner = (element, show, instant, displayStyle, fadeTransitionsIndex) => { 227 | if (instant) { 228 | element.style.display = show ? displayStyle : 'none'; 229 | } else { 230 | this.fadeTransitions[fadeTransitionsIndex] = fadeElement(element, !show, displayStyle, STANDARD_FADE_DURATION, () => { 231 | this.fadeTransitions[fadeTransitionsIndex] = null; 232 | }); 233 | } 234 | }; 235 | showHideSpinner(this.spinnerContainerPrimary, !minimized, instant, 'block', 0); 236 | showHideSpinner(this.spinnerContainerMin, minimized, instant, 'flex', 1); 237 | this.minimized = minimized; 238 | } 239 | 240 | setMessage(msg) { 241 | this.messageContainerPrimary.innerHTML = msg; 242 | this.messageContainerMin.innerHTML = msg; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/ui/Util.js: -------------------------------------------------------------------------------- 1 | export const fadeElement = (element, out, displayStyle, duration, onComplete) => { 2 | const startTime = performance.now(); 3 | 4 | let startOpacity = element.style.display === 'none' ? 0 : parseFloat(element.style.opacity); 5 | if (isNaN(startOpacity)) startOpacity = 1; 6 | 7 | const interval = window.setInterval(() => { 8 | const currentTime = performance.now(); 9 | const elapsed = currentTime - startTime; 10 | 11 | let t = Math.min(elapsed / duration, 1.0); 12 | if (t > 0.999) t = 1; 13 | 14 | let opacity; 15 | if (out) { 16 | opacity = (1.0 - t) * startOpacity; 17 | if (opacity < 0.0001) opacity = 0; 18 | } else { 19 | opacity = (1.0 - startOpacity) * t + startOpacity; 20 | } 21 | 22 | if (opacity > 0) { 23 | element.style.display = displayStyle; 24 | element.style.opacity = opacity; 25 | } else { 26 | element.style.display = 'none'; 27 | } 28 | 29 | if (t >= 1) { 30 | if (onComplete) onComplete(); 31 | window.clearInterval(interval); 32 | } 33 | }, 16); 34 | return interval; 35 | }; 36 | 37 | export const cancelFade = (interval) => { 38 | window.clearInterval(interval); 39 | }; 40 | -------------------------------------------------------------------------------- /src/webxr/ARButton.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2010-2024 three.js authors & Mark Kellogg 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | */ 14 | 15 | export class ARButton { 16 | 17 | static createButton( renderer, sessionInit = {} ) { 18 | 19 | const button = document.createElement( 'button' ); 20 | 21 | function showStartAR( /* device */ ) { 22 | 23 | if ( sessionInit.domOverlay === undefined ) { 24 | 25 | const overlay = document.createElement( 'div' ); 26 | overlay.style.display = 'none'; 27 | document.body.appendChild( overlay ); 28 | 29 | const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); 30 | svg.setAttribute( 'width', 38 ); 31 | svg.setAttribute( 'height', 38 ); 32 | svg.style.position = 'absolute'; 33 | svg.style.right = '20px'; 34 | svg.style.top = '20px'; 35 | svg.addEventListener( 'click', function() { 36 | 37 | currentSession.end(); 38 | 39 | } ); 40 | overlay.appendChild( svg ); 41 | 42 | const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); 43 | path.setAttribute( 'd', 'M 12,12 L 28,28 M 28,12 12,28' ); 44 | path.setAttribute( 'stroke', '#fff' ); 45 | path.setAttribute( 'stroke-width', 2 ); 46 | svg.appendChild( path ); 47 | 48 | if ( sessionInit.optionalFeatures === undefined ) { 49 | 50 | sessionInit.optionalFeatures = []; 51 | 52 | } 53 | 54 | sessionInit.optionalFeatures.push( 'dom-overlay' ); 55 | sessionInit.domOverlay = { root: overlay }; 56 | 57 | } 58 | 59 | // 60 | 61 | let currentSession = null; 62 | 63 | async function onSessionStarted( session ) { 64 | 65 | session.addEventListener( 'end', onSessionEnded ); 66 | 67 | renderer.xr.setReferenceSpaceType( 'local' ); 68 | 69 | await renderer.xr.setSession( session ); 70 | 71 | button.textContent = 'STOP AR'; 72 | sessionInit.domOverlay.root.style.display = ''; 73 | 74 | currentSession = session; 75 | 76 | } 77 | 78 | function onSessionEnded( /* event */ ) { 79 | 80 | currentSession.removeEventListener( 'end', onSessionEnded ); 81 | 82 | button.textContent = 'START AR'; 83 | sessionInit.domOverlay.root.style.display = 'none'; 84 | 85 | currentSession = null; 86 | 87 | } 88 | 89 | // 90 | 91 | button.style.display = ''; 92 | 93 | button.style.cursor = 'pointer'; 94 | button.style.left = 'calc(50% - 50px)'; 95 | button.style.width = '100px'; 96 | 97 | button.textContent = 'START AR'; 98 | 99 | button.onmouseenter = function() { 100 | 101 | button.style.opacity = '1.0'; 102 | 103 | }; 104 | 105 | button.onmouseleave = function() { 106 | 107 | button.style.opacity = '0.5'; 108 | 109 | }; 110 | 111 | button.onclick = function() { 112 | 113 | if ( currentSession === null ) { 114 | 115 | navigator.xr.requestSession( 'immersive-ar', sessionInit ).then( onSessionStarted ); 116 | 117 | } else { 118 | 119 | currentSession.end(); 120 | 121 | if ( navigator.xr.offerSession !== undefined ) { 122 | 123 | navigator.xr.offerSession( 'immersive-ar', sessionInit ) 124 | .then( onSessionStarted ) 125 | .catch( ( err ) => { 126 | 127 | console.warn( err ); 128 | 129 | } ); 130 | 131 | } 132 | 133 | } 134 | 135 | }; 136 | 137 | if ( navigator.xr.offerSession !== undefined ) { 138 | 139 | navigator.xr.offerSession( 'immersive-ar', sessionInit ) 140 | .then( onSessionStarted ) 141 | .catch( ( err ) => { 142 | 143 | console.warn( err ); 144 | 145 | } ); 146 | 147 | } 148 | 149 | } 150 | 151 | function disableButton() { 152 | 153 | button.style.display = ''; 154 | 155 | button.style.cursor = 'auto'; 156 | button.style.left = 'calc(50% - 75px)'; 157 | button.style.width = '150px'; 158 | 159 | button.onmouseenter = null; 160 | button.onmouseleave = null; 161 | 162 | button.onclick = null; 163 | 164 | } 165 | 166 | function showARNotSupported() { 167 | 168 | disableButton(); 169 | 170 | button.textContent = 'AR NOT SUPPORTED'; 171 | 172 | } 173 | 174 | function showARNotAllowed( exception ) { 175 | 176 | disableButton(); 177 | 178 | console.warn( 'Exception when trying to call xr.isSessionSupported', exception ); 179 | 180 | button.textContent = 'AR NOT ALLOWED'; 181 | 182 | } 183 | 184 | function stylizeElement( element ) { 185 | 186 | element.style.position = 'absolute'; 187 | element.style.bottom = '20px'; 188 | element.style.padding = '12px 6px'; 189 | element.style.border = '1px solid #fff'; 190 | element.style.borderRadius = '4px'; 191 | element.style.background = 'rgba(0,0,0,0.1)'; 192 | element.style.color = '#fff'; 193 | element.style.font = 'normal 13px sans-serif'; 194 | element.style.textAlign = 'center'; 195 | element.style.opacity = '0.5'; 196 | element.style.outline = 'none'; 197 | element.style.zIndex = '999'; 198 | 199 | } 200 | 201 | if ( 'xr' in navigator ) { 202 | 203 | button.id = 'ARButton'; 204 | button.style.display = 'none'; 205 | 206 | stylizeElement( button ); 207 | 208 | navigator.xr.isSessionSupported( 'immersive-ar' ).then( function( supported ) { 209 | 210 | supported ? showStartAR() : showARNotSupported(); 211 | 212 | } ).catch( showARNotAllowed ); 213 | 214 | return button; 215 | 216 | } else { 217 | 218 | const message = document.createElement( 'a' ); 219 | 220 | if ( window.isSecureContext === false ) { 221 | 222 | message.href = document.location.href.replace( /^http:/, 'https:' ); 223 | message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message 224 | 225 | } else { 226 | 227 | message.href = 'https://immersiveweb.dev/'; 228 | message.innerHTML = 'WEBXR NOT AVAILABLE'; 229 | 230 | } 231 | 232 | message.style.left = 'calc(50% - 90px)'; 233 | message.style.width = '180px'; 234 | message.style.textDecoration = 'none'; 235 | 236 | stylizeElement( message ); 237 | 238 | return message; 239 | 240 | } 241 | 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /src/webxr/VRButton.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2010-2024 three.js authors & Mark Kellogg 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | */ 14 | 15 | export class VRButton { 16 | 17 | static createButton( renderer, sessionInit = {} ) { 18 | 19 | const button = document.createElement( 'button' ); 20 | 21 | function showEnterVR( /* device */ ) { 22 | 23 | let currentSession = null; 24 | 25 | async function onSessionStarted( session ) { 26 | 27 | session.addEventListener( 'end', onSessionEnded ); 28 | 29 | await renderer.xr.setSession( session ); 30 | button.textContent = 'EXIT VR'; 31 | 32 | currentSession = session; 33 | 34 | } 35 | 36 | function onSessionEnded( /* event */ ) { 37 | 38 | currentSession.removeEventListener( 'end', onSessionEnded ); 39 | 40 | button.textContent = 'ENTER VR'; 41 | 42 | currentSession = null; 43 | 44 | } 45 | 46 | // 47 | 48 | button.style.display = ''; 49 | 50 | button.style.cursor = 'pointer'; 51 | button.style.left = 'calc(50% - 50px)'; 52 | button.style.width = '100px'; 53 | 54 | button.textContent = 'ENTER VR'; 55 | 56 | // WebXR's requestReferenceSpace only works if the corresponding feature 57 | // was requested at session creation time. For simplicity, just ask for 58 | // the interesting ones as optional features, but be aware that the 59 | // requestReferenceSpace call will fail if it turns out to be unavailable. 60 | // ('local' is always available for immersive sessions and doesn't need to 61 | // be requested separately.) 62 | 63 | const sessionOptions = { 64 | ...sessionInit, 65 | optionalFeatures: [ 66 | 'local-floor', 67 | 'bounded-floor', 68 | 'layers', 69 | ...( sessionInit.optionalFeatures || [] ) 70 | ], 71 | }; 72 | 73 | button.onmouseenter = function() { 74 | 75 | button.style.opacity = '1.0'; 76 | 77 | }; 78 | 79 | button.onmouseleave = function() { 80 | 81 | button.style.opacity = '0.5'; 82 | 83 | }; 84 | 85 | button.onclick = function() { 86 | 87 | if ( currentSession === null ) { 88 | 89 | navigator.xr.requestSession( 'immersive-vr', sessionOptions ).then( onSessionStarted ); 90 | 91 | } else { 92 | 93 | currentSession.end(); 94 | 95 | if ( navigator.xr.offerSession !== undefined ) { 96 | 97 | navigator.xr.offerSession( 'immersive-vr', sessionOptions ) 98 | .then( onSessionStarted ) 99 | .catch( ( err ) => { 100 | 101 | console.warn( err ); 102 | 103 | } ); 104 | 105 | } 106 | 107 | } 108 | 109 | }; 110 | 111 | if ( navigator.xr.offerSession !== undefined ) { 112 | 113 | navigator.xr.offerSession( 'immersive-vr', sessionOptions ) 114 | .then( onSessionStarted ) 115 | .catch( ( err ) => { 116 | 117 | console.warn( err ); 118 | 119 | } ); 120 | 121 | } 122 | 123 | } 124 | 125 | function disableButton() { 126 | 127 | button.style.display = ''; 128 | 129 | button.style.cursor = 'auto'; 130 | button.style.left = 'calc(50% - 75px)'; 131 | button.style.width = '150px'; 132 | 133 | button.onmouseenter = null; 134 | button.onmouseleave = null; 135 | 136 | button.onclick = null; 137 | 138 | } 139 | 140 | function showWebXRNotFound() { 141 | 142 | disableButton(); 143 | 144 | button.textContent = 'VR NOT SUPPORTED'; 145 | 146 | } 147 | 148 | function showVRNotAllowed( exception ) { 149 | 150 | disableButton(); 151 | 152 | console.warn( 'Exception when trying to call xr.isSessionSupported', exception ); 153 | 154 | button.textContent = 'VR NOT ALLOWED'; 155 | 156 | } 157 | 158 | function stylizeElement( element ) { 159 | 160 | element.style.position = 'absolute'; 161 | element.style.bottom = '20px'; 162 | element.style.padding = '12px 6px'; 163 | element.style.border = '1px solid #fff'; 164 | element.style.borderRadius = '4px'; 165 | element.style.background = 'rgba(0,0,0,0.1)'; 166 | element.style.color = '#fff'; 167 | element.style.font = 'normal 13px sans-serif'; 168 | element.style.textAlign = 'center'; 169 | element.style.opacity = '0.5'; 170 | element.style.outline = 'none'; 171 | element.style.zIndex = '999'; 172 | 173 | } 174 | 175 | if ( 'xr' in navigator ) { 176 | 177 | button.id = 'VRButton'; 178 | button.style.display = 'none'; 179 | 180 | stylizeElement( button ); 181 | 182 | navigator.xr.isSessionSupported( 'immersive-vr' ).then( function( supported ) { 183 | 184 | supported ? showEnterVR() : showWebXRNotFound(); 185 | 186 | if ( supported && VRButton.xrSessionIsGranted ) { 187 | 188 | button.click(); 189 | 190 | } 191 | 192 | } ).catch( showVRNotAllowed ); 193 | 194 | return button; 195 | 196 | } else { 197 | 198 | const message = document.createElement( 'a' ); 199 | 200 | if ( window.isSecureContext === false ) { 201 | 202 | message.href = document.location.href.replace( /^http:/, 'https:' ); 203 | message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message 204 | 205 | } else { 206 | 207 | message.href = 'https://immersiveweb.dev/'; 208 | message.innerHTML = 'WEBXR NOT AVAILABLE'; 209 | 210 | } 211 | 212 | message.style.left = 'calc(50% - 90px)'; 213 | message.style.width = '180px'; 214 | message.style.textDecoration = 'none'; 215 | 216 | stylizeElement( message ); 217 | 218 | return message; 219 | 220 | } 221 | 222 | } 223 | 224 | static registerSessionGrantedListener() { 225 | 226 | if ( typeof navigator !== 'undefined' && 'xr' in navigator ) { 227 | 228 | // WebXRViewer (based on Firefox) has a bug where addEventListener 229 | // throws a silent exception and aborts execution entirely. 230 | if ( /WebXRViewer\//i.test( navigator.userAgent ) ) return; 231 | 232 | navigator.xr.addEventListener( 'sessiongranted', () => { 233 | 234 | VRButton.xrSessionIsGranted = true; 235 | 236 | } ); 237 | 238 | } 239 | 240 | } 241 | 242 | } 243 | 244 | VRButton.xrSessionIsGranted = false; 245 | VRButton.registerSessionGrantedListener(); 246 | -------------------------------------------------------------------------------- /src/webxr/WebXRMode.js: -------------------------------------------------------------------------------- 1 | export const WebXRMode = { 2 | None: 0, 3 | VR: 1, 4 | AR: 2 5 | }; 6 | -------------------------------------------------------------------------------- /src/worker/SortWorker.js: -------------------------------------------------------------------------------- 1 | import SorterWasm from './sorter.wasm'; 2 | import SorterWasmNoSIMD from './sorter_no_simd.wasm'; 3 | import SorterWasmNonShared from './sorter_non_shared.wasm'; 4 | import SorterWasmNoSIMDNonShared from './sorter_no_simd_non_shared.wasm'; 5 | import { isIOS, getIOSSemever } from '../Util.js'; 6 | import { Constants } from '../Constants.js'; 7 | 8 | function sortWorker(self) { 9 | 10 | let wasmInstance; 11 | let wasmMemory; 12 | let useSharedMemory; 13 | let integerBasedSort; 14 | let dynamicMode; 15 | let splatCount; 16 | let indexesToSortOffset; 17 | let sortedIndexesOffset; 18 | let sceneIndexesOffset; 19 | let transformsOffset; 20 | let precomputedDistancesOffset; 21 | let mappedDistancesOffset; 22 | let frequenciesOffset; 23 | let centersOffset; 24 | let modelViewProjOffset; 25 | let countsZero; 26 | let sortedIndexesOut; 27 | let distanceMapRange; 28 | let uploadedSplatCount; 29 | let Constants; 30 | 31 | function sort(splatSortCount, splatRenderCount, modelViewProj, 32 | usePrecomputedDistances, copyIndexesToSort, copyPrecomputedDistances, copyTransforms) { 33 | const sortStartTime = performance.now(); 34 | 35 | if (!useSharedMemory) { 36 | const indexesToSort = new Uint32Array(wasmMemory, indexesToSortOffset, copyIndexesToSort.byteLength / Constants.BytesPerInt); 37 | indexesToSort.set(copyIndexesToSort); 38 | const transforms = new Float32Array(wasmMemory, transformsOffset, copyTransforms.byteLength / Constants.BytesPerFloat); 39 | transforms.set(copyTransforms); 40 | if (usePrecomputedDistances) { 41 | let precomputedDistances; 42 | if (integerBasedSort) { 43 | precomputedDistances = new Int32Array(wasmMemory, precomputedDistancesOffset, 44 | copyPrecomputedDistances.byteLength / Constants.BytesPerInt); 45 | } else { 46 | precomputedDistances = new Float32Array(wasmMemory, precomputedDistancesOffset, 47 | copyPrecomputedDistances.byteLength / Constants.BytesPerFloat); 48 | } 49 | precomputedDistances.set(copyPrecomputedDistances); 50 | } 51 | } 52 | 53 | if (!countsZero) countsZero = new Uint32Array(distanceMapRange); 54 | new Float32Array(wasmMemory, modelViewProjOffset, 16).set(modelViewProj); 55 | new Uint32Array(wasmMemory, frequenciesOffset, distanceMapRange).set(countsZero); 56 | wasmInstance.exports.sortIndexes(indexesToSortOffset, centersOffset, precomputedDistancesOffset, 57 | mappedDistancesOffset, frequenciesOffset, modelViewProjOffset, 58 | sortedIndexesOffset, sceneIndexesOffset, transformsOffset, distanceMapRange, 59 | splatSortCount, splatRenderCount, splatCount, usePrecomputedDistances, integerBasedSort, 60 | dynamicMode); 61 | 62 | const sortMessage = { 63 | 'sortDone': true, 64 | 'splatSortCount': splatSortCount, 65 | 'splatRenderCount': splatRenderCount, 66 | 'sortTime': 0 67 | }; 68 | if (!useSharedMemory) { 69 | const sortedIndexes = new Uint32Array(wasmMemory, sortedIndexesOffset, splatRenderCount); 70 | if (!sortedIndexesOut || sortedIndexesOut.length < splatRenderCount) { 71 | sortedIndexesOut = new Uint32Array(splatRenderCount); 72 | } 73 | sortedIndexesOut.set(sortedIndexes); 74 | sortMessage.sortedIndexes = sortedIndexesOut; 75 | } 76 | const sortEndTime = performance.now(); 77 | 78 | sortMessage.sortTime = sortEndTime - sortStartTime; 79 | 80 | self.postMessage(sortMessage); 81 | } 82 | 83 | self.onmessage = (e) => { 84 | if (e.data.centers) { 85 | centers = e.data.centers; 86 | sceneIndexes = e.data.sceneIndexes; 87 | if (integerBasedSort) { 88 | new Int32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerInt * 4, 89 | e.data.range.count * 4).set(new Int32Array(centers)); 90 | } else { 91 | new Float32Array(wasmMemory, centersOffset + e.data.range.from * Constants.BytesPerFloat * 4, 92 | e.data.range.count * 4).set(new Float32Array(centers)); 93 | } 94 | if (dynamicMode) { 95 | new Uint32Array(wasmMemory, sceneIndexesOffset + e.data.range.from * 4, 96 | e.data.range.count).set(new Uint32Array(sceneIndexes)); 97 | } 98 | uploadedSplatCount = e.data.range.from + e.data.range.count; 99 | } else if (e.data.sort) { 100 | const renderCount = Math.min(e.data.sort.splatRenderCount || 0, uploadedSplatCount); 101 | const sortCount = Math.min(e.data.sort.splatSortCount || 0, uploadedSplatCount); 102 | const usePrecomputedDistances = e.data.sort.usePrecomputedDistances; 103 | 104 | let copyIndexesToSort; 105 | let copyPrecomputedDistances; 106 | let copyTransforms; 107 | if (!useSharedMemory) { 108 | copyIndexesToSort = e.data.sort.indexesToSort; 109 | copyTransforms = e.data.sort.transforms; 110 | if (usePrecomputedDistances) copyPrecomputedDistances = e.data.sort.precomputedDistances; 111 | } 112 | sort(sortCount, renderCount, e.data.sort.modelViewProj, usePrecomputedDistances, 113 | copyIndexesToSort, copyPrecomputedDistances, copyTransforms); 114 | } else if (e.data.init) { 115 | // Yep, this is super hacky and gross :( 116 | Constants = e.data.init.Constants; 117 | 118 | splatCount = e.data.init.splatCount; 119 | useSharedMemory = e.data.init.useSharedMemory; 120 | integerBasedSort = e.data.init.integerBasedSort; 121 | dynamicMode = e.data.init.dynamicMode; 122 | distanceMapRange = e.data.init.distanceMapRange; 123 | uploadedSplatCount = 0; 124 | 125 | const CENTERS_BYTES_PER_ENTRY = integerBasedSort ? (Constants.BytesPerInt * 4) : (Constants.BytesPerFloat * 4); 126 | 127 | const sorterWasmBytes = new Uint8Array(e.data.init.sorterWasmBytes); 128 | 129 | const matrixSize = 16 * Constants.BytesPerFloat; 130 | const memoryRequiredForIndexesToSort = splatCount * Constants.BytesPerInt; 131 | const memoryRequiredForCenters = splatCount * CENTERS_BYTES_PER_ENTRY; 132 | const memoryRequiredForModelViewProjectionMatrix = matrixSize; 133 | const memoryRequiredForPrecomputedDistances = integerBasedSort ? 134 | (splatCount * Constants.BytesPerInt) : (splatCount * Constants.BytesPerFloat); 135 | const memoryRequiredForMappedDistances = splatCount * Constants.BytesPerInt; 136 | const memoryRequiredForSortedIndexes = splatCount * Constants.BytesPerInt; 137 | const memoryRequiredForIntermediateSortBuffers = integerBasedSort ? (distanceMapRange * Constants.BytesPerInt * 2) : 138 | (distanceMapRange * Constants.BytesPerFloat * 2); 139 | const memoryRequiredforTransformIndexes = dynamicMode ? (splatCount * Constants.BytesPerInt) : 0; 140 | const memoryRequiredforTransforms = dynamicMode ? (Constants.MaxScenes * matrixSize) : 0; 141 | const extraMemory = Constants.MemoryPageSize * 32; 142 | 143 | const totalRequiredMemory = memoryRequiredForIndexesToSort + 144 | memoryRequiredForCenters + 145 | memoryRequiredForModelViewProjectionMatrix + 146 | memoryRequiredForPrecomputedDistances + 147 | memoryRequiredForMappedDistances + 148 | memoryRequiredForIntermediateSortBuffers + 149 | memoryRequiredForSortedIndexes + 150 | memoryRequiredforTransformIndexes + 151 | memoryRequiredforTransforms + 152 | extraMemory; 153 | const totalPagesRequired = Math.floor(totalRequiredMemory / Constants.MemoryPageSize ) + 1; 154 | const sorterWasmImport = { 155 | module: {}, 156 | env: { 157 | memory: new WebAssembly.Memory({ 158 | initial: totalPagesRequired, 159 | maximum: totalPagesRequired, 160 | shared: true, 161 | }), 162 | } 163 | }; 164 | WebAssembly.compile(sorterWasmBytes) 165 | .then((wasmModule) => { 166 | return WebAssembly.instantiate(wasmModule, sorterWasmImport); 167 | }) 168 | .then((instance) => { 169 | wasmInstance = instance; 170 | indexesToSortOffset = 0; 171 | centersOffset = indexesToSortOffset + memoryRequiredForIndexesToSort; 172 | modelViewProjOffset = centersOffset + memoryRequiredForCenters; 173 | precomputedDistancesOffset = modelViewProjOffset + memoryRequiredForModelViewProjectionMatrix; 174 | mappedDistancesOffset = precomputedDistancesOffset + memoryRequiredForPrecomputedDistances; 175 | frequenciesOffset = mappedDistancesOffset + memoryRequiredForMappedDistances; 176 | sortedIndexesOffset = frequenciesOffset + memoryRequiredForIntermediateSortBuffers; 177 | sceneIndexesOffset = sortedIndexesOffset + memoryRequiredForSortedIndexes; 178 | transformsOffset = sceneIndexesOffset + memoryRequiredforTransformIndexes; 179 | wasmMemory = sorterWasmImport.env.memory.buffer; 180 | if (useSharedMemory) { 181 | self.postMessage({ 182 | 'sortSetupPhase1Complete': true, 183 | 'indexesToSortBuffer': wasmMemory, 184 | 'indexesToSortOffset': indexesToSortOffset, 185 | 'sortedIndexesBuffer': wasmMemory, 186 | 'sortedIndexesOffset': sortedIndexesOffset, 187 | 'precomputedDistancesBuffer': wasmMemory, 188 | 'precomputedDistancesOffset': precomputedDistancesOffset, 189 | 'transformsBuffer': wasmMemory, 190 | 'transformsOffset': transformsOffset 191 | }); 192 | } else { 193 | self.postMessage({ 194 | 'sortSetupPhase1Complete': true 195 | }); 196 | } 197 | }); 198 | } 199 | }; 200 | } 201 | 202 | export function createSortWorker(splatCount, useSharedMemory, enableSIMDInSort, integerBasedSort, dynamicMode, 203 | splatSortDistanceMapPrecision = Constants.DefaultSplatSortDistanceMapPrecision) { 204 | const worker = new Worker( 205 | URL.createObjectURL( 206 | new Blob(['(', sortWorker.toString(), ')(self)'], { 207 | type: 'application/javascript', 208 | }), 209 | ), 210 | ); 211 | 212 | let sourceWasm = SorterWasm; 213 | 214 | // iOS makes choosing the right WebAssembly configuration tricky :( 215 | const iOSSemVer = isIOS() ? getIOSSemever() : null; 216 | if (!enableSIMDInSort && !useSharedMemory) { 217 | sourceWasm = SorterWasmNoSIMD; 218 | // Testing on various devices has shown that even when shared memory is disabled, the WASM module with shared 219 | // memory can still be used most of the time -- the exception seems to be iOS devices below 16.4 220 | if (iOSSemVer && iOSSemVer.major <= 16 && iOSSemVer.minor < 4) { 221 | sourceWasm = SorterWasmNoSIMDNonShared; 222 | } 223 | } else if (!enableSIMDInSort) { 224 | sourceWasm = SorterWasmNoSIMD; 225 | } else if (!useSharedMemory) { 226 | // Same issue with shared memory as above on iOS devices 227 | if (iOSSemVer && iOSSemVer.major <= 16 && iOSSemVer.minor < 4) { 228 | sourceWasm = SorterWasmNonShared; 229 | } 230 | } 231 | 232 | const sorterWasmBinaryString = atob(sourceWasm); 233 | const sorterWasmBytes = new Uint8Array(sorterWasmBinaryString.length); 234 | for (let i = 0; i < sorterWasmBinaryString.length; i++) { 235 | sorterWasmBytes[i] = sorterWasmBinaryString.charCodeAt(i); 236 | } 237 | 238 | worker.postMessage({ 239 | 'init': { 240 | 'sorterWasmBytes': sorterWasmBytes.buffer, 241 | 'splatCount': splatCount, 242 | 'useSharedMemory': useSharedMemory, 243 | 'integerBasedSort': integerBasedSort, 244 | 'dynamicMode': dynamicMode, 245 | 'distanceMapRange': 1 << splatSortDistanceMapPrecision, 246 | // Super hacky 247 | 'Constants': { 248 | 'BytesPerFloat': Constants.BytesPerFloat, 249 | 'BytesPerInt': Constants.BytesPerInt, 250 | 'MemoryPageSize': Constants.MemoryPageSize, 251 | 'MaxScenes': Constants.MaxScenes 252 | } 253 | } 254 | }); 255 | return worker; 256 | } 257 | -------------------------------------------------------------------------------- /src/worker/compile_wasm.sh: -------------------------------------------------------------------------------- 1 | em++ -std=c++11 sorter.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter.wasm -s IMPORTED_MEMORY=1 -s USE_PTHREADS=1 -msimd128 2 | -------------------------------------------------------------------------------- /src/worker/compile_wasm_no_simd.sh: -------------------------------------------------------------------------------- 1 | em++ -std=c++11 sorter_no_simd.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter_no_simd.wasm -s IMPORTED_MEMORY=1 -s SHARED_MEMORY=1 -------------------------------------------------------------------------------- /src/worker/compile_wasm_no_simd_non_shared.sh: -------------------------------------------------------------------------------- 1 | em++ -std=c++11 sorter_no_simd.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter_no_simd_non_shared.wasm -s IMPORTED_MEMORY=1 -------------------------------------------------------------------------------- /src/worker/compile_wasm_non_shared.sh: -------------------------------------------------------------------------------- 1 | em++ -std=c++11 sorter.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter_non_shared.wasm -s IMPORTED_MEMORY=1 -msimd128 -------------------------------------------------------------------------------- /src/worker/sorter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef __cplusplus 6 | #define EXTERN extern "C" 7 | #else 8 | #define EXTERN 9 | #endif 10 | 11 | #define computeMatMul4x4ThirdRow(a, b, out) \ 12 | out[0] = a[2] * b[0] + a[6] * b[1] + a[10] * b[2] + a[14] * b[3]; \ 13 | out[1] = a[2] * b[4] + a[6] * b[5] + a[10] * b[6] + a[14] * b[7]; \ 14 | out[2] = a[2] * b[8] + a[6] * b[9] + a[10] * b[10] + a[14] * b[11]; \ 15 | out[3] = a[2] * b[12] + a[6] * b[13] + a[10] * b[14] + a[14] * b[15]; 16 | 17 | EXTERN EMSCRIPTEN_KEEPALIVE void sortIndexes(unsigned int* indexes, void* centers, void* precomputedDistances, 18 | int* mappedDistances, unsigned int * frequencies, float* modelViewProj, 19 | unsigned int* indexesOut, unsigned int* sceneIndexes, float* transforms, 20 | unsigned int distanceMapRange, unsigned int sortCount, unsigned int renderCount, 21 | unsigned int splatCount, bool usePrecomputedDistances, bool useIntegerSort, 22 | bool dynamicMode) { 23 | 24 | int maxDistance = -2147483640; 25 | int minDistance = 2147483640; 26 | 27 | float fMVPTRow3[4]; 28 | unsigned int sortStart = renderCount - sortCount; 29 | if (useIntegerSort) { 30 | int* intCenters = (int*)centers; 31 | if (usePrecomputedDistances) { 32 | int* intPrecomputedDistances = (int*)precomputedDistances; 33 | for (unsigned int i = sortStart; i < renderCount; i++) { 34 | int distance = intPrecomputedDistances[indexes[i]]; 35 | mappedDistances[i] = distance; 36 | if (distance > maxDistance) maxDistance = distance; 37 | if (distance < minDistance) minDistance = distance; 38 | } 39 | } else { 40 | int tempOut[4]; 41 | if (dynamicMode) { 42 | int lastTransformIndex = -1; 43 | v128_t b; 44 | for (unsigned int i = sortStart; i < renderCount; i++) { 45 | unsigned int realIndex = indexes[i]; 46 | unsigned int sceneIndex = sceneIndexes[realIndex]; 47 | if ((int)sceneIndex != lastTransformIndex) { 48 | float* transform = &transforms[sceneIndex * 16]; 49 | computeMatMul4x4ThirdRow(modelViewProj, transform, fMVPTRow3); 50 | int iMVPTRow3[] = {(int)(fMVPTRow3[0] * 1000.0), (int)(fMVPTRow3[1] * 1000.0), 51 | (int)(fMVPTRow3[2] * 1000.0), (int)(fMVPTRow3[3] * 1000.0)}; 52 | b = wasm_v128_load(&iMVPTRow3[0]); 53 | lastTransformIndex = (int)sceneIndex; 54 | } 55 | v128_t a = wasm_v128_load(&intCenters[4 * realIndex]); 56 | v128_t prod = wasm_i32x4_mul(a, b); 57 | wasm_v128_store(&tempOut[0], prod); 58 | int distance = tempOut[0] + tempOut[1] + tempOut[2] + tempOut[3]; 59 | mappedDistances[i] = distance; 60 | if (distance > maxDistance) maxDistance = distance; 61 | if (distance < minDistance) minDistance = distance; 62 | } 63 | } else { 64 | int iMVPRow3[] = {(int)(modelViewProj[2] * 1000.0), (int)(modelViewProj[6] * 1000.0), (int)(modelViewProj[10] * 1000.0), 1}; 65 | v128_t b = wasm_v128_load(&iMVPRow3[0]); 66 | for (unsigned int i = sortStart; i < renderCount; i++) { 67 | v128_t a = wasm_v128_load(&intCenters[4 * indexes[i]]); 68 | v128_t prod = wasm_i32x4_mul(a, b); 69 | wasm_v128_store(&tempOut[0], prod); 70 | int distance = tempOut[0] + tempOut[1] + tempOut[2]; 71 | mappedDistances[i] = distance; 72 | if (distance > maxDistance) maxDistance = distance; 73 | if (distance < minDistance) minDistance = distance; 74 | } 75 | } 76 | } 77 | } else { 78 | float* floatCenters = (float*)centers; 79 | if (usePrecomputedDistances) { 80 | float* floatPrecomputedDistances = (float*)precomputedDistances; 81 | for (unsigned int i = sortStart; i < renderCount; i++) { 82 | int distance = (int)(floatPrecomputedDistances[indexes[i]] * 4096.0); 83 | mappedDistances[i] = distance; 84 | if (distance > maxDistance) maxDistance = distance; 85 | if (distance < minDistance) minDistance = distance; 86 | } 87 | } else { 88 | float* fMVP = (float*)modelViewProj; 89 | float* floatTransforms = (float *)transforms; 90 | 91 | // TODO: For some reason, the SIMD approach with floats seems slower, need to investigate further... 92 | /* 93 | float tempOut[4]; 94 | float tempViewProj[] = {fMVP[2], fMVP[6], fMVP[10], 1.0}; 95 | v128_t b = wasm_v128_load(&tempViewProj[0]); 96 | for (unsigned int i = sortStart; i < renderCount; i++) { 97 | v128_t a = wasm_v128_load(&floatCenters[4 * indexes[i]]); 98 | v128_t prod = wasm_f32x4_mul(a, b); 99 | wasm_v128_store(&tempOut[0], prod); 100 | int distance = (int)((tempOut[0] + tempOut[1] + tempOut[2]) * 4096.0); 101 | mappedDistances[i] = distance; 102 | if (distance > maxDistance) maxDistance = distance; 103 | if (distance < minDistance) minDistance = distance; 104 | } 105 | */ 106 | 107 | if (dynamicMode) { 108 | int lastTransformIndex = -1; 109 | for (unsigned int i = sortStart; i < renderCount; i++) { 110 | unsigned int realIndex = indexes[i]; 111 | unsigned int indexOffset = 4 * realIndex; 112 | unsigned int sceneIndex = sceneIndexes[realIndex]; 113 | if ((int)sceneIndex != lastTransformIndex) { 114 | float* transform = &transforms[sceneIndex * 16]; 115 | computeMatMul4x4ThirdRow(modelViewProj, transform, fMVPTRow3); 116 | lastTransformIndex = (int)sceneIndex; 117 | } 118 | int distance = 119 | (int)((fMVPTRow3[0] * floatCenters[indexOffset] + 120 | fMVPTRow3[1] * floatCenters[indexOffset + 1] + 121 | fMVPTRow3[2] * floatCenters[indexOffset + 2] + 122 | fMVPTRow3[3] * floatCenters[indexOffset + 3]) * 4096.0); 123 | mappedDistances[i] = distance; 124 | if (distance > maxDistance) maxDistance = distance; 125 | if (distance < minDistance) minDistance = distance; 126 | } 127 | } else { 128 | for (unsigned int i = sortStart; i < renderCount; i++) { 129 | unsigned int indexOffset = 4 * (unsigned int)indexes[i]; 130 | int distance = 131 | (int)((fMVP[2] * floatCenters[indexOffset] + 132 | fMVP[6] * floatCenters[indexOffset + 1] + 133 | fMVP[10] * floatCenters[indexOffset + 2]) * 4096.0); 134 | mappedDistances[i] = distance; 135 | if (distance > maxDistance) maxDistance = distance; 136 | if (distance < minDistance) minDistance = distance; 137 | } 138 | } 139 | } 140 | } 141 | 142 | float distancesRange = (float)maxDistance - (float)minDistance; 143 | float rangeMap = (float)(distanceMapRange - 1) / distancesRange; 144 | 145 | for (unsigned int i = sortStart; i < renderCount; i++) { 146 | unsigned int frequenciesIndex = (int)((float)(mappedDistances[i] - minDistance) * rangeMap); 147 | mappedDistances[i] = frequenciesIndex; 148 | frequencies[frequenciesIndex] = frequencies[frequenciesIndex] + 1; 149 | } 150 | 151 | unsigned int cumulativeFreq = frequencies[0]; 152 | for (unsigned int i = 1; i < distanceMapRange; i++) { 153 | unsigned int freq = frequencies[i]; 154 | cumulativeFreq += freq; 155 | frequencies[i] = cumulativeFreq; 156 | } 157 | 158 | for (int i = (int)sortStart - 1; i >= 0; i--) { 159 | indexesOut[i] = indexes[i]; 160 | } 161 | 162 | for (int i = (int)renderCount - 1; i >= (int)sortStart; i--) { 163 | unsigned int frequenciesIndex = mappedDistances[i]; 164 | unsigned int freq = frequencies[frequenciesIndex]; 165 | indexesOut[renderCount - freq] = indexes[i]; 166 | frequencies[frequenciesIndex] = freq - 1; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/worker/sorter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/src/worker/sorter.wasm -------------------------------------------------------------------------------- /src/worker/sorter_no_simd.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifdef __cplusplus 5 | #define EXTERN extern "C" 6 | #else 7 | #define EXTERN 8 | #endif 9 | 10 | #define computeMatMul4x4ThirdRow(a, b, out) \ 11 | out[0] = a[2] * b[0] + a[6] * b[1] + a[10] * b[2] + a[14] * b[3]; \ 12 | out[1] = a[2] * b[4] + a[6] * b[5] + a[10] * b[6] + a[14] * b[7]; \ 13 | out[2] = a[2] * b[8] + a[6] * b[9] + a[10] * b[10] + a[14] * b[11]; \ 14 | out[3] = a[2] * b[12] + a[6] * b[13] + a[10] * b[14] + a[14] * b[15]; 15 | 16 | EXTERN EMSCRIPTEN_KEEPALIVE void sortIndexes(unsigned int* indexes, void* centers, void* precomputedDistances, 17 | int* mappedDistances, unsigned int * frequencies, float* modelViewProj, 18 | unsigned int* indexesOut, unsigned int* sceneIndexes, float* transforms, 19 | unsigned int distanceMapRange, unsigned int sortCount, unsigned int renderCount, 20 | unsigned int splatCount, bool usePrecomputedDistances, bool useIntegerSort, 21 | bool dynamicMode) { 22 | 23 | int maxDistance = -2147483640; 24 | int minDistance = 2147483640; 25 | 26 | float fMVPTRow3[4]; 27 | int iMVPTRow3[4]; 28 | unsigned int sortStart = renderCount - sortCount; 29 | if (useIntegerSort) { 30 | int* intCenters = (int*)centers; 31 | if (usePrecomputedDistances) { 32 | int* intPrecomputedDistances = (int*)precomputedDistances; 33 | for (unsigned int i = sortStart; i < renderCount; i++) { 34 | int distance = intPrecomputedDistances[indexes[i]]; 35 | mappedDistances[i] = distance; 36 | if (distance > maxDistance) maxDistance = distance; 37 | if (distance < minDistance) minDistance = distance; 38 | } 39 | } else { 40 | if (dynamicMode) { 41 | int lastTransformIndex = -1; 42 | for (unsigned int i = sortStart; i < renderCount; i++) { 43 | unsigned int realIndex = indexes[i]; 44 | unsigned int indexOffset = 4 * realIndex; 45 | unsigned int sceneIndex = sceneIndexes[realIndex]; 46 | if ((int)sceneIndex != lastTransformIndex) { 47 | float* transform = &transforms[sceneIndex * 16]; 48 | computeMatMul4x4ThirdRow(modelViewProj, transform, fMVPTRow3); 49 | iMVPTRow3[0] = (int)(fMVPTRow3[0] * 1000.0); 50 | iMVPTRow3[1] = (int)(fMVPTRow3[1] * 1000.0); 51 | iMVPTRow3[2] = (int)(fMVPTRow3[2] * 1000.0); 52 | iMVPTRow3[3] = (int)(fMVPTRow3[3] * 1000.0); 53 | lastTransformIndex = (int)sceneIndex; 54 | } 55 | int distance = 56 | (int)((iMVPTRow3[0] * intCenters[indexOffset] + 57 | iMVPTRow3[1] * intCenters[indexOffset + 1] + 58 | iMVPTRow3[2] * intCenters[indexOffset + 2] + 59 | iMVPTRow3[3] * intCenters[indexOffset + 3])); 60 | mappedDistances[i] = distance; 61 | if (distance > maxDistance) maxDistance = distance; 62 | if (distance < minDistance) minDistance = distance; 63 | } 64 | } else { 65 | iMVPTRow3[0] = (int)(modelViewProj[2] * 1000.0); 66 | iMVPTRow3[1] = (int)(modelViewProj[6] * 1000.0); 67 | iMVPTRow3[2] = (int)(modelViewProj[10] * 1000.0); 68 | iMVPTRow3[3] = 1; 69 | for (unsigned int i = sortStart; i < renderCount; i++) { 70 | unsigned int indexOffset = 4 * (unsigned int)indexes[i]; 71 | int distance = 72 | (int)((iMVPTRow3[0] * intCenters[indexOffset] + 73 | iMVPTRow3[1] * intCenters[indexOffset + 1] + 74 | iMVPTRow3[2] * intCenters[indexOffset + 2])); 75 | mappedDistances[i] = distance; 76 | if (distance > maxDistance) maxDistance = distance; 77 | if (distance < minDistance) minDistance = distance; 78 | } 79 | } 80 | } 81 | } else { 82 | float* floatCenters = (float*)centers; 83 | if (usePrecomputedDistances) { 84 | float* floatPrecomputedDistances = (float*)precomputedDistances; 85 | for (unsigned int i = sortStart; i < renderCount; i++) { 86 | int distance = (int)(floatPrecomputedDistances[indexes[i]] * 4096.0); 87 | mappedDistances[i] = distance; 88 | if (distance > maxDistance) maxDistance = distance; 89 | if (distance < minDistance) minDistance = distance; 90 | } 91 | } else { 92 | float* fMVP = (float*)modelViewProj; 93 | float* floatTransforms = (float *)transforms; 94 | 95 | if (dynamicMode) { 96 | int lastTransformIndex = -1; 97 | for (unsigned int i = sortStart; i < renderCount; i++) { 98 | unsigned int realIndex = indexes[i]; 99 | unsigned int indexOffset = 4 * realIndex; 100 | unsigned int sceneIndex = sceneIndexes[realIndex]; 101 | if ((int)sceneIndex != lastTransformIndex) { 102 | float* transform = &transforms[sceneIndex * 16]; 103 | computeMatMul4x4ThirdRow(modelViewProj, transform, fMVPTRow3); 104 | lastTransformIndex = (int)sceneIndex; 105 | } 106 | int distance = 107 | (int)((fMVPTRow3[0] * floatCenters[indexOffset] + 108 | fMVPTRow3[1] * floatCenters[indexOffset + 1] + 109 | fMVPTRow3[2] * floatCenters[indexOffset + 2] + 110 | fMVPTRow3[3] * floatCenters[indexOffset + 3]) * 4096.0); 111 | mappedDistances[i] = distance; 112 | if (distance > maxDistance) maxDistance = distance; 113 | if (distance < minDistance) minDistance = distance; 114 | } 115 | } else { 116 | for (unsigned int i = sortStart; i < renderCount; i++) { 117 | unsigned int indexOffset = 4 * (unsigned int)indexes[i]; 118 | int distance = 119 | (int)((fMVP[2] * floatCenters[indexOffset] + 120 | fMVP[6] * floatCenters[indexOffset + 1] + 121 | fMVP[10] * floatCenters[indexOffset + 2]) * 4096.0); 122 | mappedDistances[i] = distance; 123 | if (distance > maxDistance) maxDistance = distance; 124 | if (distance < minDistance) minDistance = distance; 125 | } 126 | } 127 | } 128 | } 129 | 130 | float distancesRange = (float)maxDistance - (float)minDistance; 131 | float rangeMap = (float)(distanceMapRange - 1) / distancesRange; 132 | 133 | for (unsigned int i = sortStart; i < renderCount; i++) { 134 | unsigned int frequenciesIndex = (int)((float)(mappedDistances[i] - minDistance) * rangeMap); 135 | mappedDistances[i] = frequenciesIndex; 136 | frequencies[frequenciesIndex] = frequencies[frequenciesIndex] + 1; 137 | } 138 | 139 | unsigned int cumulativeFreq = frequencies[0]; 140 | for (unsigned int i = 1; i < distanceMapRange; i++) { 141 | unsigned int freq = frequencies[i]; 142 | cumulativeFreq += freq; 143 | frequencies[i] = cumulativeFreq; 144 | } 145 | 146 | for (int i = (int)sortStart - 1; i >= 0; i--) { 147 | indexesOut[i] = indexes[i]; 148 | } 149 | 150 | for (int i = (int)renderCount - 1; i >= (int)sortStart; i--) { 151 | unsigned int frequenciesIndex = mappedDistances[i]; 152 | unsigned int freq = frequencies[frequenciesIndex]; 153 | indexesOut[renderCount - freq] = indexes[i]; 154 | frequencies[frequenciesIndex] = freq - 1; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/worker/sorter_no_simd.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/src/worker/sorter_no_simd.wasm -------------------------------------------------------------------------------- /src/worker/sorter_no_simd_non_shared.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/src/worker/sorter_no_simd_non_shared.wasm -------------------------------------------------------------------------------- /src/worker/sorter_non_shared.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkkellogg/GaussianSplats3D/2dfc83e497bd76e558fe970c54464b17b5f5c689/src/worker/sorter_non_shared.wasm -------------------------------------------------------------------------------- /stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["stylelint-prettier"], 3 | "rules": { 4 | "prettier/prettier": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /util/create-ksplat.js: -------------------------------------------------------------------------------- 1 | import * as GaussianSplats3D from '../build/gaussian-splats-3d.module.js'; 2 | import * as THREE from '../build/demo/lib/three.module.js'; 3 | import * as fs from 'fs'; 4 | 5 | if (process.argv.length < 4) { 6 | console.log('Expected at least 2 arguments!'); 7 | console.log('Usage: node create-ksplat.js [path to .PLY or .SPLAT] [output file name] [compression level = 0] [alpha removal threshold = 1] [scene center = "0,0,0"] [block size = 5.0] [bucket size = 256] [spherical harmonics level = 0]'); 8 | process.exit(1); 9 | } 10 | 11 | const intputFile = process.argv[2]; 12 | const outputFile = process.argv[3]; 13 | const compressionLevel = (process.argv.length >= 5) ? parseInt(process.argv[4]) : undefined; 14 | const splatAlphaRemovalThreshold = (process.argv.length >= 6) ? parseInt(process.argv[5]) : undefined; 15 | const sceneCenter = (process.argv.length >= 7) ? new THREE.Vector3().fromArray(process.argv[6].split(',')) : undefined; 16 | const blockSize = (process.argv.length >= 8) ? parseFloat(process.argv[7]) : undefined; 17 | const bucketSize = (process.argv.length >= 9) ? parseInt(process.argv[8]) : undefined; 18 | const outSphericalHarmonicsDegree = (process.argv.length >= 10) ? parseInt(process.argv[9]) : undefined; 19 | const sectionSize = 0; 20 | 21 | const fileData = fs.readFileSync(intputFile); 22 | const path = intputFile.toLowerCase().trim(); 23 | const format = GaussianSplats3D.LoaderUtils.sceneFormatFromPath(path); 24 | const splatBuffer = fileBufferToSplatBuffer(fileData.buffer, format, compressionLevel, splatAlphaRemovalThreshold); 25 | 26 | fs.writeFileSync(outputFile, Buffer.from(splatBuffer.bufferData)); 27 | 28 | function fileBufferToSplatBuffer(fileBufferData, format, compressionLevel, alphaRemovalThreshold) { 29 | let splatBuffer; 30 | if (format === GaussianSplats3D.SceneFormat.Ply || format === GaussianSplats3D.SceneFormat.Splat) { 31 | let splatArray; 32 | if (format === GaussianSplats3D.SceneFormat.Ply) { 33 | splatArray = GaussianSplats3D.PlyParser.parseToUncompressedSplatArray(fileBufferData, outSphericalHarmonicsDegree); 34 | } else { 35 | splatArray = GaussianSplats3D.SplatParser.parseStandardSplatToUncompressedSplatArray(fileBufferData); 36 | } 37 | const splatBufferGenerator = GaussianSplats3D.SplatBufferGenerator.getStandardGenerator(alphaRemovalThreshold, compressionLevel, 38 | sectionSize, sceneCenter, blockSize, 39 | bucketSize); 40 | splatBuffer = splatBufferGenerator.generateFromUncompressedSplatArray(splatArray); 41 | } else { 42 | splatBuffer = new GaussianSplats3D.SplatBuffer(fileBufferData); 43 | } 44 | 45 | return splatBuffer; 46 | } 47 | -------------------------------------------------------------------------------- /util/import-base-64.js: -------------------------------------------------------------------------------- 1 | import { createFilter } from '@rollup/pluginutils'; 2 | import { readFileSync } from 'fs'; 3 | 4 | export function base64(opts = {}) { 5 | if (!opts.include) { 6 | throw Error("include option must be specified"); 7 | } 8 | 9 | const filter = createFilter(opts.include, opts.exclude); 10 | return { 11 | name: "base64", 12 | transform(data, id) { 13 | if (filter(id)) { 14 | const fileData = readFileSync(id); 15 | return { 16 | code: `export default "${fileData.toString('base64')}";`, 17 | map: null 18 | } 19 | } 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /util/server.js: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | let baseDirectory = '.'; 6 | let port = 8080; 7 | let host = '0.0.0.0'; 8 | let lasttRequesTime = performance.now() / 1000; 9 | for(let i = 0; i < process.argv.length; ++i) { 10 | if (process.argv[i] == '-d' && i < process.argv.length - 1) { 11 | baseDirectory = process.argv[i + 1]; 12 | } 13 | if (process.argv[i] == '-p' && i < process.argv.length - 1) { 14 | port = process.argv[i + 1]; 15 | } 16 | if (process.argv[i] == '-h' && i < process.argv.length - 1) { 17 | host = process.argv[i + 1]; 18 | } 19 | } 20 | 21 | function sleep(ms) { 22 | return new Promise((resolve) => { 23 | setTimeout(resolve, ms); 24 | }); 25 | } 26 | 27 | http 28 | .createServer( async function (request, response) { 29 | 30 | response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); 31 | response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); 32 | 33 | let filePath = baseDirectory + request.url; 34 | 35 | const extname = path.extname(filePath); 36 | let contentType = "text/html"; 37 | switch (extname) { 38 | case ".js": 39 | contentType = "text/javascript"; 40 | break; 41 | case ".css": 42 | contentType = "text/css"; 43 | break; 44 | case ".json": 45 | contentType = "application/json"; 46 | break; 47 | case ".png": 48 | contentType = "image/png"; 49 | break; 50 | case ".jpg": 51 | contentType = "image/jpg"; 52 | break; 53 | } 54 | 55 | const requestTime = performance.now() / 1000; 56 | if (requestTime - lasttRequesTime > 1) { 57 | console.log(""); 58 | console.log("-----------------------------------------------"); 59 | } 60 | 61 | let queryString; 62 | let queryStringStart = filePath.indexOf('?'); 63 | if (queryStringStart && queryStringStart > 0) { 64 | queryString = filePath.substring(queryStringStart + 1); 65 | filePath = filePath.substring(0, queryStringStart); 66 | } 67 | 68 | let testDirectory = filePath; 69 | if (testDirectory.endsWith("/")) { 70 | testDirectory = testDirectory.substring(0, testDirectory.length - 1); 71 | } 72 | try { 73 | if (fs.lstatSync(filePath).isDirectory()) { 74 | let testDirectory = filePath; 75 | if (!testDirectory.endsWith("/")) testDirectory = testDirectory + "/"; 76 | if (fs.existsSync(testDirectory + "index.html")) { 77 | filePath = testDirectory + "index.html"; 78 | } else if (fs.existsSync(testDirectory + "index.htm")) { 79 | filePath = testDirectory + "index.htm"; 80 | } 81 | } 82 | } catch(err) { 83 | // ignore 84 | } 85 | 86 | try { 87 | const stats = fs.statSync(filePath); 88 | if (stats && stats.size) { 89 | const fileSizeInBytes = stats.size; 90 | response.setHeader("Content-Length", fileSizeInBytes); 91 | } 92 | } catch(err) { 93 | // ignore 94 | } 95 | 96 | fs.readFile(filePath, async function (error, content) { 97 | if (error) { 98 | if (error.code == "ENOENT") { 99 | console.log("HTTP(404) Request for " + filePath + " -> File not found."); 100 | } else { 101 | console.log("HTTP(500)) Request for " + filePath + " -> Server error."); 102 | response.writeHead(500); 103 | response.end( 104 | "Sorry, check with the site admin for error: " + 105 | error.code + 106 | " ..\n" 107 | ); 108 | response.end(); 109 | } 110 | } else { 111 | console.log("HTTP(200) Request for " + filePath); 112 | response.writeHead(200, { "Content-Type": contentType }); 113 | response.end(content, "utf-8"); 114 | } 115 | }); 116 | 117 | lasttRequesTime = requestTime; 118 | }) 119 | .listen(port, host); 120 | console.log("Server running at " + host + ':' + port); --------------------------------------------------------------------------------