bilca commited on
Commit
352fb85
·
verified ·
1 Parent(s): 70e70fd

Upload 56 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eslintrc.json +21 -0
  2. .gitattributes +1 -35
  3. .gitignore +134 -0
  4. .prettierrc +5 -0
  5. LICENSE +21 -0
  6. README.md +106 -12
  7. compile_wasm.sh +18 -0
  8. jest.config.js +5 -0
  9. package-lock.json +0 -0
  10. package.json +58 -0
  11. rollup.config.js +27 -0
  12. src/cameras/Camera.ts +42 -0
  13. src/cameras/CameraData.ts +127 -0
  14. src/controls/FPSControls.ts +122 -0
  15. src/controls/OrbitControls.ts +310 -0
  16. src/core/Object3D.ts +101 -0
  17. src/core/Scene.ts +117 -0
  18. src/custom.d.ts +6 -0
  19. src/events/EventDispatcher.ts +46 -0
  20. src/events/Events.ts +21 -0
  21. src/index.ts +28 -0
  22. src/loaders/Loader.ts +46 -0
  23. src/loaders/PLYLoader.ts +223 -0
  24. src/loaders/SplatvLoader.ts +135 -0
  25. src/math/BVH.ts +56 -0
  26. src/math/Box3.ts +52 -0
  27. src/math/Color32.ts +36 -0
  28. src/math/Matrix3.ts +110 -0
  29. src/math/Matrix4.ts +176 -0
  30. src/math/Plane.ts +29 -0
  31. src/math/Quaternion.ts +177 -0
  32. src/math/Vector3.ts +163 -0
  33. src/math/Vector4.ts +111 -0
  34. src/renderers/WebGLRenderer.ts +110 -0
  35. src/renderers/webgl/passes/FadeInPass.ts +50 -0
  36. src/renderers/webgl/passes/ShaderPass.ts +10 -0
  37. src/renderers/webgl/programs/RenderProgram.ts +574 -0
  38. src/renderers/webgl/programs/ShaderProgram.ts +136 -0
  39. src/renderers/webgl/programs/VideoRenderProgram.ts +378 -0
  40. src/renderers/webgl/utils/DataWorker.ts +163 -0
  41. src/renderers/webgl/utils/IntersectionTester.ts +87 -0
  42. src/renderers/webgl/utils/RenderData.ts +440 -0
  43. src/renderers/webgl/utils/SortWorker.ts +155 -0
  44. src/splats/Splat.ts +136 -0
  45. src/splats/SplatData.ts +213 -0
  46. src/splats/Splatv.ts +18 -0
  47. src/splats/SplatvData.ts +59 -0
  48. src/utils/Converter.ts +96 -0
  49. src/utils/LoaderUtils.ts +74 -0
  50. src/wasm/data.d.ts +20 -0
.eslintrc.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true
5
+ },
6
+ "extends": [
7
+ "eslint:recommended",
8
+ "plugin:@typescript-eslint/recommended",
9
+ "plugin:prettier/recommended"
10
+ ],
11
+ "parser": "@typescript-eslint/parser",
12
+ "parserOptions": {
13
+ "ecmaVersion": "latest",
14
+ "sourceType": "module"
15
+ },
16
+ "plugins": [
17
+ "@typescript-eslint"
18
+ ],
19
+ "rules": {
20
+ }
21
+ }
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ * text=auto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+ .pnpm-debug.log*
9
+
10
+ # Diagnostic reports (https://nodejs.org/api/report.html)
11
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12
+
13
+ # Runtime data
14
+ pids
15
+ *.pid
16
+ *.seed
17
+ *.pid.lock
18
+
19
+ # Directory for instrumented libs generated by jscoverage/JSCover
20
+ lib-cov
21
+
22
+ # Coverage directory used by tools like istanbul
23
+ coverage
24
+ *.lcov
25
+
26
+ # nyc test coverage
27
+ .nyc_output
28
+
29
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30
+ .grunt
31
+
32
+ # Bower dependency directory (https://bower.io/)
33
+ bower_components
34
+
35
+ # node-waf configuration
36
+ .lock-wscript
37
+
38
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
39
+ build/Release
40
+
41
+ # Dependency directories
42
+ node_modules/
43
+ jspm_packages/
44
+
45
+ # Snowpack dependency directory (https://snowpack.dev/)
46
+ web_modules/
47
+
48
+ # TypeScript cache
49
+ *.tsbuildinfo
50
+
51
+ # Optional npm cache directory
52
+ .npm
53
+
54
+ # Optional eslint cache
55
+ .eslintcache
56
+
57
+ # Optional stylelint cache
58
+ .stylelintcache
59
+
60
+ # Microbundle cache
61
+ .rpt2_cache/
62
+ .rts2_cache_cjs/
63
+ .rts2_cache_es/
64
+ .rts2_cache_umd/
65
+
66
+ # Optional REPL history
67
+ .node_repl_history
68
+
69
+ # Output of 'npm pack'
70
+ *.tgz
71
+
72
+ # Yarn Integrity file
73
+ .yarn-integrity
74
+
75
+ # dotenv environment variable files
76
+ .env
77
+ .env.development.local
78
+ .env.test.local
79
+ .env.production.local
80
+ .env.local
81
+
82
+ # parcel-bundler cache (https://parceljs.org/)
83
+ .cache
84
+ .parcel-cache
85
+
86
+ # Next.js build output
87
+ .next
88
+ out
89
+
90
+ # Nuxt.js build / generate output
91
+ .nuxt
92
+ dist
93
+
94
+ # Gatsby files
95
+ .cache/
96
+ # Comment in the public line in if your project uses Gatsby and not Next.js
97
+ # https://nextjs.org/blog/next-9-1#public-directory-support
98
+ # public
99
+
100
+ # vuepress build output
101
+ .vuepress/dist
102
+
103
+ # vuepress v2.x temp and cache directory
104
+ .temp
105
+ .cache
106
+
107
+ # Docusaurus cache and generated files
108
+ .docusaurus
109
+
110
+ # Serverless directories
111
+ .serverless/
112
+
113
+ # FuseBox cache
114
+ .fusebox/
115
+
116
+ # DynamoDB Local files
117
+ .dynamodb/
118
+
119
+ # TernJS port file
120
+ .tern-port
121
+
122
+ # Stores VSCode versions used for testing VSCode extensions
123
+ .vscode-test
124
+
125
+ # yarn v2
126
+ .yarn/cache
127
+ .yarn/unplugged
128
+ .yarn/build-state.yml
129
+ .yarn/install-state.gz
130
+ .pnp.*
131
+
132
+ # IDE
133
+ .vscode/
134
+ playground/
.prettierrc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "tabWidth": 4,
3
+ "useTabs": false,
4
+ "printWidth": 120
5
+ }
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Dylan Ebert
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.
README.md CHANGED
@@ -1,12 +1,106 @@
1
- ---
2
- title: Gsplat Library
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: blue
6
- sdk: static
7
- pinned: false
8
- license: mit
9
- short_description: gsplat_library based on dylanebert library
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # gsplat.js
2
+
3
+ #### JavaScript Gaussian Splatting library
4
+
5
+ gsplat.js is an easy-to-use, general-purpose, open-source 3D Gaussian Splatting library, providing functionality similar to [three.js](https://github.com/mrdoob/three.js) but for Gaussian Splatting.
6
+
7
+ ### Quick Start
8
+
9
+ - **Live Viewer Demo:** Explore this library in action in the 🤗 [Hugging Face demo](https://huggingface.co/spaces/dylanebert/igf). Note: May not work on all devices; use `Bonsai` for the lowest memory requirements.
10
+ - **Editor Demo:** Try new real-time updates and editing features in the [gsplat.js editor](https://huggingface.co/spaces/dylanebert/gsplat-editor).
11
+ - **Code Example:** Start coding immediately with this [jsfiddle example](https://jsfiddle.net/wdn6vasc/).
12
+
13
+ ### Installation
14
+
15
+ **Prerequisites**: Ensure your development environment supports ES6 modules.
16
+
17
+ 1. **Set Up a Project:** (If not already set up)
18
+
19
+ Install [Node.js](https://nodejs.org/en/download/) and [NPM](https://www.npmjs.com/get-npm), then initialize a new project using a module bundler like [Vite](https://vitejs.dev/):
20
+
21
+ ```bash
22
+ npm create vite@latest gsplat -- --template vanilla-ts
23
+ ```
24
+
25
+ 2. **Test Your Environment:**
26
+
27
+ ```bash
28
+ cd gsplat
29
+ npm install
30
+ npm run dev
31
+ ```
32
+
33
+ 3. **Install gsplat.js:**
34
+
35
+ ```bash
36
+ npm install --save gsplat
37
+ ```
38
+
39
+ ### Usage
40
+
41
+ #### Creating a Scene
42
+
43
+ - Import **gsplat.js** components and set up a basic scene.
44
+ - Load Gaussian Splatting data and start a rendering loop.
45
+
46
+ (in `src/main.ts` if you followed the Vite setup)
47
+
48
+ ```js
49
+ import * as SPLAT from "gsplat";
50
+
51
+ const scene = new SPLAT.Scene();
52
+ const camera = new SPLAT.Camera();
53
+ const renderer = new SPLAT.WebGLRenderer();
54
+ const controls = new SPLAT.OrbitControls(camera, renderer.canvas);
55
+
56
+ async function main() {
57
+ const url = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat";
58
+
59
+ await SPLAT.Loader.LoadAsync(url, scene, () => {});
60
+
61
+ const frame = () => {
62
+ controls.update();
63
+ renderer.render(scene, camera);
64
+
65
+ requestAnimationFrame(frame);
66
+ };
67
+
68
+ requestAnimationFrame(frame);
69
+ }
70
+
71
+ main();
72
+ ```
73
+
74
+ This script sets up a basic scene with Gaussian Splatting data loaded from URL and starts a rendering loop.
75
+
76
+ ### FAQ
77
+
78
+ **Q: Can I use .ply files?**
79
+
80
+ A: Yes, gsplat.js supports `.ply` files. See the [ply-converter example](https://github.com/dylanebert/gsplat.js/blob/main/examples/ply-converter/src/main.ts) for details on how to convert `.ply` to `.splat`. Alternatively, convert PLY files from URL in this [jsfiddle example](https://jsfiddle.net/2sq3pvdt/1/).
81
+
82
+ **Q: What are .splat files?**
83
+
84
+ A: `.splat` files are a compact form of the splat data, offering quicker loading times than `.ply` files. They consist of a raw Uint8Array buffer.
85
+
86
+ > ⚠️ The `.splat` format does not contain SH coefficients, so colors are not view-dependent.
87
+
88
+ **Q: Can I convert .splat files to .ply?**
89
+
90
+ A: Yes, see the commented code in the [ply-converter example](https://github.com/dylanebert/gsplat.js/blob/main/examples/ply-converter/src/main.ts). Alternatively, convert `.splat` to `.ply` from URL in this [jsfiddle example](https://jsfiddle.net/aL81ds3e/).
91
+
92
+ > ⚠️ When converting `.ply` -> `.splat` -> `.ply`, SH coefficients will be lost.
93
+
94
+ ### License
95
+
96
+ This project is released under the MIT license. It is built upon several other open-source projects:
97
+
98
+ - [three.js](https://github.com/mrdoob/three.js), MIT License (c) 2010-2023 three.js authors
99
+ - [antimatter15/splat](https://github.com/antimatter15/splat), MIT License (c) 2023 Kevin Kwok
100
+ - [UnityGaussianSplatting](https://github.com/aras-p/UnityGaussianSplatting), MIT License (c) 2023 Aras Pranckevičius
101
+
102
+ Please note that the license of the original [3D Gaussian Splatting](https://github.com/graphdeco-inria/gaussian-splatting) research project is non-commercial. While this library provides an open-source rendering implementation, users should consider the source of the splat data separately.
103
+
104
+ ### Contact
105
+
106
+ Feel free to open issues, join the [Hugging Face Discord](https://hf.co/join/discord), or email me directly at [[email protected]](mailto:[email protected]).
compile_wasm.sh ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ emcc --bind wasm/sort.cpp -Oz -o src/wasm/sort.js \
3
+ -s EXPORT_ES6=1 \
4
+ -s MODULARIZE=1 \
5
+ -s EXPORT_NAME=loadWasm \
6
+ -s EXPORTED_FUNCTIONS="[_sort, _malloc, _free]" \
7
+ -s SINGLE_FILE=1 \
8
+ -s ALLOW_MEMORY_GROWTH=1 \
9
+ -s ENVIRONMENT=worker
10
+
11
+ emcc --bind wasm/data.cpp -Oz -o src/wasm/data.js \
12
+ -s EXPORT_ES6=1 \
13
+ -s MODULARIZE=1 \
14
+ -s EXPORT_NAME=loadWasm \
15
+ -s EXPORTED_FUNCTIONS="[_pack, _malloc, _free]" \
16
+ -s SINGLE_FILE=1 \
17
+ -s ALLOW_MEMORY_GROWTH=1 \
18
+ -s ENVIRONMENT=worker
jest.config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /** @type {import("ts-jest").JestConfigWithTsJest} */
2
+ export default {
3
+ preset: "ts-jest",
4
+ testEnvironment: "node",
5
+ };
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gsplat",
3
+ "version": "1.2.4",
4
+ "description": "JavaScript Gaussian Splatting library",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build:wasm": "sh ./compile_wasm.sh",
10
+ "copy:wasm": "ncp ./src/wasm ./dist/wasm",
11
+ "build": "npm run build:wasm && rollup -c && npm run copy:wasm",
12
+ "test": "jest --passWithNoTests",
13
+ "lint": "eslint \"src/**/*.ts\" \"examples/**/*.ts\"",
14
+ "format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\""
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/dylanebert/splat.js.git"
19
+ },
20
+ "keywords": [
21
+ "gsplat",
22
+ "gaussian splatting",
23
+ "javascript",
24
+ "3d",
25
+ "webgl"
26
+ ],
27
+ "author": "dylanebert",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/dylanebert/splat.js/issues"
31
+ },
32
+ "homepage": "https://github.com/dylanebert/splat.js#readme",
33
+ "devDependencies": {
34
+ "@jest/globals": "^29.7.0",
35
+ "@rollup/plugin-commonjs": "^25.0.7",
36
+ "@rollup/plugin-node-resolve": "^15.2.3",
37
+ "@rollup/plugin-replace": "^5.0.5",
38
+ "@rollup/plugin-terser": "^0.4.4",
39
+ "@rollup/plugin-typescript": "^11.1.5",
40
+ "@types/jest": "^29.5.8",
41
+ "@types/node": "^20.8.10",
42
+ "@typescript-eslint/eslint-plugin": "^6.9.1",
43
+ "@typescript-eslint/parser": "^6.9.1",
44
+ "eslint": "^8.52.0",
45
+ "eslint-config-prettier": "^9.0.0",
46
+ "eslint-plugin-prettier": "^5.0.1",
47
+ "jest": "^29.7.0",
48
+ "ncp": "^2.0.0",
49
+ "prettier": "^3.0.3",
50
+ "rollup": "^4.3.0",
51
+ "rollup-plugin-web-worker-loader": "^1.6.1",
52
+ "ts-jest": "^29.1.1",
53
+ "typescript": "^5.2.2"
54
+ },
55
+ "files": [
56
+ "dist/**/*"
57
+ ]
58
+ }
rollup.config.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import resolve from "@rollup/plugin-node-resolve";
2
+ import commonjs from "@rollup/plugin-commonjs";
3
+ import terser from "@rollup/plugin-terser";
4
+ import typescript from "@rollup/plugin-typescript";
5
+ import workerLoader from "rollup-plugin-web-worker-loader";
6
+ import replace from "@rollup/plugin-replace";
7
+
8
+ export default {
9
+ input: "src/index.ts",
10
+ output: {
11
+ dir: "dist",
12
+ format: "esm",
13
+ name: "gsplat",
14
+ sourcemap: true,
15
+ plugins: [terser()],
16
+ },
17
+ plugins: [
18
+ replace({
19
+ "import.meta.url": "''",
20
+ preventAssignment: true,
21
+ }),
22
+ workerLoader({ targetPlatform: "browser" }),
23
+ resolve({ browser: true, preferBuiltins: false }),
24
+ commonjs(),
25
+ typescript(),
26
+ ],
27
+ };
src/cameras/Camera.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CameraData } from "./CameraData";
2
+ import { Object3D } from "../core/Object3D";
3
+ import { Vector3 } from "../math/Vector3";
4
+ import { Vector4 } from "../math/Vector4";
5
+
6
+ class Camera extends Object3D {
7
+ private _data: CameraData;
8
+
9
+ screenPointToRay: (x: number, y: number) => Vector3;
10
+
11
+ constructor(camera: CameraData | undefined = undefined) {
12
+ super();
13
+
14
+ this._data = camera ? camera : new CameraData();
15
+ this._position = new Vector3(0, 0, -5);
16
+
17
+ this.update = () => {
18
+ this.data.update(this.position, this.rotation);
19
+ };
20
+
21
+ this.screenPointToRay = (x: number, y: number) => {
22
+ const clipSpaceCoords = new Vector4(x, y, -1, 1);
23
+ const inverseProjectionMatrix = this._data.projectionMatrix.invert();
24
+ const cameraSpaceCoords = clipSpaceCoords.multiply(inverseProjectionMatrix);
25
+ const inverseViewMatrix = this._data.viewMatrix.invert();
26
+ const worldSpaceCoords = cameraSpaceCoords.multiply(inverseViewMatrix);
27
+ const worldSpacePosition = new Vector3(
28
+ worldSpaceCoords.x / worldSpaceCoords.w,
29
+ worldSpaceCoords.y / worldSpaceCoords.w,
30
+ worldSpaceCoords.z / worldSpaceCoords.w,
31
+ );
32
+ const direction = worldSpacePosition.subtract(this.position).normalize();
33
+ return direction;
34
+ };
35
+ }
36
+
37
+ get data() {
38
+ return this._data;
39
+ }
40
+ }
41
+
42
+ export { Camera };
src/cameras/CameraData.ts ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Quaternion } from "../math/Quaternion";
2
+ import { Matrix3 } from "../math/Matrix3";
3
+ import { Matrix4 } from "../math/Matrix4";
4
+ import { Vector3 } from "../math/Vector3";
5
+
6
+ class CameraData {
7
+ private _fx: number = 1132;
8
+ private _fy: number = 1132;
9
+ private _near: number = 0.1;
10
+ private _far: number = 100;
11
+
12
+ private _width: number = 512;
13
+ private _height: number = 512;
14
+
15
+ private _projectionMatrix: Matrix4 = new Matrix4();
16
+ private _viewMatrix: Matrix4 = new Matrix4();
17
+ private _viewProj: Matrix4 = new Matrix4();
18
+
19
+ update: (position: Vector3, rotation: Quaternion) => void;
20
+ setSize: (width: number, height: number) => void;
21
+
22
+ private _updateProjectionMatrix: () => void;
23
+
24
+ constructor() {
25
+ this._updateProjectionMatrix = () => {
26
+ // prettier-ignore
27
+ this._projectionMatrix = new Matrix4(
28
+ 2 * this.fx / this.width, 0, 0, 0,
29
+ 0, -2 * this.fy / this.height, 0, 0,
30
+ 0, 0, this.far / (this.far - this.near), 1,
31
+ 0, 0, -(this.far * this.near) / (this.far - this.near), 0
32
+ );
33
+
34
+ this._viewProj = this.projectionMatrix.multiply(this.viewMatrix);
35
+ };
36
+
37
+ this.update = (position: Vector3, rotation: Quaternion) => {
38
+ const R = Matrix3.RotationFromQuaternion(rotation).buffer;
39
+ const t = position.flat();
40
+
41
+ // prettier-ignore
42
+ this._viewMatrix = new Matrix4(
43
+ R[0], R[1], R[2], 0,
44
+ R[3], R[4], R[5], 0,
45
+ R[6], R[7], R[8], 0,
46
+ -t[0] * R[0] - t[1] * R[3] - t[2] * R[6],
47
+ -t[0] * R[1] - t[1] * R[4] - t[2] * R[7],
48
+ -t[0] * R[2] - t[1] * R[5] - t[2] * R[8],
49
+ 1,
50
+ );
51
+
52
+ this._viewProj = this.projectionMatrix.multiply(this.viewMatrix);
53
+ };
54
+
55
+ this.setSize = (width: number, height: number) => {
56
+ this._width = width;
57
+ this._height = height;
58
+ this._updateProjectionMatrix();
59
+ };
60
+ }
61
+
62
+ get fx() {
63
+ return this._fx;
64
+ }
65
+
66
+ set fx(fx: number) {
67
+ if (this._fx !== fx) {
68
+ this._fx = fx;
69
+ this._updateProjectionMatrix();
70
+ }
71
+ }
72
+
73
+ get fy() {
74
+ return this._fy;
75
+ }
76
+
77
+ set fy(fy: number) {
78
+ if (this._fy !== fy) {
79
+ this._fy = fy;
80
+ this._updateProjectionMatrix();
81
+ }
82
+ }
83
+
84
+ get near() {
85
+ return this._near;
86
+ }
87
+
88
+ set near(near: number) {
89
+ if (this._near !== near) {
90
+ this._near = near;
91
+ this._updateProjectionMatrix();
92
+ }
93
+ }
94
+
95
+ get far() {
96
+ return this._far;
97
+ }
98
+
99
+ set far(far: number) {
100
+ if (this._far !== far) {
101
+ this._far = far;
102
+ this._updateProjectionMatrix();
103
+ }
104
+ }
105
+
106
+ get width() {
107
+ return this._width;
108
+ }
109
+
110
+ get height() {
111
+ return this._height;
112
+ }
113
+
114
+ get projectionMatrix() {
115
+ return this._projectionMatrix;
116
+ }
117
+
118
+ get viewMatrix() {
119
+ return this._viewMatrix;
120
+ }
121
+
122
+ get viewProj() {
123
+ return this._viewProj;
124
+ }
125
+ }
126
+
127
+ export { CameraData };
src/controls/FPSControls.ts ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Camera } from "../cameras/Camera";
2
+ import { Quaternion } from "../math/Quaternion";
3
+ import { Matrix3 } from "../math/Matrix3";
4
+ import { Vector3 } from "../math/Vector3";
5
+
6
+ class FPSControls {
7
+ moveSpeed: number = 1.5;
8
+ lookSpeed: number = 0.7;
9
+ dampening: number = 0.5;
10
+ update: () => void;
11
+ dispose: () => void;
12
+
13
+ constructor(camera: Camera, canvas: HTMLCanvasElement) {
14
+ const keys: { [key: string]: boolean } = {};
15
+ let pitch = camera.rotation.toEuler().x;
16
+ let yaw = camera.rotation.toEuler().y;
17
+ let targetPosition = camera.position;
18
+ let pointerLock = false;
19
+
20
+ const onMouseDown = () => {
21
+ canvas.requestPointerLock();
22
+ };
23
+
24
+ const onPointerLockChange = () => {
25
+ pointerLock = document.pointerLockElement === canvas;
26
+ if (pointerLock) {
27
+ canvas.addEventListener("mousemove", onMouseMove);
28
+ } else {
29
+ canvas.removeEventListener("mousemove", onMouseMove);
30
+ }
31
+ };
32
+
33
+ const onMouseMove = (e: MouseEvent) => {
34
+ const mouseX = e.movementX;
35
+ const mouseY = e.movementY;
36
+
37
+ yaw += mouseX * this.lookSpeed * 0.001;
38
+ pitch -= mouseY * this.lookSpeed * 0.001;
39
+ pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
40
+ };
41
+
42
+ const onKeyDown = (e: KeyboardEvent) => {
43
+ keys[e.code] = true;
44
+ // Map arrow keys to WASD keys
45
+ if (e.code === "ArrowUp") keys["KeyW"] = true;
46
+ if (e.code === "ArrowDown") keys["KeyS"] = true;
47
+ if (e.code === "ArrowLeft") keys["KeyA"] = true;
48
+ if (e.code === "ArrowRight") keys["KeyD"] = true;
49
+ };
50
+
51
+ const onKeyUp = (e: KeyboardEvent) => {
52
+ keys[e.code] = false;
53
+ // Map arrow keys to WASD keys
54
+ if (e.code === "ArrowUp") keys["KeyW"] = false;
55
+ if (e.code === "ArrowDown") keys["KeyS"] = false;
56
+ if (e.code === "ArrowLeft") keys["KeyA"] = false;
57
+ if (e.code === "ArrowRight") keys["KeyD"] = false;
58
+ if (e.code === "Escape") document.exitPointerLock();
59
+ };
60
+
61
+ this.update = () => {
62
+ const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
63
+ const forward = new Vector3(-R[2], -R[5], -R[8]);
64
+ const right = new Vector3(R[0], R[3], R[6]);
65
+ let move = new Vector3(0, 0, 0);
66
+ if (keys["KeyS"]) {
67
+ move = move.add(forward);
68
+ }
69
+ if (keys["KeyW"]) {
70
+ move = move.subtract(forward);
71
+ }
72
+ if (keys["KeyA"]) {
73
+ move = move.subtract(right);
74
+ }
75
+ if (keys["KeyD"]) {
76
+ move = move.add(right);
77
+ }
78
+ move = new Vector3(move.x, 0, move.z);
79
+ if (move.magnitude() > 0) {
80
+ move = move.normalize();
81
+ }
82
+
83
+ targetPosition = targetPosition.add(move.multiply(this.moveSpeed * 0.01));
84
+ camera.position = camera.position.add(targetPosition.subtract(camera.position).multiply(this.dampening));
85
+
86
+ camera.rotation = Quaternion.FromEuler(new Vector3(pitch, yaw, 0));
87
+ };
88
+
89
+ const preventDefault = (e: Event) => {
90
+ e.preventDefault();
91
+ e.stopPropagation();
92
+ };
93
+
94
+ this.dispose = () => {
95
+ canvas.removeEventListener("dragenter", preventDefault);
96
+ canvas.removeEventListener("dragover", preventDefault);
97
+ canvas.removeEventListener("dragleave", preventDefault);
98
+ canvas.removeEventListener("contextmenu", preventDefault);
99
+ canvas.removeEventListener("mousedown", onMouseDown);
100
+
101
+ document.removeEventListener("pointerlockchange", onPointerLockChange);
102
+
103
+ window.removeEventListener("keydown", onKeyDown);
104
+ window.removeEventListener("keyup", onKeyUp);
105
+ };
106
+
107
+ window.addEventListener("keydown", onKeyDown);
108
+ window.addEventListener("keyup", onKeyUp);
109
+
110
+ canvas.addEventListener("dragenter", preventDefault);
111
+ canvas.addEventListener("dragover", preventDefault);
112
+ canvas.addEventListener("dragleave", preventDefault);
113
+ canvas.addEventListener("contextmenu", preventDefault);
114
+ canvas.addEventListener("mousedown", onMouseDown);
115
+
116
+ document.addEventListener("pointerlockchange", onPointerLockChange);
117
+
118
+ this.update();
119
+ }
120
+ }
121
+
122
+ export { FPSControls };
src/controls/OrbitControls.ts ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Camera } from "../cameras/Camera";
2
+ import { Matrix3 } from "../math/Matrix3";
3
+ import { Quaternion } from "../math/Quaternion";
4
+ import { Vector3 } from "../math/Vector3";
5
+
6
+ class OrbitControls {
7
+ minAngle: number = -90;
8
+ maxAngle: number = 90;
9
+ minZoom: number = 0.1;
10
+ maxZoom: number = 30;
11
+ orbitSpeed: number = 1;
12
+ panSpeed: number = 1;
13
+ zoomSpeed: number = 1;
14
+ dampening: number = 0.12;
15
+ setCameraTarget: (newTarget: Vector3) => void = () => {};
16
+ update: () => void;
17
+ dispose: () => void;
18
+
19
+ constructor(
20
+ camera: Camera,
21
+ canvas: HTMLElement,
22
+ alpha: number = 0.5,
23
+ beta: number = 0.5,
24
+ radius: number = 5,
25
+ enableKeyboardControls: boolean = true,
26
+ inputTarget: Vector3 = new Vector3(),
27
+ ) {
28
+ let target = inputTarget.clone();
29
+
30
+ let desiredTarget = target.clone();
31
+ let desiredAlpha = alpha;
32
+ let desiredBeta = beta;
33
+ let desiredRadius = radius;
34
+
35
+ let dragging = false;
36
+ let panning = false;
37
+ let lastDist = 0;
38
+ let lastX = 0;
39
+ let lastY = 0;
40
+
41
+ const keys: { [key: string]: boolean } = {};
42
+
43
+ let isUpdatingCamera = false;
44
+
45
+ const onCameraChange = () => {
46
+ if (isUpdatingCamera) return;
47
+
48
+ const eulerRotation = camera.rotation.toEuler();
49
+ desiredAlpha = -eulerRotation.y;
50
+ desiredBeta = -eulerRotation.x;
51
+
52
+ const x = camera.position.x - desiredRadius * Math.sin(desiredAlpha) * Math.cos(desiredBeta);
53
+ const y = camera.position.y + desiredRadius * Math.sin(desiredBeta);
54
+ const z = camera.position.z + desiredRadius * Math.cos(desiredAlpha) * Math.cos(desiredBeta);
55
+
56
+ desiredTarget = new Vector3(x, y, z);
57
+ };
58
+
59
+ camera.addEventListener("objectChanged", onCameraChange);
60
+
61
+ this.setCameraTarget = (newTarget: Vector3) => {
62
+ const dx = newTarget.x - camera.position.x;
63
+ const dy = newTarget.y - camera.position.y;
64
+ const dz = newTarget.z - camera.position.z;
65
+ desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz);
66
+ desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
67
+ desiredAlpha = -Math.atan2(dx, dz);
68
+ desiredTarget = new Vector3(newTarget.x, newTarget.y, newTarget.z);
69
+ };
70
+
71
+ const computeZoomNorm = () => {
72
+ return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom);
73
+ };
74
+
75
+ const onKeyDown = (e: KeyboardEvent) => {
76
+ keys[e.code] = true;
77
+ // Map arrow keys to WASD keys
78
+ if (e.code === "ArrowUp") keys["KeyW"] = true;
79
+ if (e.code === "ArrowDown") keys["KeyS"] = true;
80
+ if (e.code === "ArrowLeft") keys["KeyA"] = true;
81
+ if (e.code === "ArrowRight") keys["KeyD"] = true;
82
+ };
83
+
84
+ const onKeyUp = (e: KeyboardEvent) => {
85
+ keys[e.code] = false; // Map arrow keys to WASD keys
86
+ if (e.code === "ArrowUp") keys["KeyW"] = false;
87
+ if (e.code === "ArrowDown") keys["KeyS"] = false;
88
+ if (e.code === "ArrowLeft") keys["KeyA"] = false;
89
+ if (e.code === "ArrowRight") keys["KeyD"] = false;
90
+ };
91
+
92
+ const onMouseDown = (e: MouseEvent) => {
93
+ preventDefault(e);
94
+
95
+ dragging = true;
96
+ panning = e.button === 2;
97
+ lastX = e.clientX;
98
+ lastY = e.clientY;
99
+ window.addEventListener("mouseup", onMouseUp);
100
+ };
101
+
102
+ const onMouseUp = (e: MouseEvent) => {
103
+ preventDefault(e);
104
+
105
+ dragging = false;
106
+ panning = false;
107
+ window.removeEventListener("mouseup", onMouseUp);
108
+ };
109
+
110
+ const onMouseMove = (e: MouseEvent) => {
111
+ preventDefault(e);
112
+
113
+ if (!dragging || !camera) return;
114
+
115
+ const dx = e.clientX - lastX;
116
+ const dy = e.clientY - lastY;
117
+
118
+ if (panning) {
119
+ const zoomNorm = computeZoomNorm();
120
+ const panX = -dx * this.panSpeed * 0.01 * zoomNorm;
121
+ const panY = -dy * this.panSpeed * 0.01 * zoomNorm;
122
+ const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
123
+ const right = new Vector3(R[0], R[3], R[6]);
124
+ const up = new Vector3(R[1], R[4], R[7]);
125
+ desiredTarget = desiredTarget.add(right.multiply(panX));
126
+ desiredTarget = desiredTarget.add(up.multiply(panY));
127
+ } else {
128
+ desiredAlpha -= dx * this.orbitSpeed * 0.003;
129
+ desiredBeta += dy * this.orbitSpeed * 0.003;
130
+ desiredBeta = Math.min(
131
+ Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
132
+ (this.maxAngle * Math.PI) / 180,
133
+ );
134
+ }
135
+
136
+ lastX = e.clientX;
137
+ lastY = e.clientY;
138
+ };
139
+
140
+ const onWheel = (e: WheelEvent) => {
141
+ preventDefault(e);
142
+
143
+ const zoomNorm = computeZoomNorm();
144
+ desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm;
145
+ desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
146
+ };
147
+
148
+ const onTouchStart = (e: TouchEvent) => {
149
+ preventDefault(e);
150
+
151
+ if (e.touches.length === 1) {
152
+ dragging = true;
153
+ panning = false;
154
+ lastX = e.touches[0].clientX;
155
+ lastY = e.touches[0].clientY;
156
+ lastDist = 0;
157
+ } else if (e.touches.length === 2) {
158
+ dragging = true;
159
+ panning = true;
160
+ lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
161
+ lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
162
+ const distX = e.touches[0].clientX - e.touches[1].clientX;
163
+ const distY = e.touches[0].clientY - e.touches[1].clientY;
164
+ lastDist = Math.sqrt(distX * distX + distY * distY);
165
+ }
166
+ };
167
+
168
+ const onTouchEnd = (e: TouchEvent) => {
169
+ preventDefault(e);
170
+
171
+ dragging = false;
172
+ panning = false;
173
+ };
174
+
175
+ const onTouchMove = (e: TouchEvent) => {
176
+ preventDefault(e);
177
+
178
+ if (!dragging || !camera) return;
179
+
180
+ if (panning) {
181
+ const zoomNorm = computeZoomNorm();
182
+
183
+ const distX = e.touches[0].clientX - e.touches[1].clientX;
184
+ const distY = e.touches[0].clientY - e.touches[1].clientY;
185
+ const dist = Math.sqrt(distX * distX + distY * distY);
186
+ const delta = lastDist - dist;
187
+ desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm;
188
+ desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
189
+ lastDist = dist;
190
+
191
+ const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
192
+ const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
193
+ const dx = touchX - lastX;
194
+ const dy = touchY - lastY;
195
+ const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
196
+ const right = new Vector3(R[0], R[3], R[6]);
197
+ const up = new Vector3(R[1], R[4], R[7]);
198
+ desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm));
199
+ desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm));
200
+ lastX = touchX;
201
+ lastY = touchY;
202
+ } else {
203
+ const dx = e.touches[0].clientX - lastX;
204
+ const dy = e.touches[0].clientY - lastY;
205
+
206
+ desiredAlpha -= dx * this.orbitSpeed * 0.003;
207
+ desiredBeta += dy * this.orbitSpeed * 0.003;
208
+ desiredBeta = Math.min(
209
+ Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
210
+ (this.maxAngle * Math.PI) / 180,
211
+ );
212
+
213
+ lastX = e.touches[0].clientX;
214
+ lastY = e.touches[0].clientY;
215
+ }
216
+ };
217
+
218
+ const lerp = (a: number, b: number, t: number) => {
219
+ return (1 - t) * a + t * b;
220
+ };
221
+
222
+ this.update = () => {
223
+ isUpdatingCamera = true;
224
+
225
+ alpha = lerp(alpha, desiredAlpha, this.dampening);
226
+ beta = lerp(beta, desiredBeta, this.dampening);
227
+ radius = lerp(radius, desiredRadius, this.dampening);
228
+ target = target.lerp(desiredTarget, this.dampening);
229
+
230
+ const x = target.x + radius * Math.sin(alpha) * Math.cos(beta);
231
+ const y = target.y - radius * Math.sin(beta);
232
+ const z = target.z - radius * Math.cos(alpha) * Math.cos(beta);
233
+ camera.position = new Vector3(x, y, z);
234
+
235
+ const direction = target.subtract(camera.position).normalize();
236
+ const rx = Math.asin(-direction.y);
237
+ const ry = Math.atan2(direction.x, direction.z);
238
+ camera.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0));
239
+
240
+ const moveSpeed = 0.025;
241
+ const rotateSpeed = 0.01;
242
+
243
+ const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer;
244
+ const forward = new Vector3(-R[2], -R[5], -R[8]);
245
+ const right = new Vector3(R[0], R[3], R[6]);
246
+
247
+ if (keys["KeyS"]) desiredTarget = desiredTarget.add(forward.multiply(moveSpeed));
248
+ if (keys["KeyW"]) desiredTarget = desiredTarget.subtract(forward.multiply(moveSpeed));
249
+ if (keys["KeyA"]) desiredTarget = desiredTarget.subtract(right.multiply(moveSpeed));
250
+ if (keys["KeyD"]) desiredTarget = desiredTarget.add(right.multiply(moveSpeed));
251
+
252
+ // Add rotation with 'e' and 'q' for horizontal rotation
253
+ if (keys["KeyE"]) desiredAlpha += rotateSpeed;
254
+ if (keys["KeyQ"]) desiredAlpha -= rotateSpeed;
255
+
256
+ // Add rotation with 'r' and 'f' for vertical rotation
257
+ if (keys["KeyR"]) desiredBeta += rotateSpeed;
258
+ if (keys["KeyF"]) desiredBeta -= rotateSpeed;
259
+
260
+ isUpdatingCamera = false;
261
+ };
262
+
263
+ const preventDefault = (e: Event) => {
264
+ e.preventDefault();
265
+ e.stopPropagation();
266
+ };
267
+
268
+ this.dispose = () => {
269
+ canvas.removeEventListener("dragenter", preventDefault);
270
+ canvas.removeEventListener("dragover", preventDefault);
271
+ canvas.removeEventListener("dragleave", preventDefault);
272
+ canvas.removeEventListener("contextmenu", preventDefault);
273
+
274
+ canvas.removeEventListener("mousedown", onMouseDown);
275
+ canvas.removeEventListener("mousemove", onMouseMove);
276
+ canvas.removeEventListener("wheel", onWheel);
277
+
278
+ canvas.removeEventListener("touchstart", onTouchStart);
279
+ canvas.removeEventListener("touchend", onTouchEnd);
280
+ canvas.removeEventListener("touchmove", onTouchMove);
281
+
282
+ if (enableKeyboardControls) {
283
+ window.removeEventListener("keydown", onKeyDown);
284
+ window.removeEventListener("keyup", onKeyUp);
285
+ }
286
+ };
287
+
288
+ if (enableKeyboardControls) {
289
+ window.addEventListener("keydown", onKeyDown);
290
+ window.addEventListener("keyup", onKeyUp);
291
+ }
292
+
293
+ canvas.addEventListener("dragenter", preventDefault);
294
+ canvas.addEventListener("dragover", preventDefault);
295
+ canvas.addEventListener("dragleave", preventDefault);
296
+ canvas.addEventListener("contextmenu", preventDefault);
297
+
298
+ canvas.addEventListener("mousedown", onMouseDown);
299
+ canvas.addEventListener("mousemove", onMouseMove);
300
+ canvas.addEventListener("wheel", onWheel);
301
+
302
+ canvas.addEventListener("touchstart", onTouchStart);
303
+ canvas.addEventListener("touchend", onTouchEnd);
304
+ canvas.addEventListener("touchmove", onTouchMove);
305
+
306
+ this.update();
307
+ }
308
+ }
309
+
310
+ export { OrbitControls };
src/core/Object3D.ts ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Vector3 } from "../math/Vector3";
2
+ import { Quaternion } from "../math/Quaternion";
3
+ import { EventDispatcher } from "../events/EventDispatcher";
4
+ import { Matrix4 } from "../math/Matrix4";
5
+ import { ObjectChangedEvent } from "../events/Events";
6
+
7
+ abstract class Object3D extends EventDispatcher {
8
+ public positionChanged: boolean = false;
9
+ public rotationChanged: boolean = false;
10
+ public scaleChanged: boolean = false;
11
+
12
+ protected _position: Vector3 = new Vector3();
13
+ protected _rotation: Quaternion = new Quaternion();
14
+ protected _scale: Vector3 = new Vector3(1, 1, 1);
15
+ protected _transform: Matrix4 = new Matrix4();
16
+
17
+ protected _changeEvent = new ObjectChangedEvent(this);
18
+
19
+ update: () => void;
20
+ applyPosition: () => void;
21
+ applyRotation: () => void;
22
+ applyScale: () => void;
23
+ raiseChangeEvent: () => void;
24
+
25
+ constructor() {
26
+ super();
27
+
28
+ this.update = () => {};
29
+
30
+ this.applyPosition = () => {
31
+ this.position = new Vector3();
32
+ };
33
+
34
+ this.applyRotation = () => {
35
+ this.rotation = new Quaternion();
36
+ };
37
+
38
+ this.applyScale = () => {
39
+ this.scale = new Vector3(1, 1, 1);
40
+ };
41
+
42
+ this.raiseChangeEvent = () => {
43
+ this.dispatchEvent(this._changeEvent);
44
+ };
45
+ }
46
+
47
+ protected _updateMatrix() {
48
+ this._transform = Matrix4.Compose(this._position, this._rotation, this._scale);
49
+ }
50
+
51
+ get position() {
52
+ return this._position;
53
+ }
54
+
55
+ set position(position: Vector3) {
56
+ if (!this._position.equals(position)) {
57
+ this._position = position;
58
+ this.positionChanged = true;
59
+ this._updateMatrix();
60
+ this.dispatchEvent(this._changeEvent);
61
+ }
62
+ }
63
+
64
+ get rotation() {
65
+ return this._rotation;
66
+ }
67
+
68
+ set rotation(rotation: Quaternion) {
69
+ if (!this._rotation.equals(rotation)) {
70
+ this._rotation = rotation;
71
+ this.rotationChanged = true;
72
+ this._updateMatrix();
73
+ this.dispatchEvent(this._changeEvent);
74
+ }
75
+ }
76
+
77
+ get scale() {
78
+ return this._scale;
79
+ }
80
+
81
+ set scale(scale: Vector3) {
82
+ if (!this._scale.equals(scale)) {
83
+ this._scale = scale;
84
+ this.scaleChanged = true;
85
+ this._updateMatrix();
86
+ this.dispatchEvent(this._changeEvent);
87
+ }
88
+ }
89
+
90
+ get forward() {
91
+ let forward = new Vector3(0, 0, 1);
92
+ forward = this.rotation.apply(forward);
93
+ return forward;
94
+ }
95
+
96
+ get transform() {
97
+ return this._transform;
98
+ }
99
+ }
100
+
101
+ export { Object3D };
src/core/Scene.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Object3D } from "./Object3D";
2
+ import { SplatData } from "../splats/SplatData";
3
+ import { Splat } from "../splats/Splat";
4
+ import { EventDispatcher } from "../events/EventDispatcher";
5
+ import { ObjectAddedEvent, ObjectRemovedEvent } from "../events/Events";
6
+ import { Converter } from "../utils/Converter";
7
+
8
+ class Scene extends EventDispatcher {
9
+ private _objects: Object3D[] = [];
10
+
11
+ addObject: (object: Object3D) => void;
12
+ removeObject: (object: Object3D) => void;
13
+ findObject: (predicate: (object: Object3D) => boolean) => Object3D | undefined;
14
+ findObjectOfType: <T extends Object3D>(type: { new (): T }) => T | undefined;
15
+ reset: () => void;
16
+
17
+ constructor() {
18
+ super();
19
+
20
+ this.addObject = (object: Object3D) => {
21
+ this.objects.push(object);
22
+ this.dispatchEvent(new ObjectAddedEvent(object));
23
+ };
24
+
25
+ this.removeObject = (object: Object3D) => {
26
+ const index = this.objects.indexOf(object);
27
+ if (index < 0) {
28
+ throw new Error("Object not found in scene");
29
+ }
30
+ this.objects.splice(index, 1);
31
+ this.dispatchEvent(new ObjectRemovedEvent(object));
32
+ };
33
+
34
+ this.findObject = (predicate: (object: Object3D) => boolean) => {
35
+ for (const object of this.objects) {
36
+ if (predicate(object)) {
37
+ return object;
38
+ }
39
+ }
40
+ return undefined;
41
+ };
42
+
43
+ this.findObjectOfType = <T extends Object3D>(type: { new (): T }) => {
44
+ for (const object of this.objects) {
45
+ if (object instanceof type) {
46
+ return object;
47
+ }
48
+ }
49
+ return undefined;
50
+ };
51
+
52
+ this.reset = () => {
53
+ const objectsToRemove = this.objects.slice();
54
+ for (const object of objectsToRemove) {
55
+ this.removeObject(object);
56
+ }
57
+ };
58
+
59
+ this.reset();
60
+ }
61
+
62
+ getMergedSceneDataBuffer(format: "splat" | "ply" = "splat"): ArrayBuffer {
63
+ const buffers: Uint8Array[] = [];
64
+ let vertexCount = 0;
65
+
66
+ for (const object of this.objects) {
67
+ if (object instanceof Splat) {
68
+ const splatClone = object.clone() as Splat;
69
+
70
+ splatClone.applyRotation();
71
+ splatClone.applyScale();
72
+ splatClone.applyPosition();
73
+ const buffer = splatClone.data.serialize();
74
+
75
+ buffers.push(buffer);
76
+ vertexCount += splatClone.data.vertexCount;
77
+ }
78
+ }
79
+
80
+ const mergedSplatData = new Uint8Array(vertexCount * SplatData.RowLength);
81
+ let offset = 0;
82
+ for (const buffer of buffers) {
83
+ mergedSplatData.set(buffer, offset);
84
+ offset += buffer.length;
85
+ }
86
+
87
+ if (format === "ply") {
88
+ return Converter.SplatToPLY(mergedSplatData.buffer, vertexCount);
89
+ }
90
+
91
+ return mergedSplatData.buffer;
92
+ }
93
+
94
+ saveToFile(name: string | null = null, format: "splat" | "ply" = "splat") {
95
+ if (!document) return;
96
+
97
+ if (!name) {
98
+ const now = new Date();
99
+ name = `scene-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.${format}`;
100
+ }
101
+
102
+ const mergedData = this.getMergedSceneDataBuffer(format);
103
+
104
+ const blob = new Blob([mergedData], { type: "application/octet-stream" });
105
+
106
+ const link = document.createElement("a");
107
+ link.download = name;
108
+ link.href = URL.createObjectURL(blob);
109
+ link.click();
110
+ }
111
+
112
+ get objects() {
113
+ return this._objects;
114
+ }
115
+ }
116
+
117
+ export { Scene };
src/custom.d.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ declare module "web-worker:*" {
2
+ const WorkerConstructor: {
3
+ new (): Worker;
4
+ };
5
+ export default WorkerConstructor;
6
+ }
src/events/EventDispatcher.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class EventDispatcher {
2
+ addEventListener: (type: string, listener: (event: Event) => void) => void;
3
+ removeEventListener: (type: string, listener: (event: Event) => void) => void;
4
+ hasEventListener: (type: string, listener: (event: Event) => void) => boolean;
5
+ dispatchEvent: (event: Event) => void;
6
+
7
+ constructor() {
8
+ const listeners = new Map<string, Set<(event: Event) => void>>();
9
+
10
+ this.addEventListener = (type: string, listener: (event: Event) => void) => {
11
+ if (!listeners.has(type)) {
12
+ listeners.set(type, new Set());
13
+ }
14
+
15
+ listeners.get(type)!.add(listener);
16
+ };
17
+
18
+ this.removeEventListener = (type: string, listener: (event: Event) => void) => {
19
+ if (!listeners.has(type)) {
20
+ return;
21
+ }
22
+
23
+ listeners.get(type)!.delete(listener);
24
+ };
25
+
26
+ this.hasEventListener = (type: string, listener: (event: Event) => void) => {
27
+ if (!listeners.has(type)) {
28
+ return false;
29
+ }
30
+
31
+ return listeners.get(type)!.has(listener);
32
+ };
33
+
34
+ this.dispatchEvent = (event: Event) => {
35
+ if (!listeners.has(event.type)) {
36
+ return;
37
+ }
38
+
39
+ for (const listener of listeners.get(event.type)!) {
40
+ listener(event);
41
+ }
42
+ };
43
+ }
44
+ }
45
+
46
+ export { EventDispatcher };
src/events/Events.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Object3D } from "../core/Object3D";
2
+
3
+ class ObjectAddedEvent extends Event {
4
+ constructor(public object: Object3D) {
5
+ super("objectAdded");
6
+ }
7
+ }
8
+
9
+ class ObjectRemovedEvent extends Event {
10
+ constructor(public object: Object3D) {
11
+ super("objectRemoved");
12
+ }
13
+ }
14
+
15
+ class ObjectChangedEvent extends Event {
16
+ constructor(public object: Object3D) {
17
+ super("objectChanged");
18
+ }
19
+ }
20
+
21
+ export { ObjectAddedEvent, ObjectRemovedEvent, ObjectChangedEvent };
src/index.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export { Object3D } from "./core/Object3D";
2
+ export { SplatData } from "./splats/SplatData";
3
+ export { SplatvData } from "./splats/SplatvData";
4
+ export { Splat } from "./splats/Splat";
5
+ export { Splatv } from "./splats/Splatv";
6
+ export { CameraData } from "./cameras/CameraData";
7
+ export { Camera } from "./cameras/Camera";
8
+ export { Scene } from "./core/Scene";
9
+ export { Loader } from "./loaders/Loader";
10
+ export { PLYLoader } from "./loaders/PLYLoader";
11
+ export { SplatvLoader } from "./loaders/SplatvLoader";
12
+ export { WebGLRenderer } from "./renderers/WebGLRenderer";
13
+ export { OrbitControls } from "./controls/OrbitControls";
14
+ export { FPSControls } from "./controls/FPSControls";
15
+ export { Quaternion } from "./math/Quaternion";
16
+ export { Vector3 } from "./math/Vector3";
17
+ export { Vector4 } from "./math/Vector4";
18
+ export { Matrix4 } from "./math/Matrix4";
19
+ export { Matrix3 } from "./math/Matrix3";
20
+ export { Color32 } from "./math/Color32";
21
+ export { Plane } from "./math/Plane";
22
+ export { ShaderPass } from "./renderers/webgl/passes/ShaderPass";
23
+ export { FadeInPass } from "./renderers/webgl/passes/FadeInPass";
24
+ export { RenderData } from "./renderers/webgl/utils/RenderData";
25
+ export { ShaderProgram } from "./renderers/webgl/programs/ShaderProgram";
26
+ export { RenderProgram } from "./renderers/webgl/programs/RenderProgram";
27
+ export { VideoRenderProgram } from "./renderers/webgl/programs/VideoRenderProgram";
28
+ export { IntersectionTester } from "./renderers/webgl/utils/IntersectionTester";
src/loaders/Loader.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Scene } from "../core/Scene";
2
+ import { Splat } from "../splats/Splat";
3
+ import { SplatData } from "../splats/SplatData";
4
+ import { initiateFetchRequest, loadRequestDataIntoBuffer } from "../utils/LoaderUtils";
5
+
6
+ class Loader {
7
+ static async LoadAsync(
8
+ url: string,
9
+ scene: Scene,
10
+ onProgress?: (progress: number) => void,
11
+ useCache: boolean = false,
12
+ ): Promise<Splat> {
13
+ const res: Response = await initiateFetchRequest(url, useCache);
14
+
15
+ const buffer = await loadRequestDataIntoBuffer(res, onProgress);
16
+ return this.LoadFromArrayBuffer(buffer, scene);
17
+ }
18
+
19
+ static async LoadFromFileAsync(file: File, scene: Scene, onProgress?: (progress: number) => void): Promise<Splat> {
20
+ const reader = new FileReader();
21
+ let splat = new Splat();
22
+ reader.onload = (e) => {
23
+ splat = this.LoadFromArrayBuffer(e.target!.result as ArrayBuffer, scene);
24
+ };
25
+ reader.onprogress = (e) => {
26
+ onProgress?.(e.loaded / e.total);
27
+ };
28
+ reader.readAsArrayBuffer(file);
29
+ await new Promise<void>((resolve) => {
30
+ reader.onloadend = () => {
31
+ resolve();
32
+ };
33
+ });
34
+ return splat;
35
+ }
36
+
37
+ static LoadFromArrayBuffer(arrayBuffer: ArrayBufferLike, scene: Scene): Splat {
38
+ const buffer = new Uint8Array(arrayBuffer);
39
+ const data = SplatData.Deserialize(buffer);
40
+ const splat = new Splat(data);
41
+ scene.addObject(splat);
42
+ return splat;
43
+ }
44
+ }
45
+
46
+ export { Loader };
src/loaders/PLYLoader.ts ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Scene } from "../core/Scene";
2
+ import { Vector3 } from "../math/Vector3";
3
+ import { Quaternion } from "../math/Quaternion";
4
+ import { SplatData } from "../splats/SplatData";
5
+ import { Splat } from "../splats/Splat";
6
+ import { Converter } from "../utils/Converter";
7
+ import { initiateFetchRequest, loadRequestDataIntoBuffer } from "../utils/LoaderUtils";
8
+
9
+ class PLYLoader {
10
+ static async LoadAsync(
11
+ url: string,
12
+ scene: Scene,
13
+ onProgress?: (progress: number) => void,
14
+ format: string = "",
15
+ useCache: boolean = false,
16
+ ): Promise<Splat> {
17
+ const res: Response = await initiateFetchRequest(url, useCache);
18
+
19
+ const plyData = await loadRequestDataIntoBuffer(res, onProgress);
20
+
21
+ if (plyData[0] !== 112 || plyData[1] !== 108 || plyData[2] !== 121 || plyData[3] !== 10) {
22
+ throw new Error("Invalid PLY file");
23
+ }
24
+
25
+ return this.LoadFromArrayBuffer(plyData.buffer, scene, format);
26
+ }
27
+
28
+ static async LoadFromFileAsync(
29
+ file: File,
30
+ scene: Scene,
31
+ onProgress?: (progress: number) => void,
32
+ format: string = "",
33
+ ): Promise<Splat> {
34
+ const reader = new FileReader();
35
+ let splat = new Splat();
36
+ reader.onload = (e) => {
37
+ splat = this.LoadFromArrayBuffer(e.target!.result as ArrayBuffer, scene, format);
38
+ };
39
+ reader.onprogress = (e) => {
40
+ onProgress?.(e.loaded / e.total);
41
+ };
42
+ reader.readAsArrayBuffer(file);
43
+ await new Promise<void>((resolve) => {
44
+ reader.onloadend = () => {
45
+ resolve();
46
+ };
47
+ });
48
+ return splat;
49
+ }
50
+
51
+ static LoadFromArrayBuffer(arrayBuffer: ArrayBufferLike, scene: Scene, format: string = ""): Splat {
52
+ const buffer = new Uint8Array(this._ParsePLYBuffer(arrayBuffer, format));
53
+ const data = SplatData.Deserialize(buffer);
54
+ const splat = new Splat(data);
55
+ scene.addObject(splat);
56
+ return splat;
57
+ }
58
+
59
+ private static _ParsePLYBuffer(inputBuffer: ArrayBuffer, format: string): ArrayBuffer {
60
+ type PlyProperty = {
61
+ name: string;
62
+ type: string;
63
+ offset: number;
64
+ };
65
+
66
+ const ubuf = new Uint8Array(inputBuffer);
67
+ const headerText = new TextDecoder().decode(ubuf.slice(0, 1024 * 10));
68
+ const header_end = "end_header\n";
69
+ const header_end_index = headerText.indexOf(header_end);
70
+ if (header_end_index < 0) throw new Error("Unable to read .ply file header");
71
+
72
+ const vertexCount = parseInt(/element vertex (\d+)\n/.exec(headerText)![1]);
73
+
74
+ let rowOffset = 0;
75
+ const offsets: Record<string, number> = {
76
+ double: 8,
77
+ int: 4,
78
+ uint: 4,
79
+ float: 4,
80
+ short: 2,
81
+ ushort: 2,
82
+ uchar: 1,
83
+ };
84
+
85
+ const properties: PlyProperty[] = [];
86
+ for (const prop of headerText
87
+ .slice(0, header_end_index)
88
+ .split("\n")
89
+ .filter((k) => k.startsWith("property "))) {
90
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
91
+ const [_p, type, name] = prop.split(" ");
92
+ properties.push({ name, type, offset: rowOffset });
93
+
94
+ if (!offsets[type]) throw new Error(`Unsupported property type: ${type}`);
95
+ rowOffset += offsets[type];
96
+ }
97
+
98
+ const dataView = new DataView(inputBuffer, header_end_index + header_end.length);
99
+ const buffer = new ArrayBuffer(SplatData.RowLength * vertexCount);
100
+
101
+ const q_polycam = Quaternion.FromEuler(new Vector3(Math.PI / 2, 0, 0));
102
+
103
+ for (let i = 0; i < vertexCount; i++) {
104
+ const position = new Float32Array(buffer, i * SplatData.RowLength, 3);
105
+ const scale = new Float32Array(buffer, i * SplatData.RowLength + 12, 3);
106
+ const rgba = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 24, 4);
107
+ const rot = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 28, 4);
108
+
109
+ let r0: number = 255;
110
+ let r1: number = 0;
111
+ let r2: number = 0;
112
+ let r3: number = 0;
113
+
114
+ properties.forEach((property) => {
115
+ let value;
116
+ switch (property.type) {
117
+ case "float":
118
+ value = dataView.getFloat32(property.offset + i * rowOffset, true);
119
+ break;
120
+ case "int":
121
+ value = dataView.getInt32(property.offset + i * rowOffset, true);
122
+ break;
123
+ default:
124
+ throw new Error(`Unsupported property type: ${property.type}`);
125
+ }
126
+
127
+ switch (property.name) {
128
+ case "x":
129
+ position[0] = value;
130
+ break;
131
+ case "y":
132
+ position[1] = value;
133
+ break;
134
+ case "z":
135
+ position[2] = value;
136
+ break;
137
+ case "scale_0":
138
+ case "scaling_0":
139
+ scale[0] = Math.exp(value);
140
+ break;
141
+ case "scale_1":
142
+ case "scaling_1":
143
+ scale[1] = Math.exp(value);
144
+ break;
145
+ case "scale_2":
146
+ case "scaling_2":
147
+ scale[2] = Math.exp(value);
148
+ break;
149
+ case "red":
150
+ rgba[0] = value;
151
+ break;
152
+ case "green":
153
+ rgba[1] = value;
154
+ break;
155
+ case "blue":
156
+ rgba[2] = value;
157
+ break;
158
+ case "f_dc_0":
159
+ case "features_0":
160
+ rgba[0] = (0.5 + Converter.SH_C0 * value) * 255;
161
+ break;
162
+ case "f_dc_1":
163
+ case "features_1":
164
+ rgba[1] = (0.5 + Converter.SH_C0 * value) * 255;
165
+ break;
166
+ case "f_dc_2":
167
+ case "features_2":
168
+ rgba[2] = (0.5 + Converter.SH_C0 * value) * 255;
169
+ break;
170
+ case "f_dc_3":
171
+ rgba[3] = (0.5 + Converter.SH_C0 * value) * 255;
172
+ break;
173
+ case "opacity":
174
+ case "opacity_0":
175
+ rgba[3] = (1 / (1 + Math.exp(-value))) * 255;
176
+ break;
177
+ case "rot_0":
178
+ case "rotation_0":
179
+ r0 = value;
180
+ break;
181
+ case "rot_1":
182
+ case "rotation_1":
183
+ r1 = value;
184
+ break;
185
+ case "rot_2":
186
+ case "rotation_2":
187
+ r2 = value;
188
+ break;
189
+ case "rot_3":
190
+ case "rotation_3":
191
+ r3 = value;
192
+ break;
193
+ }
194
+ });
195
+
196
+ let q = new Quaternion(r1, r2, r3, r0);
197
+
198
+ switch (format) {
199
+ case "polycam": {
200
+ const temp = position[1];
201
+ position[1] = -position[2];
202
+ position[2] = temp;
203
+ q = q_polycam.multiply(q);
204
+ break;
205
+ }
206
+ case "":
207
+ break;
208
+ default:
209
+ throw new Error(`Unsupported format: ${format}`);
210
+ }
211
+
212
+ q = q.normalize();
213
+ rot[0] = q.w * 128 + 128;
214
+ rot[1] = q.x * 128 + 128;
215
+ rot[2] = q.y * 128 + 128;
216
+ rot[3] = q.z * 128 + 128;
217
+ }
218
+
219
+ return buffer;
220
+ }
221
+ }
222
+
223
+ export { PLYLoader };
src/loaders/SplatvLoader.ts ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Camera } from "../cameras/Camera";
2
+ import type { Scene } from "../core/Scene";
3
+ import { Matrix3 } from "../math/Matrix3";
4
+ import { Quaternion } from "../math/Quaternion";
5
+ import { Vector3 } from "../math/Vector3";
6
+ import { Splatv } from "../splats/Splatv";
7
+ import { SplatvData } from "../splats/SplatvData";
8
+ import { initiateFetchRequest, loadRequestDataIntoBuffer } from "../utils/LoaderUtils";
9
+
10
+ class SplatvLoader {
11
+ static async LoadAsync(
12
+ url: string,
13
+ scene: Scene,
14
+ camera: Camera | null,
15
+ onProgress?: (progress: number) => void,
16
+ useCache: boolean = false,
17
+ ): Promise<Splatv> {
18
+ const res: Response = await initiateFetchRequest(url, useCache);
19
+
20
+ const buffer = await loadRequestDataIntoBuffer(res, onProgress);
21
+ return this._ParseSplatvBuffer(buffer.buffer, scene, camera);
22
+ }
23
+
24
+ static async LoadFromFileAsync(
25
+ file: File,
26
+ scene: Scene,
27
+ camera: Camera | null,
28
+ onProgress?: (progress: number) => void,
29
+ ): Promise<Splatv> {
30
+ const reader = new FileReader();
31
+ let splatv: Splatv | null = null;
32
+ reader.onload = (e) => {
33
+ splatv = this._ParseSplatvBuffer(e.target!.result as ArrayBuffer, scene, camera);
34
+ };
35
+ reader.onprogress = (e) => {
36
+ onProgress?.(e.loaded / e.total);
37
+ };
38
+ reader.readAsArrayBuffer(file);
39
+ await new Promise<void>((resolve) => {
40
+ reader.onloadend = () => {
41
+ resolve();
42
+ };
43
+ });
44
+ if (!splatv) {
45
+ throw new Error("Failed to load splatv file");
46
+ }
47
+ return splatv;
48
+ }
49
+
50
+ private static _ParseSplatvBuffer(inputBuffer: ArrayBuffer, scene: Scene, camera: Camera | null): Splatv {
51
+ let result: Splatv | null = null;
52
+
53
+ const handleChunk = (
54
+ chunk: { size: number; type: string; texwidth: number; texheight: number },
55
+ buffer: Uint8Array,
56
+ chunks: { size: number; type: string }[],
57
+ ) => {
58
+ if (chunk.type === "magic") {
59
+ const intView = new Int32Array(buffer.buffer);
60
+ if (intView[0] !== 0x674b) {
61
+ throw new Error("Invalid splatv file");
62
+ }
63
+ chunks.push({ size: intView[1], type: "chunks" });
64
+ } else if (chunk.type === "chunks") {
65
+ const splatChunks = JSON.parse(new TextDecoder("utf-8").decode(buffer));
66
+ if (splatChunks.length == 0) {
67
+ throw new Error("Invalid splatv file");
68
+ } else if (splatChunks.length > 1) {
69
+ console.warn("Splatv file contains more than one chunk, only the first one will be loaded");
70
+ }
71
+ const chunk = splatChunks[0];
72
+ const cameras = chunk.cameras as { position: number[]; rotation: number[][] }[];
73
+ if (camera && cameras && cameras.length) {
74
+ const cameraData = cameras[0];
75
+ const position = new Vector3(
76
+ cameraData.position[0],
77
+ cameraData.position[1],
78
+ cameraData.position[2],
79
+ );
80
+ const rotation = Quaternion.FromMatrix3(
81
+ new Matrix3(
82
+ cameraData.rotation[0][0],
83
+ cameraData.rotation[0][1],
84
+ cameraData.rotation[0][2],
85
+ cameraData.rotation[1][0],
86
+ cameraData.rotation[1][1],
87
+ cameraData.rotation[1][2],
88
+ cameraData.rotation[2][0],
89
+ cameraData.rotation[2][1],
90
+ cameraData.rotation[2][2],
91
+ ),
92
+ );
93
+ camera.position = position;
94
+ camera.rotation = rotation;
95
+ }
96
+ chunks.push(chunk);
97
+ } else if (chunk.type === "splat") {
98
+ const data = SplatvData.Deserialize(buffer, chunk.texwidth, chunk.texheight);
99
+ const splatv = new Splatv(data);
100
+ scene.addObject(splatv);
101
+ result = splatv;
102
+ }
103
+ };
104
+
105
+ const ubuf = new Uint8Array(inputBuffer);
106
+ const chunks: { size: number; type: string; texwidth: number; texheight: number }[] = [
107
+ { size: 8, type: "magic", texwidth: 0, texheight: 0 },
108
+ ];
109
+ let chunk: { size: number; type: string; texwidth: number; texheight: number } | undefined = chunks.shift();
110
+ let buffer = new Uint8Array(chunk!.size);
111
+ let offset = 0;
112
+ let inputOffset = 0;
113
+ while (chunk) {
114
+ while (offset < chunk.size) {
115
+ const sizeToRead = Math.min(chunk.size - offset, ubuf.length - inputOffset);
116
+ buffer.set(ubuf.subarray(inputOffset, inputOffset + sizeToRead), offset);
117
+ offset += sizeToRead;
118
+ inputOffset += sizeToRead;
119
+ }
120
+ handleChunk(chunk, buffer, chunks);
121
+ if (result) {
122
+ return result;
123
+ }
124
+ chunk = chunks.shift();
125
+ if (chunk) {
126
+ buffer = new Uint8Array(chunk.size);
127
+ offset = 0;
128
+ }
129
+ }
130
+
131
+ throw new Error("Invalid splatv file");
132
+ }
133
+ }
134
+
135
+ export { SplatvLoader };
src/math/BVH.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box3 } from "./Box3";
2
+
3
+ class BVHNode {
4
+ public left: BVHNode | null = null;
5
+ public right: BVHNode | null = null;
6
+ public pointIndices: number[] = [];
7
+
8
+ constructor(
9
+ public bounds: Box3,
10
+ public boxes: Box3[],
11
+ pointIndices: number[],
12
+ ) {
13
+ if (pointIndices.length > 1) {
14
+ this.split(bounds, boxes, pointIndices);
15
+ } else if (pointIndices.length > 0) {
16
+ this.pointIndices = pointIndices;
17
+ }
18
+ }
19
+
20
+ private split(bounds: Box3, boxes: Box3[], pointIndices: number[]) {
21
+ const axis = bounds.size().maxComponent();
22
+ pointIndices.sort((a, b) => boxes[a].center().getComponent(axis) - boxes[b].center().getComponent(axis));
23
+
24
+ const mid = Math.floor(pointIndices.length / 2);
25
+ const leftIndices = pointIndices.slice(0, mid);
26
+ const rightIndices = pointIndices.slice(mid);
27
+
28
+ this.left = new BVHNode(bounds, boxes, leftIndices);
29
+ this.right = new BVHNode(bounds, boxes, rightIndices);
30
+ }
31
+
32
+ public queryRange(range: Box3): number[] {
33
+ if (!this.bounds.intersects(range)) {
34
+ return [];
35
+ } else if (this.left !== null && this.right !== null) {
36
+ return this.left.queryRange(range).concat(this.right.queryRange(range));
37
+ } else {
38
+ return this.pointIndices.filter((index) => range.intersects(this.boxes[index]));
39
+ }
40
+ }
41
+ }
42
+
43
+ class BVH {
44
+ public root: BVHNode;
45
+
46
+ constructor(bounds: Box3, boxes: Box3[]) {
47
+ const pointIndices = boxes.map((_, index) => index);
48
+ this.root = new BVHNode(bounds, boxes, pointIndices);
49
+ }
50
+
51
+ public queryRange(range: Box3) {
52
+ return this.root.queryRange(range);
53
+ }
54
+ }
55
+
56
+ export { BVH };
src/math/Box3.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Vector3 } from "./Vector3";
2
+
3
+ class Box3 {
4
+ constructor(
5
+ public min: Vector3,
6
+ public max: Vector3,
7
+ ) {}
8
+
9
+ public contains(point: Vector3) {
10
+ return (
11
+ point.x >= this.min.x &&
12
+ point.x <= this.max.x &&
13
+ point.y >= this.min.y &&
14
+ point.y <= this.max.y &&
15
+ point.z >= this.min.z &&
16
+ point.z <= this.max.z
17
+ );
18
+ }
19
+
20
+ public intersects(box: Box3) {
21
+ return (
22
+ this.max.x >= box.min.x &&
23
+ this.min.x <= box.max.x &&
24
+ this.max.y >= box.min.y &&
25
+ this.min.y <= box.max.y &&
26
+ this.max.z >= box.min.z &&
27
+ this.min.z <= box.max.z
28
+ );
29
+ }
30
+
31
+ public size() {
32
+ return this.max.subtract(this.min);
33
+ }
34
+
35
+ public center() {
36
+ return this.min.add(this.max).divide(2);
37
+ }
38
+
39
+ public expand(point: Vector3) {
40
+ this.min = this.min.min(point);
41
+ this.max = this.max.max(point);
42
+ }
43
+
44
+ public permute() {
45
+ const min = this.min;
46
+ const max = this.max;
47
+ this.min = new Vector3(Math.min(min.x, max.x), Math.min(min.y, max.y), Math.min(min.z, max.z));
48
+ this.max = new Vector3(Math.max(min.x, max.x), Math.max(min.y, max.y), Math.max(min.z, max.z));
49
+ }
50
+ }
51
+
52
+ export { Box3 };
src/math/Color32.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Color32 {
2
+ public readonly r: number;
3
+ public readonly g: number;
4
+ public readonly b: number;
5
+ public readonly a: number;
6
+
7
+ constructor(r: number = 0, g: number = 0, b: number = 0, a: number = 255) {
8
+ this.r = r;
9
+ this.g = g;
10
+ this.b = b;
11
+ this.a = a;
12
+ }
13
+
14
+ flat(): number[] {
15
+ return [this.r, this.g, this.b, this.a];
16
+ }
17
+
18
+ flatNorm(): number[] {
19
+ return [this.r / 255, this.g / 255, this.b / 255, this.a / 255];
20
+ }
21
+
22
+ toHexString(): string {
23
+ return (
24
+ "#" +
25
+ this.flat()
26
+ .map((x) => x.toString(16).padStart(2, "0"))
27
+ .join("")
28
+ );
29
+ }
30
+
31
+ toString(): string {
32
+ return `[${this.flat().join(", ")}]`;
33
+ }
34
+ }
35
+
36
+ export { Color32 };
src/math/Matrix3.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Quaternion } from "./Quaternion";
2
+ import type { Vector3 } from "./Vector3";
3
+
4
+ class Matrix3 {
5
+ public readonly buffer: number[];
6
+
7
+ // prettier-ignore
8
+ constructor(n11: number = 1, n12: number = 0, n13: number = 0,
9
+ n21: number = 0, n22: number = 1, n23: number = 0,
10
+ n31: number = 0, n32: number = 0, n33: number = 1) {
11
+ this.buffer = [
12
+ n11, n12, n13,
13
+ n21, n22, n23,
14
+ n31, n32, n33
15
+ ];
16
+ }
17
+
18
+ equals(m: Matrix3): boolean {
19
+ if (this.buffer.length !== m.buffer.length) {
20
+ return false;
21
+ }
22
+ if (this.buffer === m.buffer) {
23
+ return true;
24
+ }
25
+ for (let i = 0; i < this.buffer.length; i++) {
26
+ if (this.buffer[i] !== m.buffer[i]) {
27
+ return false;
28
+ }
29
+ }
30
+ return true;
31
+ }
32
+
33
+ multiply(v: Matrix3): Matrix3 {
34
+ const a = this.buffer;
35
+ const b = v.buffer;
36
+ return new Matrix3(
37
+ b[0] * a[0] + b[3] * a[1] + b[6] * a[2],
38
+ b[1] * a[0] + b[4] * a[1] + b[7] * a[2],
39
+ b[2] * a[0] + b[5] * a[1] + b[8] * a[2],
40
+ b[0] * a[3] + b[3] * a[4] + b[6] * a[5],
41
+ b[1] * a[3] + b[4] * a[4] + b[7] * a[5],
42
+ b[2] * a[3] + b[5] * a[4] + b[8] * a[5],
43
+ b[0] * a[6] + b[3] * a[7] + b[6] * a[8],
44
+ b[1] * a[6] + b[4] * a[7] + b[7] * a[8],
45
+ b[2] * a[6] + b[5] * a[7] + b[8] * a[8],
46
+ );
47
+ }
48
+
49
+ clone(): Matrix3 {
50
+ const e = this.buffer;
51
+ // prettier-ignore
52
+ return new Matrix3(
53
+ e[0], e[1], e[2],
54
+ e[3], e[4], e[5],
55
+ e[6], e[7], e[8]
56
+ );
57
+ }
58
+
59
+ static Eye(v: number = 1): Matrix3 {
60
+ return new Matrix3(v, 0, 0, 0, v, 0, 0, 0, v);
61
+ }
62
+
63
+ static Diagonal(v: Vector3): Matrix3 {
64
+ return new Matrix3(v.x, 0, 0, 0, v.y, 0, 0, 0, v.z);
65
+ }
66
+
67
+ static RotationFromQuaternion(q: Quaternion): Matrix3 {
68
+ const matrix = new Matrix3(
69
+ 1 - 2 * q.y * q.y - 2 * q.z * q.z,
70
+ 2 * q.x * q.y - 2 * q.z * q.w,
71
+ 2 * q.x * q.z + 2 * q.y * q.w,
72
+ 2 * q.x * q.y + 2 * q.z * q.w,
73
+ 1 - 2 * q.x * q.x - 2 * q.z * q.z,
74
+ 2 * q.y * q.z - 2 * q.x * q.w,
75
+ 2 * q.x * q.z - 2 * q.y * q.w,
76
+ 2 * q.y * q.z + 2 * q.x * q.w,
77
+ 1 - 2 * q.x * q.x - 2 * q.y * q.y,
78
+ );
79
+ return matrix;
80
+ }
81
+
82
+ static RotationFromEuler(m: Vector3): Matrix3 {
83
+ const cx = Math.cos(m.x);
84
+ const sx = Math.sin(m.x);
85
+ const cy = Math.cos(m.y);
86
+ const sy = Math.sin(m.y);
87
+ const cz = Math.cos(m.z);
88
+ const sz = Math.sin(m.z);
89
+
90
+ const rotationMatrix = [
91
+ cy * cz + sy * sx * sz,
92
+ -cy * sz + sy * sx * cz,
93
+ sy * cx,
94
+ cx * sz,
95
+ cx * cz,
96
+ -sx,
97
+ -sy * cz + cy * sx * sz,
98
+ sy * sz + cy * sx * cz,
99
+ cy * cx,
100
+ ];
101
+
102
+ return new Matrix3(...rotationMatrix);
103
+ }
104
+
105
+ toString(): string {
106
+ return `[${this.buffer.join(", ")}]`;
107
+ }
108
+ }
109
+
110
+ export { Matrix3 };
src/math/Matrix4.ts ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Quaternion } from "./Quaternion";
2
+ import { Vector3 } from "./Vector3";
3
+
4
+ class Matrix4 {
5
+ public readonly buffer: number[];
6
+
7
+ // prettier-ignore
8
+ constructor(n11: number = 1, n12: number = 0, n13: number = 0, n14: number = 0,
9
+ n21: number = 0, n22: number = 1, n23: number = 0, n24: number = 0,
10
+ n31: number = 0, n32: number = 0, n33: number = 1, n34: number = 0,
11
+ n41: number = 0, n42: number = 0, n43: number = 0, n44: number = 1) {
12
+ this.buffer = [
13
+ n11, n12, n13, n14,
14
+ n21, n22, n23, n24,
15
+ n31, n32, n33, n34,
16
+ n41, n42, n43, n44
17
+ ];
18
+ }
19
+
20
+ equals(m: Matrix4): boolean {
21
+ if (this.buffer.length !== m.buffer.length) {
22
+ return false;
23
+ }
24
+ if (this.buffer === m.buffer) {
25
+ return true;
26
+ }
27
+ for (let i = 0; i < this.buffer.length; i++) {
28
+ if (this.buffer[i] !== m.buffer[i]) {
29
+ return false;
30
+ }
31
+ }
32
+ return true;
33
+ }
34
+
35
+ multiply(m: Matrix4): Matrix4 {
36
+ const a = this.buffer;
37
+ const b = m.buffer;
38
+ return new Matrix4(
39
+ b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12],
40
+ b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13],
41
+ b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14],
42
+ b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15],
43
+ b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12],
44
+ b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13],
45
+ b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14],
46
+ b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15],
47
+ b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12],
48
+ b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13],
49
+ b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14],
50
+ b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15],
51
+ b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12],
52
+ b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13],
53
+ b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14],
54
+ b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15],
55
+ );
56
+ }
57
+
58
+ clone(): Matrix4 {
59
+ const e = this.buffer;
60
+ // prettier-ignore
61
+ return new Matrix4(
62
+ e[0], e[1], e[2], e[3],
63
+ e[4], e[5], e[6], e[7],
64
+ e[8], e[9], e[10], e[11],
65
+ e[12], e[13], e[14], e[15]
66
+ );
67
+ }
68
+
69
+ determinant(): number {
70
+ const e = this.buffer;
71
+ // prettier-ignore
72
+ return (
73
+ e[12] * e[9] * e[6] * e[3] - e[8] * e[13] * e[6] * e[3] - e[12] * e[5] * e[10] * e[3] + e[4] * e[13] * e[10] * e[3] +
74
+ e[8] * e[5] * e[14] * e[3] - e[4] * e[9] * e[14] * e[3] - e[12] * e[9] * e[2] * e[7] + e[8] * e[13] * e[2] * e[7] +
75
+ e[12] * e[1] * e[10] * e[7] - e[0] * e[13] * e[10] * e[7] - e[8] * e[1] * e[14] * e[7] + e[0] * e[9] * e[14] * e[7] +
76
+ e[12] * e[5] * e[2] * e[11] - e[4] * e[13] * e[2] * e[11] - e[12] * e[1] * e[6] * e[11] + e[0] * e[13] * e[6] * e[11] +
77
+ e[4] * e[1] * e[14] * e[11] - e[0] * e[5] * e[14] * e[11] - e[8] * e[5] * e[2] * e[15] + e[4] * e[9] * e[2] * e[15] +
78
+ e[8] * e[1] * e[6] * e[15] - e[0] * e[9] * e[6] * e[15] - e[4] * e[1] * e[10] * e[15] + e[0] * e[5] * e[10] * e[15]
79
+ );
80
+ }
81
+
82
+ invert(): Matrix4 {
83
+ const e = this.buffer;
84
+ const det = this.determinant();
85
+ if (det === 0) {
86
+ throw new Error("Matrix is not invertible.");
87
+ }
88
+ const invDet = 1 / det;
89
+ // prettier-ignore
90
+ return new Matrix4(
91
+ invDet * (
92
+ e[5] * e[10] * e[15] - e[5] * e[11] * e[14] - e[9] * e[6] * e[15] + e[9] * e[7] * e[14] + e[13] * e[6] * e[11] - e[13] * e[7] * e[10]
93
+ ),
94
+ invDet * (
95
+ -e[1] * e[10] * e[15] + e[1] * e[11] * e[14] + e[9] * e[2] * e[15] - e[9] * e[3] * e[14] - e[13] * e[2] * e[11] + e[13] * e[3] * e[10]
96
+ ),
97
+ invDet * (
98
+ e[1] * e[6] * e[15] - e[1] * e[7] * e[14] - e[5] * e[2] * e[15] + e[5] * e[3] * e[14] + e[13] * e[2] * e[7] - e[13] * e[3] * e[6]
99
+ ),
100
+ invDet * (
101
+ -e[1] * e[6] * e[11] + e[1] * e[7] * e[10] + e[5] * e[2] * e[11] - e[5] * e[3] * e[10] - e[9] * e[2] * e[7] + e[9] * e[3] * e[6]
102
+ ),
103
+ invDet * (
104
+ -e[4] * e[10] * e[15] + e[4] * e[11] * e[14] + e[8] * e[6] * e[15] - e[8] * e[7] * e[14] - e[12] * e[6] * e[11] + e[12] * e[7] * e[10]
105
+ ),
106
+ invDet * (
107
+ e[0] * e[10] * e[15] - e[0] * e[11] * e[14] - e[8] * e[2] * e[15] + e[8] * e[3] * e[14] + e[12] * e[2] * e[11] - e[12] * e[3] * e[10]
108
+ ),
109
+ invDet * (
110
+ -e[0] * e[6] * e[15] + e[0] * e[7] * e[14] + e[4] * e[2] * e[15] - e[4] * e[3] * e[14] - e[12] * e[2] * e[7] + e[12] * e[3] * e[6]
111
+ ),
112
+ invDet * (
113
+ e[0] * e[6] * e[11] - e[0] * e[7] * e[10] - e[4] * e[2] * e[11] + e[4] * e[3] * e[10] + e[8] * e[2] * e[7] - e[8] * e[3] * e[6]
114
+ ),
115
+ invDet * (
116
+ e[4] * e[9] * e[15] - e[4] * e[11] * e[13] - e[8] * e[5] * e[15] + e[8] * e[7] * e[13] + e[12] * e[5] * e[11] - e[12] * e[7] * e[9]
117
+ ),
118
+ invDet * (
119
+ -e[0] * e[9] * e[15] + e[0] * e[11] * e[13] + e[8] * e[1] * e[15] - e[8] * e[3] * e[13] - e[12] * e[1] * e[11] + e[12] * e[3] * e[9]
120
+ ),
121
+ invDet * (
122
+ e[0] * e[5] * e[15] - e[0] * e[7] * e[13] - e[4] * e[1] * e[15] + e[4] * e[3] * e[13] + e[12] * e[1] * e[7] - e[12] * e[3] * e[5]
123
+ ),
124
+ invDet * (
125
+ -e[0] * e[5] * e[11] + e[0] * e[7] * e[9] + e[4] * e[1] * e[11] - e[4] * e[3] * e[9] - e[8] * e[1] * e[7] + e[8] * e[3] * e[5]
126
+ ),
127
+ invDet * (
128
+ -e[4] * e[9] * e[14] + e[4] * e[10] * e[13] + e[8] * e[5] * e[14] - e[8] * e[6] * e[13] - e[12] * e[5] * e[10] + e[12] * e[6] * e[9]
129
+ ),
130
+ invDet * (
131
+ e[0] * e[9] * e[14] - e[0] * e[10] * e[13] - e[8] * e[1] * e[14] + e[8] * e[2] * e[13] + e[12] * e[1] * e[10] - e[12] * e[2] * e[9]
132
+ ),
133
+ invDet * (
134
+ -e[0] * e[5] * e[14] + e[0] * e[6] * e[13] + e[4] * e[1] * e[14] - e[4] * e[2] * e[13] - e[12] * e[1] * e[6] + e[12] * e[2] * e[5]
135
+ ),
136
+ invDet * (
137
+ e[0] * e[5] * e[10] - e[0] * e[6] * e[9] - e[4] * e[1] * e[10] + e[4] * e[2] * e[9] + e[8] * e[1] * e[6] - e[8] * e[2] * e[5]
138
+ ),
139
+ );
140
+ }
141
+
142
+ static Compose(position: Vector3, rotation: Quaternion, scale: Vector3): Matrix4 {
143
+ const x = rotation.x,
144
+ y = rotation.y,
145
+ z = rotation.z,
146
+ w = rotation.w;
147
+ const x2 = x + x,
148
+ y2 = y + y,
149
+ z2 = z + z;
150
+ const xx = x * x2,
151
+ xy = x * y2,
152
+ xz = x * z2;
153
+ const yy = y * y2,
154
+ yz = y * z2,
155
+ zz = z * z2;
156
+ const wx = w * x2,
157
+ wy = w * y2,
158
+ wz = w * z2;
159
+ const sx = scale.x,
160
+ sy = scale.y,
161
+ sz = scale.z;
162
+ // prettier-ignore
163
+ return new Matrix4(
164
+ (1 - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0,
165
+ (xy - wz) * sy, (1 - (xx + zz)) * sy, (yz + wx) * sy, 0,
166
+ (xz + wy) * sz, (yz - wx) * sz, (1 - (xx + yy)) * sz, 0,
167
+ position.x, position.y, position.z, 1
168
+ );
169
+ }
170
+
171
+ toString(): string {
172
+ return `[${this.buffer.join(", ")}]`;
173
+ }
174
+ }
175
+
176
+ export { Matrix4 };
src/math/Plane.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Vector3 } from "./Vector3";
2
+
3
+ class Plane {
4
+ public readonly normal: Vector3;
5
+ public readonly point: Vector3;
6
+
7
+ constructor(normal: Vector3, point: Vector3) {
8
+ this.normal = normal;
9
+ this.point = point;
10
+ }
11
+
12
+ intersect(origin: Vector3, direction: Vector3): Vector3 | null {
13
+ const denominator = this.normal.dot(direction);
14
+
15
+ if (Math.abs(denominator) < 0.0001) {
16
+ return null;
17
+ }
18
+
19
+ const t = this.normal.dot(this.point.subtract(origin)) / denominator;
20
+
21
+ if (t < 0) {
22
+ return null;
23
+ }
24
+
25
+ return origin.add(direction.multiply(t));
26
+ }
27
+ }
28
+
29
+ export { Plane };
src/math/Quaternion.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Matrix3 } from "./Matrix3";
2
+ import { Vector3 } from "./Vector3";
3
+
4
+ class Quaternion {
5
+ public readonly x: number;
6
+ public readonly y: number;
7
+ public readonly z: number;
8
+ public readonly w: number;
9
+
10
+ constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) {
11
+ this.x = x;
12
+ this.y = y;
13
+ this.z = z;
14
+ this.w = w;
15
+ }
16
+
17
+ equals(q: Quaternion): boolean {
18
+ if (this.x !== q.x) {
19
+ return false;
20
+ }
21
+ if (this.y !== q.y) {
22
+ return false;
23
+ }
24
+ if (this.z !== q.z) {
25
+ return false;
26
+ }
27
+ if (this.w !== q.w) {
28
+ return false;
29
+ }
30
+
31
+ return true;
32
+ }
33
+
34
+ normalize(): Quaternion {
35
+ const l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
36
+ return new Quaternion(this.x / l, this.y / l, this.z / l, this.w / l);
37
+ }
38
+
39
+ multiply(q: Quaternion): Quaternion {
40
+ const w1 = this.w,
41
+ x1 = this.x,
42
+ y1 = this.y,
43
+ z1 = this.z;
44
+ const w2 = q.w,
45
+ x2 = q.x,
46
+ y2 = q.y,
47
+ z2 = q.z;
48
+
49
+ return new Quaternion(
50
+ w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
51
+ w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
52
+ w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
53
+ w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
54
+ );
55
+ }
56
+
57
+ inverse(): Quaternion {
58
+ const l = this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;
59
+ return new Quaternion(-this.x / l, -this.y / l, -this.z / l, this.w / l);
60
+ }
61
+
62
+ apply(v: Vector3): Vector3 {
63
+ const vecQuat = new Quaternion(v.x, v.y, v.z, 0);
64
+ const conjugate = new Quaternion(-this.x, -this.y, -this.z, this.w);
65
+ const rotatedQuat = this.multiply(vecQuat).multiply(conjugate);
66
+ return new Vector3(rotatedQuat.x, rotatedQuat.y, rotatedQuat.z);
67
+ }
68
+
69
+ flat(): number[] {
70
+ return [this.x, this.y, this.z, this.w];
71
+ }
72
+
73
+ clone(): Quaternion {
74
+ return new Quaternion(this.x, this.y, this.z, this.w);
75
+ }
76
+
77
+ static FromEuler(e: Vector3): Quaternion {
78
+ const halfX = e.x / 2;
79
+ const halfY = e.y / 2;
80
+ const halfZ = e.z / 2;
81
+ const cy = Math.cos(halfY);
82
+ const sy = Math.sin(halfY);
83
+ const cp = Math.cos(halfX);
84
+ const sp = Math.sin(halfX);
85
+ const cz = Math.cos(halfZ);
86
+ const sz = Math.sin(halfZ);
87
+
88
+ const q = new Quaternion(
89
+ cy * sp * cz + sy * cp * sz,
90
+ sy * cp * cz - cy * sp * sz,
91
+ cy * cp * sz - sy * sp * cz,
92
+ cy * cp * cz + sy * sp * sz,
93
+ );
94
+ return q;
95
+ }
96
+
97
+ toEuler(): Vector3 {
98
+ const sinr_cosp = 2 * (this.w * this.x + this.y * this.z);
99
+ const cosr_cosp = 1 - 2 * (this.x * this.x + this.y * this.y);
100
+ const x = Math.atan2(sinr_cosp, cosr_cosp);
101
+
102
+ let y;
103
+ const sinp = 2 * (this.w * this.y - this.z * this.x);
104
+ if (Math.abs(sinp) >= 1) {
105
+ y = (Math.sign(sinp) * Math.PI) / 2;
106
+ } else {
107
+ y = Math.asin(sinp);
108
+ }
109
+
110
+ const siny_cosp = 2 * (this.w * this.z + this.x * this.y);
111
+ const cosy_cosp = 1 - 2 * (this.y * this.y + this.z * this.z);
112
+ const z = Math.atan2(siny_cosp, cosy_cosp);
113
+
114
+ return new Vector3(x, y, z);
115
+ }
116
+
117
+ static FromMatrix3(matrix: Matrix3): Quaternion {
118
+ const m = matrix.buffer;
119
+ const trace = m[0] + m[4] + m[8];
120
+ let x, y, z, w;
121
+ if (trace > 0) {
122
+ const s = 0.5 / Math.sqrt(trace + 1.0);
123
+ w = 0.25 / s;
124
+ x = (m[7] - m[5]) * s;
125
+ y = (m[2] - m[6]) * s;
126
+ z = (m[3] - m[1]) * s;
127
+ } else if (m[0] > m[4] && m[0] > m[8]) {
128
+ const s = 2.0 * Math.sqrt(1.0 + m[0] - m[4] - m[8]);
129
+ w = (m[7] - m[5]) / s;
130
+ x = 0.25 * s;
131
+ y = (m[1] + m[3]) / s;
132
+ z = (m[2] + m[6]) / s;
133
+ } else if (m[4] > m[8]) {
134
+ const s = 2.0 * Math.sqrt(1.0 + m[4] - m[0] - m[8]);
135
+ w = (m[2] - m[6]) / s;
136
+ x = (m[1] + m[3]) / s;
137
+ y = 0.25 * s;
138
+ z = (m[5] + m[7]) / s;
139
+ } else {
140
+ const s = 2.0 * Math.sqrt(1.0 + m[8] - m[0] - m[4]);
141
+ w = (m[3] - m[1]) / s;
142
+ x = (m[2] + m[6]) / s;
143
+ y = (m[5] + m[7]) / s;
144
+ z = 0.25 * s;
145
+ }
146
+ return new Quaternion(x, y, z, w);
147
+ }
148
+
149
+ static FromAxisAngle(axis: Vector3, angle: number): Quaternion {
150
+ const halfAngle = angle / 2;
151
+ const sin = Math.sin(halfAngle);
152
+ const cos = Math.cos(halfAngle);
153
+ return new Quaternion(axis.x * sin, axis.y * sin, axis.z * sin, cos);
154
+ }
155
+
156
+ static LookRotation(direction: Vector3): Quaternion {
157
+ const forward = new Vector3(0, 0, 1);
158
+ const dot = forward.dot(direction);
159
+
160
+ if (Math.abs(dot - -1.0) < 0.000001) {
161
+ return new Quaternion(0, 1, 0, Math.PI);
162
+ }
163
+ if (Math.abs(dot - 1.0) < 0.000001) {
164
+ return new Quaternion();
165
+ }
166
+
167
+ const rotAngle = Math.acos(dot);
168
+ const rotAxis = forward.cross(direction).normalize();
169
+ return Quaternion.FromAxisAngle(rotAxis, rotAngle);
170
+ }
171
+
172
+ toString(): string {
173
+ return `[${this.flat().join(", ")}]`;
174
+ }
175
+ }
176
+
177
+ export { Quaternion };
src/math/Vector3.ts ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Matrix4 } from "./Matrix4";
2
+
3
+ class Vector3 {
4
+ public readonly x: number;
5
+ public readonly y: number;
6
+ public readonly z: number;
7
+
8
+ constructor(x: number = 0, y: number = 0, z: number = 0) {
9
+ this.x = x;
10
+ this.y = y;
11
+ this.z = z;
12
+ }
13
+
14
+ equals(v: Vector3): boolean {
15
+ if (this.x !== v.x) {
16
+ return false;
17
+ }
18
+ if (this.y !== v.y) {
19
+ return false;
20
+ }
21
+ if (this.z !== v.z) {
22
+ return false;
23
+ }
24
+
25
+ return true;
26
+ }
27
+
28
+ add(v: number): Vector3;
29
+ add(v: Vector3): Vector3;
30
+ add(v: number | Vector3): Vector3 {
31
+ if (typeof v === "number") {
32
+ return new Vector3(this.x + v, this.y + v, this.z + v);
33
+ } else {
34
+ return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
35
+ }
36
+ }
37
+
38
+ subtract(v: number): Vector3;
39
+ subtract(v: Vector3): Vector3;
40
+ subtract(v: number | Vector3): Vector3 {
41
+ if (typeof v === "number") {
42
+ return new Vector3(this.x - v, this.y - v, this.z - v);
43
+ } else {
44
+ return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
45
+ }
46
+ }
47
+
48
+ multiply(v: number): Vector3;
49
+ multiply(v: Vector3): Vector3;
50
+ multiply(v: Matrix4): Vector3;
51
+ multiply(v: number | Vector3 | Matrix4): Vector3 {
52
+ if (typeof v === "number") {
53
+ return new Vector3(this.x * v, this.y * v, this.z * v);
54
+ } else if (v instanceof Vector3) {
55
+ return new Vector3(this.x * v.x, this.y * v.y, this.z * v.z);
56
+ } else {
57
+ return new Vector3(
58
+ this.x * v.buffer[0] + this.y * v.buffer[4] + this.z * v.buffer[8] + v.buffer[12],
59
+ this.x * v.buffer[1] + this.y * v.buffer[5] + this.z * v.buffer[9] + v.buffer[13],
60
+ this.x * v.buffer[2] + this.y * v.buffer[6] + this.z * v.buffer[10] + v.buffer[14],
61
+ );
62
+ }
63
+ }
64
+
65
+ divide(v: number): Vector3;
66
+ divide(v: Vector3): Vector3;
67
+ divide(v: number | Vector3): Vector3 {
68
+ if (typeof v === "number") {
69
+ return new Vector3(this.x / v, this.y / v, this.z / v);
70
+ } else {
71
+ return new Vector3(this.x / v.x, this.y / v.y, this.z / v.z);
72
+ }
73
+ }
74
+
75
+ cross(v: Vector3): Vector3 {
76
+ const x = this.y * v.z - this.z * v.y;
77
+ const y = this.z * v.x - this.x * v.z;
78
+ const z = this.x * v.y - this.y * v.x;
79
+
80
+ return new Vector3(x, y, z);
81
+ }
82
+
83
+ dot(v: Vector3): number {
84
+ return this.x * v.x + this.y * v.y + this.z * v.z;
85
+ }
86
+
87
+ lerp(v: Vector3, t: number): Vector3 {
88
+ return new Vector3(this.x + (v.x - this.x) * t, this.y + (v.y - this.y) * t, this.z + (v.z - this.z) * t);
89
+ }
90
+
91
+ min(v: Vector3): Vector3 {
92
+ return new Vector3(Math.min(this.x, v.x), Math.min(this.y, v.y), Math.min(this.z, v.z));
93
+ }
94
+
95
+ max(v: Vector3): Vector3 {
96
+ return new Vector3(Math.max(this.x, v.x), Math.max(this.y, v.y), Math.max(this.z, v.z));
97
+ }
98
+
99
+ getComponent(axis: number) {
100
+ switch (axis) {
101
+ case 0:
102
+ return this.x;
103
+ case 1:
104
+ return this.y;
105
+ case 2:
106
+ return this.z;
107
+ default:
108
+ throw new Error(`Invalid component index: ${axis}`);
109
+ }
110
+ }
111
+
112
+ minComponent(): number {
113
+ if (this.x < this.y && this.x < this.z) {
114
+ return 0;
115
+ } else if (this.y < this.z) {
116
+ return 1;
117
+ } else {
118
+ return 2;
119
+ }
120
+ }
121
+
122
+ maxComponent(): number {
123
+ if (this.x > this.y && this.x > this.z) {
124
+ return 0;
125
+ } else if (this.y > this.z) {
126
+ return 1;
127
+ } else {
128
+ return 2;
129
+ }
130
+ }
131
+
132
+ magnitude(): number {
133
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
134
+ }
135
+
136
+ distanceTo(v: Vector3): number {
137
+ return Math.sqrt((this.x - v.x) ** 2 + (this.y - v.y) ** 2 + (this.z - v.z) ** 2);
138
+ }
139
+
140
+ normalize(): Vector3 {
141
+ const length = this.magnitude();
142
+
143
+ return new Vector3(this.x / length, this.y / length, this.z / length);
144
+ }
145
+
146
+ flat(): number[] {
147
+ return [this.x, this.y, this.z];
148
+ }
149
+
150
+ clone(): Vector3 {
151
+ return new Vector3(this.x, this.y, this.z);
152
+ }
153
+
154
+ toString(): string {
155
+ return `[${this.flat().join(", ")}]`;
156
+ }
157
+
158
+ static One(value: number = 1): Vector3 {
159
+ return new Vector3(value, value, value);
160
+ }
161
+ }
162
+
163
+ export { Vector3 };
src/math/Vector4.ts ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Matrix4 } from "./Matrix4";
2
+
3
+ class Vector4 {
4
+ public readonly x: number;
5
+ public readonly y: number;
6
+ public readonly z: number;
7
+ public readonly w: number;
8
+
9
+ constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 0) {
10
+ this.x = x;
11
+ this.y = y;
12
+ this.z = z;
13
+ this.w = w;
14
+ }
15
+
16
+ equals(v: Vector4): boolean {
17
+ if (this.x !== v.x) {
18
+ return false;
19
+ }
20
+ if (this.y !== v.y) {
21
+ return false;
22
+ }
23
+ if (this.z !== v.z) {
24
+ return false;
25
+ }
26
+ if (this.w !== v.w) {
27
+ return false;
28
+ }
29
+
30
+ return true;
31
+ }
32
+
33
+ add(v: number): Vector4;
34
+ add(v: Vector4): Vector4;
35
+ add(v: number | Vector4): Vector4 {
36
+ if (typeof v === "number") {
37
+ return new Vector4(this.x + v, this.y + v, this.z + v, this.w + v);
38
+ } else {
39
+ return new Vector4(this.x + v.x, this.y + v.y, this.z + v.z, this.w + v.w);
40
+ }
41
+ }
42
+
43
+ subtract(v: number): Vector4;
44
+ subtract(v: Vector4): Vector4;
45
+ subtract(v: number | Vector4): Vector4 {
46
+ if (typeof v === "number") {
47
+ return new Vector4(this.x - v, this.y - v, this.z - v, this.w - v);
48
+ } else {
49
+ return new Vector4(this.x - v.x, this.y - v.y, this.z - v.z, this.w - v.w);
50
+ }
51
+ }
52
+
53
+ multiply(v: number): Vector4;
54
+ multiply(v: Vector4): Vector4;
55
+ multiply(v: Matrix4): Vector4;
56
+ multiply(v: number | Vector4 | Matrix4): Vector4 {
57
+ if (typeof v === "number") {
58
+ return new Vector4(this.x * v, this.y * v, this.z * v, this.w * v);
59
+ } else if (v instanceof Vector4) {
60
+ return new Vector4(this.x * v.x, this.y * v.y, this.z * v.z, this.w * v.w);
61
+ } else {
62
+ return new Vector4(
63
+ this.x * v.buffer[0] + this.y * v.buffer[4] + this.z * v.buffer[8] + this.w * v.buffer[12],
64
+ this.x * v.buffer[1] + this.y * v.buffer[5] + this.z * v.buffer[9] + this.w * v.buffer[13],
65
+ this.x * v.buffer[2] + this.y * v.buffer[6] + this.z * v.buffer[10] + this.w * v.buffer[14],
66
+ this.x * v.buffer[3] + this.y * v.buffer[7] + this.z * v.buffer[11] + this.w * v.buffer[15],
67
+ );
68
+ }
69
+ }
70
+
71
+ dot(v: Vector4): number {
72
+ return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;
73
+ }
74
+
75
+ lerp(v: Vector4, t: number): Vector4 {
76
+ return new Vector4(
77
+ this.x + (v.x - this.x) * t,
78
+ this.y + (v.y - this.y) * t,
79
+ this.z + (v.z - this.z) * t,
80
+ this.w + (v.w - this.w) * t,
81
+ );
82
+ }
83
+
84
+ magnitude(): number {
85
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
86
+ }
87
+
88
+ distanceTo(v: Vector4): number {
89
+ return Math.sqrt((this.x - v.x) ** 2 + (this.y - v.y) ** 2 + (this.z - v.z) ** 2 + (this.w - v.w) ** 2);
90
+ }
91
+
92
+ normalize(): Vector4 {
93
+ const length = this.magnitude();
94
+
95
+ return new Vector4(this.x / length, this.y / length, this.z / length, this.w / length);
96
+ }
97
+
98
+ flat(): number[] {
99
+ return [this.x, this.y, this.z, this.w];
100
+ }
101
+
102
+ clone(): Vector4 {
103
+ return new Vector4(this.x, this.y, this.z, this.w);
104
+ }
105
+
106
+ toString(): string {
107
+ return `[${this.flat().join(", ")}]`;
108
+ }
109
+ }
110
+
111
+ export { Vector4 };
src/renderers/WebGLRenderer.ts ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Scene } from "../core/Scene";
2
+ import { FadeInPass } from "./webgl/passes/FadeInPass";
3
+ import { Camera } from "../cameras/Camera";
4
+ import { Color32 } from "../math/Color32";
5
+ import { ShaderProgram } from "./webgl/programs/ShaderProgram";
6
+ import { RenderProgram } from "./webgl/programs/RenderProgram";
7
+ import { ShaderPass } from "./webgl/passes/ShaderPass";
8
+
9
+ export class WebGLRenderer {
10
+ private _canvas: HTMLCanvasElement;
11
+ private _gl: WebGL2RenderingContext;
12
+ private _backgroundColor: Color32 = new Color32();
13
+ private _renderProgram: RenderProgram;
14
+
15
+ addProgram: (program: ShaderProgram) => void;
16
+ removeProgram: (program: ShaderProgram) => void;
17
+ resize: () => void;
18
+ setSize: (width: number, height: number) => void;
19
+ render: (scene: Scene, camera: Camera) => void;
20
+ dispose: () => void;
21
+
22
+ constructor(optionalCanvas: HTMLCanvasElement | null = null, optionalRenderPasses: ShaderPass[] | null = null) {
23
+ const canvas: HTMLCanvasElement = optionalCanvas || document.createElement("canvas");
24
+ if (!optionalCanvas) {
25
+ canvas.style.display = "block";
26
+ canvas.style.boxSizing = "border-box";
27
+ canvas.style.width = "100%";
28
+ canvas.style.height = "100%";
29
+ canvas.style.margin = "0";
30
+ canvas.style.padding = "0";
31
+ document.body.appendChild(canvas);
32
+ }
33
+ canvas.style.background = this._backgroundColor.toHexString();
34
+ this._canvas = canvas;
35
+
36
+ this._gl = canvas.getContext("webgl2", { antialias: false }) as WebGL2RenderingContext;
37
+
38
+ const renderPasses = optionalRenderPasses || [];
39
+ if (!optionalRenderPasses) {
40
+ renderPasses.push(new FadeInPass());
41
+ }
42
+
43
+ this._renderProgram = new RenderProgram(this, renderPasses);
44
+ const programs = [this._renderProgram] as ShaderProgram[];
45
+
46
+ this.resize = () => {
47
+ const width = canvas.clientWidth;
48
+ const height = canvas.clientHeight;
49
+ if (canvas.width !== width || canvas.height !== height) {
50
+ this.setSize(width, height);
51
+ }
52
+ };
53
+
54
+ this.setSize = (width: number, height: number) => {
55
+ canvas.width = width;
56
+ canvas.height = height;
57
+ this._gl.viewport(0, 0, canvas.width, canvas.height);
58
+ for (const program of programs) {
59
+ program.resize();
60
+ }
61
+ };
62
+
63
+ this.render = (scene: Scene, camera: Camera) => {
64
+ for (const program of programs) {
65
+ program.render(scene, camera);
66
+ }
67
+ };
68
+
69
+ this.dispose = () => {
70
+ for (const program of programs) {
71
+ program.dispose();
72
+ }
73
+ };
74
+
75
+ this.addProgram = (program: ShaderProgram) => {
76
+ programs.push(program);
77
+ };
78
+
79
+ this.removeProgram = (program: ShaderProgram) => {
80
+ const index = programs.indexOf(program);
81
+ if (index < 0) {
82
+ throw new Error("Program not found");
83
+ }
84
+ programs.splice(index, 1);
85
+ };
86
+
87
+ this.resize();
88
+ }
89
+
90
+ get canvas() {
91
+ return this._canvas;
92
+ }
93
+
94
+ get gl() {
95
+ return this._gl;
96
+ }
97
+
98
+ get renderProgram() {
99
+ return this._renderProgram;
100
+ }
101
+
102
+ get backgroundColor() {
103
+ return this._backgroundColor;
104
+ }
105
+
106
+ set backgroundColor(value: Color32) {
107
+ this._backgroundColor = value;
108
+ this._canvas.style.background = value.toHexString();
109
+ }
110
+ }
src/renderers/webgl/passes/FadeInPass.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RenderProgram } from "../programs/RenderProgram";
2
+ import { ShaderProgram } from "../programs/ShaderProgram";
3
+ import { ShaderPass } from "./ShaderPass";
4
+
5
+ class FadeInPass implements ShaderPass {
6
+ initialize: (program: ShaderProgram) => void;
7
+ render: () => void;
8
+
9
+ constructor(speed: number = 1.0) {
10
+ let value = 0.0;
11
+ let active = false;
12
+
13
+ let renderProgram: RenderProgram;
14
+ let gl: WebGL2RenderingContext;
15
+ let u_useDepthFade: WebGLUniformLocation;
16
+ let u_depthFade: WebGLUniformLocation;
17
+
18
+ this.initialize = (program: ShaderProgram) => {
19
+ if (!(program instanceof RenderProgram)) {
20
+ throw new Error("FadeInPass requires a RenderProgram");
21
+ }
22
+
23
+ value = program.started ? 1.0 : 0.0;
24
+ active = true;
25
+ renderProgram = program;
26
+ gl = program.renderer.gl;
27
+
28
+ u_useDepthFade = gl.getUniformLocation(renderProgram.program, "useDepthFade") as WebGLUniformLocation;
29
+ gl.uniform1i(u_useDepthFade, 1);
30
+
31
+ u_depthFade = gl.getUniformLocation(renderProgram.program, "depthFade") as WebGLUniformLocation;
32
+ gl.uniform1f(u_depthFade, value);
33
+ };
34
+
35
+ this.render = () => {
36
+ if (!active || renderProgram.renderData?.updating) return;
37
+ gl.useProgram(renderProgram.program);
38
+ value = Math.min(value + speed * 0.01, 1.0);
39
+ if (value >= 1.0) {
40
+ active = false;
41
+ gl.uniform1i(u_useDepthFade, 0);
42
+ }
43
+ gl.uniform1f(u_depthFade, value);
44
+ };
45
+ }
46
+
47
+ dispose() {}
48
+ }
49
+
50
+ export { FadeInPass };
src/renderers/webgl/passes/ShaderPass.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ShaderProgram } from "../programs/ShaderProgram";
2
+
3
+ class ShaderPass {
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ initialize(program: ShaderProgram) {}
6
+ render() {}
7
+ dispose() {}
8
+ }
9
+
10
+ export { ShaderPass };
src/renderers/webgl/programs/RenderProgram.ts ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SortWorker from "web-worker:../utils/SortWorker.ts";
2
+
3
+ import { ShaderProgram } from "./ShaderProgram";
4
+ import { ShaderPass } from "../passes/ShaderPass";
5
+ import { RenderData } from "../utils/RenderData";
6
+ import { Color32 } from "../../../math/Color32";
7
+ import { ObjectAddedEvent, ObjectChangedEvent, ObjectRemovedEvent } from "../../../events/Events";
8
+ import { Splat } from "../../../splats/Splat";
9
+ import { WebGLRenderer } from "../../WebGLRenderer";
10
+ import { Scene } from "../../../core/Scene"
11
+
12
+ const vertexShaderSource = /* glsl */ `#version 300 es
13
+ precision highp float;
14
+ precision highp int;
15
+
16
+ uniform highp usampler2D u_texture;
17
+ uniform highp sampler2D u_transforms;
18
+ uniform highp usampler2D u_transformIndices;
19
+ uniform highp sampler2D u_colorTransforms;
20
+ uniform highp usampler2D u_colorTransformIndices;
21
+ uniform mat4 projection, view;
22
+ uniform vec2 focal;
23
+ uniform vec2 viewport;
24
+
25
+ uniform bool useDepthFade;
26
+ uniform float depthFade;
27
+
28
+ in vec2 position;
29
+ in int index;
30
+
31
+ out vec4 vColor;
32
+ out vec2 vPosition;
33
+ out float vSize;
34
+ out float vSelected;
35
+
36
+ void main () {
37
+ uvec4 cen = texelFetch(u_texture, ivec2((uint(index) & 0x3ffu) << 1, uint(index) >> 10), 0);
38
+ float selected = float((cen.w >> 24) & 0xffu);
39
+
40
+ uint transformIndex = texelFetch(u_transformIndices, ivec2(uint(index) & 0x3ffu, uint(index) >> 10), 0).x;
41
+ mat4 transform = mat4(
42
+ texelFetch(u_transforms, ivec2(0, transformIndex), 0),
43
+ texelFetch(u_transforms, ivec2(1, transformIndex), 0),
44
+ texelFetch(u_transforms, ivec2(2, transformIndex), 0),
45
+ texelFetch(u_transforms, ivec2(3, transformIndex), 0)
46
+ );
47
+
48
+ if (selected < 0.5) {
49
+ selected = texelFetch(u_transforms, ivec2(4, transformIndex), 0).x;
50
+ }
51
+
52
+ mat4 viewTransform = view * transform;
53
+
54
+ vec4 cam = viewTransform * vec4(uintBitsToFloat(cen.xyz), 1);
55
+ vec4 pos2d = projection * cam;
56
+
57
+ float clip = 1.2 * pos2d.w;
58
+ if (pos2d.z < -pos2d.w || pos2d.z > pos2d.w || pos2d.x < -clip || pos2d.x > clip || pos2d.y < -clip || pos2d.y > clip) {
59
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
60
+ return;
61
+ }
62
+
63
+ uvec4 cov = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 1) | 1u, uint(index) >> 10), 0);
64
+ vec2 u1 = unpackHalf2x16(cov.x), u2 = unpackHalf2x16(cov.y), u3 = unpackHalf2x16(cov.z);
65
+ mat3 Vrk = mat3(u1.x, u1.y, u2.x, u1.y, u2.y, u3.x, u2.x, u3.x, u3.y);
66
+
67
+ mat3 J = mat3(
68
+ focal.x / cam.z, 0., -(focal.x * cam.x) / (cam.z * cam.z),
69
+ 0., -focal.y / cam.z, (focal.y * cam.y) / (cam.z * cam.z),
70
+ 0., 0., 0.
71
+ );
72
+
73
+ mat3 T = transpose(mat3(viewTransform)) * J;
74
+ mat3 cov2d = transpose(T) * Vrk * T;
75
+
76
+ //ref: https://github.com/graphdeco-inria/diff-gaussian-rasterization/blob/main/cuda_rasterizer/forward.cu#L110-L111
77
+ cov2d[0][0] += 0.3;
78
+ cov2d[1][1] += 0.3;
79
+
80
+ float mid = (cov2d[0][0] + cov2d[1][1]) / 2.0;
81
+ float radius = length(vec2((cov2d[0][0] - cov2d[1][1]) / 2.0, cov2d[0][1]));
82
+ float lambda1 = mid + radius, lambda2 = mid - radius;
83
+
84
+ if (lambda2 < 0.0) return;
85
+ vec2 diagonalVector = normalize(vec2(cov2d[0][1], lambda1 - cov2d[0][0]));
86
+ vec2 majorAxis = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector;
87
+ vec2 minorAxis = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x);
88
+
89
+ uint colorTransformIndex = texelFetch(u_colorTransformIndices, ivec2(uint(index) & 0x3ffu, uint(index) >> 10), 0).x;
90
+ mat4 colorTransform = mat4(
91
+ texelFetch(u_colorTransforms, ivec2(0, colorTransformIndex), 0),
92
+ texelFetch(u_colorTransforms, ivec2(1, colorTransformIndex), 0),
93
+ texelFetch(u_colorTransforms, ivec2(2, colorTransformIndex), 0),
94
+ texelFetch(u_colorTransforms, ivec2(3, colorTransformIndex), 0)
95
+ );
96
+
97
+ vec4 color = vec4((cov.w) & 0xffu, (cov.w >> 8) & 0xffu, (cov.w >> 16) & 0xffu, (cov.w >> 24) & 0xffu) / 255.0;
98
+ vColor = colorTransform * color;
99
+
100
+ vPosition = position;
101
+ vSize = length(majorAxis);
102
+ vSelected = selected;
103
+
104
+ float scalingFactor = 1.0;
105
+
106
+ if (useDepthFade) {
107
+ float depthNorm = (pos2d.z / pos2d.w + 1.0) / 2.0;
108
+ float near = 0.1; float far = 100.0;
109
+ float normalizedDepth = (2.0 * near) / (far + near - depthNorm * (far - near));
110
+ float start = max(normalizedDepth - 0.1, 0.0);
111
+ float end = min(normalizedDepth + 0.1, 1.0);
112
+ scalingFactor = clamp((depthFade - start) / (end - start), 0.0, 1.0);
113
+ }
114
+
115
+ vec2 vCenter = vec2(pos2d) / pos2d.w;
116
+ gl_Position = vec4(
117
+ vCenter
118
+ + position.x * majorAxis * scalingFactor / viewport
119
+ + position.y * minorAxis * scalingFactor / viewport, 0.0, 1.0);
120
+ }
121
+ `;
122
+
123
+ const fragmentShaderSource = /* glsl */ `#version 300 es
124
+ precision highp float;
125
+
126
+ uniform float outlineThickness;
127
+ uniform vec4 outlineColor;
128
+
129
+ in vec4 vColor;
130
+ in vec2 vPosition;
131
+ in float vSize;
132
+ in float vSelected;
133
+
134
+ out vec4 fragColor;
135
+
136
+ void main () {
137
+ float A = -dot(vPosition, vPosition);
138
+
139
+ if (A < -4.0) discard;
140
+
141
+ if (vSelected < 0.5) {
142
+ float B = exp(A) * vColor.a;
143
+ fragColor = vec4(B * vColor.rgb, B);
144
+ return;
145
+ }
146
+
147
+ float outlineThreshold = -4.0 + (outlineThickness / vSize);
148
+
149
+ if (A < outlineThreshold) {
150
+ fragColor = outlineColor;
151
+ }
152
+ else {
153
+ float B = exp(A) * vColor.a;
154
+ fragColor = vec4(B * vColor.rgb, B);
155
+ }
156
+ }
157
+ `;
158
+
159
+ class RenderProgram extends ShaderProgram {
160
+ private _outlineThickness: number = 10.0;
161
+ private _outlineColor: Color32 = new Color32(255, 165, 0, 255);
162
+ private _renderData: RenderData | null = null;
163
+ private _depthIndex: Uint32Array = new Uint32Array();
164
+ private _splatTexture: WebGLTexture | null = null;
165
+ private _worker: Worker | null = null;
166
+
167
+ protected _initialize: () => void;
168
+ protected _resize: () => void;
169
+ protected _render: () => void;
170
+ protected _dispose: () => void;
171
+
172
+ private _setOutlineThickness: (value: number) => void;
173
+ private _setOutlineColor: (value: Color32) => void;
174
+
175
+ constructor(renderer: WebGLRenderer, passes: ShaderPass[]) {
176
+ super(renderer, passes);
177
+
178
+ const canvas = renderer.canvas;
179
+ const gl = renderer.gl;
180
+
181
+ let u_projection: WebGLUniformLocation;
182
+ let u_viewport: WebGLUniformLocation;
183
+ let u_focal: WebGLUniformLocation;
184
+ let u_view: WebGLUniformLocation;
185
+ let u_texture: WebGLUniformLocation;
186
+ let u_transforms: WebGLUniformLocation;
187
+ let u_transformIndices: WebGLUniformLocation;
188
+ let u_colorTransforms: WebGLUniformLocation;
189
+ let u_colorTransformIndices: WebGLUniformLocation;
190
+
191
+ let u_outlineThickness: WebGLUniformLocation;
192
+ let u_outlineColor: WebGLUniformLocation;
193
+
194
+ let positionAttribute: number;
195
+ let indexAttribute: number;
196
+
197
+ let transformsTexture: WebGLTexture;
198
+ let transformIndicesTexture: WebGLTexture;
199
+
200
+ let colorTransformsTexture: WebGLTexture;
201
+ let colorTransformIndicesTexture: WebGLTexture;
202
+
203
+ let vertexBuffer: WebGLBuffer;
204
+ let indexBuffer: WebGLBuffer;
205
+
206
+ this._resize = () => {
207
+ if (!this._camera) return;
208
+
209
+ this._camera.data.setSize(canvas.width, canvas.height);
210
+ this._camera.update();
211
+
212
+ u_projection = gl.getUniformLocation(this.program, "projection") as WebGLUniformLocation;
213
+ gl.uniformMatrix4fv(u_projection, false, this._camera.data.projectionMatrix.buffer);
214
+
215
+ u_viewport = gl.getUniformLocation(this.program, "viewport") as WebGLUniformLocation;
216
+ gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height]));
217
+ };
218
+
219
+ const createWorker = () => {
220
+ this._worker = new SortWorker();
221
+ this._worker.onmessage = (e) => {
222
+ if (e.data.depthIndex) {
223
+ const { depthIndex } = e.data;
224
+ this._depthIndex = depthIndex;
225
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
226
+ gl.bufferData(gl.ARRAY_BUFFER, depthIndex, gl.STATIC_DRAW);
227
+ }
228
+ };
229
+ };
230
+
231
+ this._initialize = () => {
232
+ if (!this._scene || !this._camera) {
233
+ console.error("Cannot render without scene and camera");
234
+ return;
235
+ }
236
+
237
+ this._resize();
238
+
239
+ this._scene.addEventListener("objectAdded", handleObjectAdded);
240
+ this._scene.addEventListener("objectRemoved", handleObjectRemoved);
241
+ for (const object of this._scene.objects) {
242
+ if (object instanceof Splat) {
243
+ object.addEventListener("objectChanged", handleObjectChanged);
244
+ }
245
+ }
246
+
247
+ this._renderData = new RenderData(this._scene);
248
+
249
+ u_focal = gl.getUniformLocation(this.program, "focal") as WebGLUniformLocation;
250
+ gl.uniform2fv(u_focal, new Float32Array([this._camera.data.fx, this._camera.data.fy]));
251
+
252
+ u_view = gl.getUniformLocation(this.program, "view") as WebGLUniformLocation;
253
+ gl.uniformMatrix4fv(u_view, false, this._camera.data.viewMatrix.buffer);
254
+
255
+ u_outlineThickness = gl.getUniformLocation(this.program, "outlineThickness") as WebGLUniformLocation;
256
+ gl.uniform1f(u_outlineThickness, this.outlineThickness);
257
+
258
+ u_outlineColor = gl.getUniformLocation(this.program, "outlineColor") as WebGLUniformLocation;
259
+ gl.uniform4fv(u_outlineColor, new Float32Array(this.outlineColor.flatNorm()));
260
+
261
+ this._splatTexture = gl.createTexture() as WebGLTexture;
262
+ u_texture = gl.getUniformLocation(this.program, "u_texture") as WebGLUniformLocation;
263
+ gl.uniform1i(u_texture, 0);
264
+
265
+ transformsTexture = gl.createTexture() as WebGLTexture;
266
+ u_transforms = gl.getUniformLocation(this.program, "u_transforms") as WebGLUniformLocation;
267
+ gl.uniform1i(u_transforms, 1);
268
+
269
+ transformIndicesTexture = gl.createTexture() as WebGLTexture;
270
+ u_transformIndices = gl.getUniformLocation(this.program, "u_transformIndices") as WebGLUniformLocation;
271
+ gl.uniform1i(u_transformIndices, 2);
272
+
273
+ colorTransformsTexture = gl.createTexture() as WebGLTexture;
274
+ u_colorTransforms = gl.getUniformLocation(this.program, "u_colorTransforms") as WebGLUniformLocation;
275
+ gl.uniform1i(u_colorTransforms, 3);
276
+
277
+ colorTransformIndicesTexture = gl.createTexture() as WebGLTexture;
278
+ u_colorTransformIndices = gl.getUniformLocation(
279
+ this.program,
280
+ "u_colorTransformIndices",
281
+ ) as WebGLUniformLocation;
282
+ gl.uniform1i(u_colorTransformIndices, 4);
283
+
284
+ vertexBuffer = gl.createBuffer() as WebGLBuffer;
285
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
286
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-2, -2, 2, -2, 2, 2, -2, 2]), gl.STATIC_DRAW);
287
+
288
+ positionAttribute = gl.getAttribLocation(this.program, "position");
289
+ gl.enableVertexAttribArray(positionAttribute);
290
+ gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
291
+
292
+ indexBuffer = gl.createBuffer() as WebGLBuffer;
293
+ indexAttribute = gl.getAttribLocation(this.program, "index");
294
+ gl.enableVertexAttribArray(indexAttribute);
295
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
296
+
297
+ createWorker();
298
+ };
299
+
300
+ const handleObjectAdded = (event: Event) => {
301
+ const e = event as ObjectAddedEvent;
302
+
303
+ if (e.object instanceof Splat) {
304
+ e.object.addEventListener("objectChanged", handleObjectChanged);
305
+ }
306
+
307
+ resetSplatData();
308
+ };
309
+
310
+ const handleObjectRemoved = (event: Event) => {
311
+ const e = event as ObjectRemovedEvent;
312
+
313
+ if (e.object instanceof Splat) {
314
+ e.object.removeEventListener("objectChanged", handleObjectChanged);
315
+ }
316
+
317
+ resetSplatData();
318
+ };
319
+
320
+ const handleObjectChanged = (event: Event) => {
321
+ const e = event as ObjectChangedEvent;
322
+
323
+ if (e.object instanceof Splat && this._renderData) {
324
+ this._renderData.markDirty(e.object);
325
+ }
326
+ };
327
+
328
+ const resetSplatData = () => {
329
+ this._renderData?.dispose();
330
+ this._renderData = new RenderData(this._scene as Scene);
331
+
332
+ this._worker?.terminate();
333
+ createWorker();
334
+ };
335
+
336
+ this._render = () => {
337
+ if (!this._scene || !this._camera || !this.renderData) {
338
+ console.error("Cannot render without scene and camera");
339
+ return;
340
+ }
341
+
342
+ if (this.renderData.needsRebuild) {
343
+ this.renderData.rebuild();
344
+ }
345
+
346
+ if (
347
+ this.renderData.dataChanged ||
348
+ this.renderData.transformsChanged ||
349
+ this.renderData.colorTransformsChanged
350
+ ) {
351
+ if (this.renderData.dataChanged) {
352
+ gl.activeTexture(gl.TEXTURE0);
353
+ gl.bindTexture(gl.TEXTURE_2D, this.splatTexture);
354
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
355
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
356
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
357
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
358
+ gl.texImage2D(
359
+ gl.TEXTURE_2D,
360
+ 0,
361
+ gl.RGBA32UI,
362
+ this.renderData.width,
363
+ this.renderData.height,
364
+ 0,
365
+ gl.RGBA_INTEGER,
366
+ gl.UNSIGNED_INT,
367
+ this.renderData.data,
368
+ );
369
+ }
370
+
371
+ if (this.renderData.transformsChanged) {
372
+ gl.activeTexture(gl.TEXTURE1);
373
+ gl.bindTexture(gl.TEXTURE_2D, transformsTexture);
374
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
375
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
376
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
377
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
378
+ gl.texImage2D(
379
+ gl.TEXTURE_2D,
380
+ 0,
381
+ gl.RGBA32F,
382
+ this.renderData.transformsWidth,
383
+ this.renderData.transformsHeight,
384
+ 0,
385
+ gl.RGBA,
386
+ gl.FLOAT,
387
+ this.renderData.transforms,
388
+ );
389
+
390
+ gl.activeTexture(gl.TEXTURE2);
391
+ gl.bindTexture(gl.TEXTURE_2D, transformIndicesTexture);
392
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
393
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
394
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
395
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
396
+ gl.texImage2D(
397
+ gl.TEXTURE_2D,
398
+ 0,
399
+ gl.R32UI,
400
+ this.renderData.transformIndicesWidth,
401
+ this.renderData.transformIndicesHeight,
402
+ 0,
403
+ gl.RED_INTEGER,
404
+ gl.UNSIGNED_INT,
405
+ this.renderData.transformIndices,
406
+ );
407
+ }
408
+
409
+ if (this.renderData.colorTransformsChanged) {
410
+ gl.activeTexture(gl.TEXTURE3);
411
+ gl.bindTexture(gl.TEXTURE_2D, colorTransformsTexture);
412
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
413
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
414
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
415
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
416
+ gl.texImage2D(
417
+ gl.TEXTURE_2D,
418
+ 0,
419
+ gl.RGBA32F,
420
+ this.renderData.colorTransformsWidth,
421
+ this.renderData.colorTransformsHeight,
422
+ 0,
423
+ gl.RGBA,
424
+ gl.FLOAT,
425
+ this.renderData.colorTransforms,
426
+ );
427
+
428
+ gl.activeTexture(gl.TEXTURE4);
429
+ gl.bindTexture(gl.TEXTURE_2D, colorTransformIndicesTexture);
430
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
431
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
432
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
433
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
434
+ gl.texImage2D(
435
+ gl.TEXTURE_2D,
436
+ 0,
437
+ gl.R32UI,
438
+ this.renderData.colorTransformIndicesWidth,
439
+ this.renderData.colorTransformIndicesHeight,
440
+ 0,
441
+ gl.RED_INTEGER,
442
+ gl.UNSIGNED_INT,
443
+ this.renderData.colorTransformIndices,
444
+ );
445
+ }
446
+
447
+ const detachedPositions = new Float32Array(this.renderData.positions.slice().buffer);
448
+ const detachedTransforms = new Float32Array(this.renderData.transforms.slice().buffer);
449
+ const detachedTransformIndices = new Uint32Array(this.renderData.transformIndices.slice().buffer);
450
+ this._worker?.postMessage(
451
+ {
452
+ sortData: {
453
+ positions: detachedPositions,
454
+ transforms: detachedTransforms,
455
+ transformIndices: detachedTransformIndices,
456
+ vertexCount: this.renderData.vertexCount,
457
+ },
458
+ },
459
+ [detachedPositions.buffer, detachedTransforms.buffer, detachedTransformIndices.buffer],
460
+ );
461
+
462
+ this.renderData.dataChanged = false;
463
+ this.renderData.transformsChanged = false;
464
+ this.renderData.colorTransformsChanged = false;
465
+ }
466
+
467
+ this._camera.update();
468
+ this._worker?.postMessage({ viewProj: this._camera.data.viewProj.buffer });
469
+
470
+ gl.viewport(0, 0, canvas.width, canvas.height);
471
+ gl.clearColor(0, 0, 0, 0);
472
+ gl.clear(gl.COLOR_BUFFER_BIT);
473
+
474
+ gl.disable(gl.DEPTH_TEST);
475
+ gl.enable(gl.BLEND);
476
+ gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
477
+ gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
478
+
479
+ gl.uniformMatrix4fv(u_projection, false, this._camera.data.projectionMatrix.buffer);
480
+ gl.uniformMatrix4fv(u_view, false, this._camera.data.viewMatrix.buffer);
481
+
482
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
483
+ gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
484
+
485
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
486
+ gl.bufferData(gl.ARRAY_BUFFER, this.depthIndex, gl.STATIC_DRAW);
487
+ gl.vertexAttribIPointer(indexAttribute, 1, gl.INT, 0, 0);
488
+ gl.vertexAttribDivisor(indexAttribute, 1);
489
+
490
+ gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, this.depthIndex.length);
491
+ };
492
+
493
+ this._dispose = () => {
494
+ if (!this._scene || !this._camera || !this.renderData) {
495
+ console.error("Cannot dispose without scene and camera");
496
+ return;
497
+ }
498
+
499
+ this._scene.removeEventListener("objectAdded", handleObjectAdded);
500
+ this._scene.removeEventListener("objectRemoved", handleObjectRemoved);
501
+ for (const object of this._scene.objects) {
502
+ if (object instanceof Splat) {
503
+ object.removeEventListener("objectChanged", handleObjectChanged);
504
+ }
505
+ }
506
+
507
+ this._worker?.terminate();
508
+ this.renderData.dispose();
509
+
510
+ gl.deleteTexture(this.splatTexture);
511
+ gl.deleteTexture(transformsTexture);
512
+ gl.deleteTexture(transformIndicesTexture);
513
+
514
+ gl.deleteBuffer(indexBuffer);
515
+ gl.deleteBuffer(vertexBuffer);
516
+ };
517
+
518
+ this._setOutlineThickness = (value: number) => {
519
+ this._outlineThickness = value;
520
+ if (this._initialized) {
521
+ gl.uniform1f(u_outlineThickness, value);
522
+ }
523
+ };
524
+
525
+ this._setOutlineColor = (value: Color32) => {
526
+ this._outlineColor = value;
527
+ if (this._initialized) {
528
+ gl.uniform4fv(u_outlineColor, new Float32Array(value.flatNorm()));
529
+ }
530
+ };
531
+ }
532
+
533
+ get renderData() {
534
+ return this._renderData;
535
+ }
536
+
537
+ get depthIndex() {
538
+ return this._depthIndex;
539
+ }
540
+
541
+ get splatTexture() {
542
+ return this._splatTexture;
543
+ }
544
+
545
+ get outlineThickness() {
546
+ return this._outlineThickness;
547
+ }
548
+
549
+ set outlineThickness(value: number) {
550
+ this._setOutlineThickness(value);
551
+ }
552
+
553
+ get outlineColor() {
554
+ return this._outlineColor;
555
+ }
556
+
557
+ set outlineColor(value: Color32) {
558
+ this._setOutlineColor(value);
559
+ }
560
+
561
+ get worker() {
562
+ return this._worker;
563
+ }
564
+
565
+ protected _getVertexSource() {
566
+ return vertexShaderSource;
567
+ }
568
+
569
+ protected _getFragmentSource() {
570
+ return fragmentShaderSource;
571
+ }
572
+ }
573
+
574
+ export { RenderProgram };
src/renderers/webgl/programs/ShaderProgram.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Camera } from "../../../cameras/Camera";
2
+ import { Scene } from "../../../core/Scene";
3
+ import { WebGLRenderer } from "../../WebGLRenderer";
4
+ import { ShaderPass } from "../passes/ShaderPass";
5
+
6
+ abstract class ShaderProgram {
7
+ private _renderer: WebGLRenderer;
8
+ private _program: WebGLProgram;
9
+ private _passes: ShaderPass[];
10
+
11
+ protected _scene: Scene | null = null;
12
+ protected _camera: Camera | null = null;
13
+ protected _started: boolean = false;
14
+ protected _initialized: boolean = false;
15
+
16
+ protected abstract _initialize: () => void;
17
+ protected abstract _resize: () => void;
18
+ protected abstract _render: () => void;
19
+ protected abstract _dispose: () => void;
20
+
21
+ initialize: () => void;
22
+ resize: () => void;
23
+ render: (scene: Scene, camera: Camera) => void;
24
+ dispose: () => void;
25
+
26
+ constructor(renderer: WebGLRenderer, passes: ShaderPass[]) {
27
+ this._renderer = renderer;
28
+ const gl = renderer.gl;
29
+
30
+ this._program = gl.createProgram() as WebGLProgram;
31
+ this._passes = passes || [];
32
+
33
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER) as WebGLShader;
34
+ gl.shaderSource(vertexShader, this._getVertexSource());
35
+ gl.compileShader(vertexShader);
36
+ if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
37
+ console.error(gl.getShaderInfoLog(vertexShader));
38
+ }
39
+
40
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) as WebGLShader;
41
+ gl.shaderSource(fragmentShader, this._getFragmentSource());
42
+ gl.compileShader(fragmentShader);
43
+ if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
44
+ console.error(gl.getShaderInfoLog(fragmentShader));
45
+ }
46
+
47
+ gl.attachShader(this.program, vertexShader);
48
+ gl.attachShader(this.program, fragmentShader);
49
+ gl.linkProgram(this.program);
50
+ if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
51
+ console.error(gl.getProgramInfoLog(this.program));
52
+ }
53
+
54
+ this.resize = () => {
55
+ gl.useProgram(this._program);
56
+
57
+ this._resize();
58
+ };
59
+
60
+ this.initialize = () => {
61
+ console.assert(!this._initialized, "ShaderProgram already initialized");
62
+
63
+ gl.useProgram(this._program);
64
+
65
+ this._initialize();
66
+ for (const pass of this.passes) {
67
+ pass.initialize(this);
68
+ }
69
+
70
+ this._initialized = true;
71
+ this._started = true;
72
+ };
73
+
74
+ this.render = (scene: Scene, camera: Camera) => {
75
+ gl.useProgram(this._program);
76
+
77
+ if (this._scene !== scene || this._camera !== camera) {
78
+ this.dispose();
79
+ this._scene = scene;
80
+ this._camera = camera;
81
+ this.initialize();
82
+ }
83
+
84
+ for (const pass of this.passes) {
85
+ pass.render();
86
+ }
87
+
88
+ this._render();
89
+ };
90
+
91
+ this.dispose = () => {
92
+ if (!this._initialized) return;
93
+
94
+ gl.useProgram(this._program);
95
+
96
+ for (const pass of this.passes) {
97
+ pass.dispose();
98
+ }
99
+
100
+ this._dispose();
101
+
102
+ this._scene = null;
103
+ this._camera = null;
104
+ this._initialized = false;
105
+ };
106
+ }
107
+
108
+ get renderer() {
109
+ return this._renderer;
110
+ }
111
+
112
+ get scene() {
113
+ return this._scene;
114
+ }
115
+
116
+ get camera() {
117
+ return this._camera;
118
+ }
119
+
120
+ get program() {
121
+ return this._program;
122
+ }
123
+
124
+ get passes() {
125
+ return this._passes;
126
+ }
127
+
128
+ get started() {
129
+ return this._started;
130
+ }
131
+
132
+ protected abstract _getVertexSource(): string;
133
+ protected abstract _getFragmentSource(): string;
134
+ }
135
+
136
+ export { ShaderProgram };
src/renderers/webgl/programs/VideoRenderProgram.ts ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Splatv } from "../../../splats/Splatv";
2
+ import { SplatvData } from "../../../splats/SplatvData";
3
+ import { WebGLRenderer } from "../../WebGLRenderer";
4
+ import { ShaderPass } from "../passes/ShaderPass";
5
+ import { ShaderProgram } from "./ShaderProgram";
6
+ import { ObjectAddedEvent, ObjectChangedEvent, ObjectRemovedEvent } from "../../../events/Events";
7
+ import { Matrix4 } from "../../../math/Matrix4";
8
+
9
+ const vertexShaderSource = /* glsl */ `#version 300 es
10
+ precision highp float;
11
+ precision highp int;
12
+
13
+ uniform highp usampler2D u_texture;
14
+ uniform mat4 projection, view;
15
+ uniform vec2 focal;
16
+ uniform vec2 viewport;
17
+ uniform float time;
18
+
19
+ in vec2 position;
20
+ in int index;
21
+
22
+ out vec4 vColor;
23
+ out vec2 vPosition;
24
+
25
+ void main () {
26
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
27
+
28
+ uvec4 motion1 = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 2) | 3u, uint(index) >> 10), 0);
29
+ vec2 trbf = unpackHalf2x16(motion1.w);
30
+ float dt = time - trbf.x;
31
+
32
+ float topacity = exp(-1.0 * pow(dt / trbf.y, 2.0));
33
+ if(topacity < 0.02) return;
34
+
35
+ uvec4 motion0 = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 2) | 2u, uint(index) >> 10), 0);
36
+ uvec4 static0 = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 2), uint(index) >> 10), 0);
37
+
38
+ vec2 m0 = unpackHalf2x16(motion0.x), m1 = unpackHalf2x16(motion0.y), m2 = unpackHalf2x16(motion0.z),
39
+ m3 = unpackHalf2x16(motion0.w), m4 = unpackHalf2x16(motion1.x);
40
+
41
+ vec4 trot = vec4(unpackHalf2x16(motion1.y).xy, unpackHalf2x16(motion1.z).xy) * dt;
42
+ vec3 tpos = (vec3(m0.xy, m1.x) * dt + vec3(m1.y, m2.xy) * dt*dt + vec3(m3.xy, m4.x) * dt*dt*dt);
43
+
44
+ vec4 cam = view * vec4(uintBitsToFloat(static0.xyz) + tpos, 1);
45
+ vec4 pos = projection * cam;
46
+
47
+ float clip = 1.2 * pos.w;
48
+ if (pos.z < -clip || pos.x < -clip || pos.x > clip || pos.y < -clip || pos.y > clip) return;
49
+ uvec4 static1 = texelFetch(u_texture, ivec2(((uint(index) & 0x3ffu) << 2) | 1u, uint(index) >> 10), 0);
50
+
51
+ vec4 rot = vec4(unpackHalf2x16(static0.w).xy, unpackHalf2x16(static1.x).xy) + trot;
52
+ vec3 scale = vec3(unpackHalf2x16(static1.y).xy, unpackHalf2x16(static1.z).x);
53
+ rot /= sqrt(dot(rot, rot));
54
+
55
+ mat3 S = mat3(scale.x, 0.0, 0.0, 0.0, scale.y, 0.0, 0.0, 0.0, scale.z);
56
+ mat3 R = mat3(
57
+ 1.0 - 2.0 * (rot.z * rot.z + rot.w * rot.w), 2.0 * (rot.y * rot.z - rot.x * rot.w), 2.0 * (rot.y * rot.w + rot.x * rot.z),
58
+ 2.0 * (rot.y * rot.z + rot.x * rot.w), 1.0 - 2.0 * (rot.y * rot.y + rot.w * rot.w), 2.0 * (rot.z * rot.w - rot.x * rot.y),
59
+ 2.0 * (rot.y * rot.w - rot.x * rot.z), 2.0 * (rot.z * rot.w + rot.x * rot.y), 1.0 - 2.0 * (rot.y * rot.y + rot.z * rot.z));
60
+ mat3 M = S * R;
61
+ mat3 Vrk = 4.0 * transpose(M) * M;
62
+ mat3 J = mat3(
63
+ focal.x / cam.z, 0., -(focal.x * cam.x) / (cam.z * cam.z),
64
+ 0., -focal.y / cam.z, (focal.y * cam.y) / (cam.z * cam.z),
65
+ 0., 0., 0.
66
+ );
67
+
68
+ mat3 T = transpose(mat3(view)) * J;
69
+ mat3 cov2d = transpose(T) * Vrk * T;
70
+
71
+ float mid = (cov2d[0][0] + cov2d[1][1]) / 2.0;
72
+ float radius = length(vec2((cov2d[0][0] - cov2d[1][1]) / 2.0, cov2d[0][1]));
73
+ float lambda1 = mid + radius, lambda2 = mid - radius;
74
+
75
+ if(lambda2 < 0.0) return;
76
+ vec2 diagonalVector = normalize(vec2(cov2d[0][1], lambda1 - cov2d[0][0]));
77
+ vec2 majorAxis = min(sqrt(2.0 * lambda1), 1024.0) * diagonalVector;
78
+ vec2 minorAxis = min(sqrt(2.0 * lambda2), 1024.0) * vec2(diagonalVector.y, -diagonalVector.x);
79
+
80
+ uint rgba = static1.w;
81
+ vColor =
82
+ clamp(pos.z/pos.w+1.0, 0.0, 1.0) *
83
+ vec4(1.0, 1.0, 1.0, topacity) *
84
+ vec4(
85
+ (rgba) & 0xffu,
86
+ (rgba >> 8) & 0xffu,
87
+ (rgba >> 16) & 0xffu,
88
+ (rgba >> 24) & 0xffu) / 255.0;
89
+
90
+ vec2 vCenter = vec2(pos) / pos.w;
91
+ gl_Position = vec4(
92
+ vCenter
93
+ + position.x * majorAxis / viewport
94
+ + position.y * minorAxis / viewport, 0.0, 1.0);
95
+
96
+ vPosition = position;
97
+ }
98
+ `;
99
+
100
+ const fragmentShaderSource = /* glsl */ `#version 300 es
101
+ precision highp float;
102
+
103
+ in vec4 vColor;
104
+ in vec2 vPosition;
105
+
106
+ out vec4 fragColor;
107
+
108
+ void main () {
109
+ float A = -dot(vPosition, vPosition);
110
+ if (A < -4.0) discard;
111
+ float B = exp(A) * vColor.a;
112
+ fragColor = vec4(B * vColor.rgb, B);
113
+ }
114
+ `;
115
+
116
+ class VideoRenderProgram extends ShaderProgram {
117
+ private _renderData: SplatvData | null = null;
118
+ private _depthIndex: Uint32Array = new Uint32Array();
119
+ private _splatTexture: WebGLTexture | null = null;
120
+
121
+ protected _initialize: () => void;
122
+ protected _resize: () => void;
123
+ protected _render: () => void;
124
+ protected _dispose: () => void;
125
+
126
+ constructor(renderer: WebGLRenderer, passes: ShaderPass[] = []) {
127
+ super(renderer, passes);
128
+
129
+ const canvas = renderer.canvas;
130
+ const gl = renderer.gl;
131
+
132
+ let worker: Worker;
133
+
134
+ let u_projection: WebGLUniformLocation;
135
+ let u_viewport: WebGLUniformLocation;
136
+ let u_focal: WebGLUniformLocation;
137
+ let u_view: WebGLUniformLocation;
138
+ let u_texture: WebGLUniformLocation;
139
+ let u_time: WebGLUniformLocation;
140
+
141
+ let positionAttribute: number;
142
+ let indexAttribute: number;
143
+
144
+ let vertexBuffer: WebGLBuffer;
145
+ let indexBuffer: WebGLBuffer;
146
+
147
+ this._resize = () => {
148
+ if (!this._camera) return;
149
+
150
+ this._camera.data.setSize(canvas.width, canvas.height);
151
+ this._camera.update();
152
+
153
+ u_projection = gl.getUniformLocation(this.program, "projection") as WebGLUniformLocation;
154
+ gl.uniformMatrix4fv(u_projection, false, this._camera.data.projectionMatrix.buffer);
155
+
156
+ u_viewport = gl.getUniformLocation(this.program, "viewport") as WebGLUniformLocation;
157
+ gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height]));
158
+ };
159
+
160
+ const setupWorker = () => {
161
+ if (renderer.renderProgram.worker === null) {
162
+ console.error("Render program is not initialized. Cannot render without worker");
163
+ return;
164
+ }
165
+ worker = renderer.renderProgram.worker;
166
+ worker.onmessage = (e) => {
167
+ if (e.data.depthIndex) {
168
+ const { depthIndex } = e.data;
169
+ this._depthIndex = depthIndex;
170
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
171
+ gl.bufferData(gl.ARRAY_BUFFER, depthIndex, gl.STATIC_DRAW);
172
+ }
173
+ };
174
+ };
175
+
176
+ this._initialize = () => {
177
+ if (!this._scene || !this._camera) {
178
+ console.error("Cannot render without scene and camera");
179
+ return;
180
+ }
181
+
182
+ this._resize();
183
+
184
+ this._scene.addEventListener("objectAdded", handleObjectAdded);
185
+ this._scene.addEventListener("objectRemoved", handleObjectRemoved);
186
+ for (const object of this._scene.objects) {
187
+ if (object instanceof Splatv) {
188
+ if (this._renderData === null) {
189
+ this._renderData = object.data;
190
+ object.addEventListener("objectChanged", handleObjectChanged);
191
+ } else {
192
+ console.warn("Multiple Splatv objects are not currently supported");
193
+ }
194
+ }
195
+ }
196
+
197
+ if (this._renderData === null) {
198
+ console.error("Cannot render without Splatv object");
199
+ return;
200
+ }
201
+
202
+ u_focal = gl.getUniformLocation(this.program, "focal") as WebGLUniformLocation;
203
+ gl.uniform2fv(u_focal, new Float32Array([this._camera.data.fx, this._camera.data.fy]));
204
+
205
+ u_view = gl.getUniformLocation(this.program, "view") as WebGLUniformLocation;
206
+ gl.uniformMatrix4fv(u_view, false, this._camera.data.viewMatrix.buffer);
207
+
208
+ this._splatTexture = gl.createTexture() as WebGLTexture;
209
+ u_texture = gl.getUniformLocation(this.program, "u_texture") as WebGLUniformLocation;
210
+ gl.uniform1i(u_texture, 0);
211
+
212
+ u_time = gl.getUniformLocation(this.program, "time") as WebGLUniformLocation;
213
+ gl.uniform1f(u_time, Math.sin(Date.now() / 1000) / 2 + 1 / 2);
214
+
215
+ vertexBuffer = gl.createBuffer() as WebGLBuffer;
216
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
217
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-2, -2, 2, -2, 2, 2, -2, 2]), gl.STATIC_DRAW);
218
+
219
+ positionAttribute = gl.getAttribLocation(this.program, "position");
220
+ gl.enableVertexAttribArray(positionAttribute);
221
+ gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
222
+
223
+ indexBuffer = gl.createBuffer() as WebGLBuffer;
224
+ indexAttribute = gl.getAttribLocation(this.program, "index");
225
+ gl.enableVertexAttribArray(indexAttribute);
226
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
227
+
228
+ setupWorker();
229
+
230
+ gl.activeTexture(gl.TEXTURE0);
231
+ gl.bindTexture(gl.TEXTURE_2D, this._splatTexture);
232
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
233
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
234
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
235
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
236
+ gl.texImage2D(
237
+ gl.TEXTURE_2D,
238
+ 0,
239
+ gl.RGBA32UI,
240
+ this._renderData.width,
241
+ this._renderData.height,
242
+ 0,
243
+ gl.RGBA_INTEGER,
244
+ gl.UNSIGNED_INT,
245
+ this._renderData.data,
246
+ );
247
+
248
+ const positions = this._renderData.positions;
249
+ const dummyTransforms = new Float32Array(new Matrix4().buffer);
250
+ const dummyTransformIndices = new Uint32Array(this._renderData.vertexCount);
251
+ dummyTransformIndices.fill(0);
252
+ worker.postMessage(
253
+ {
254
+ sortData: {
255
+ positions: positions,
256
+ transforms: dummyTransforms,
257
+ transformIndices: dummyTransformIndices,
258
+ vertexCount: this._renderData.vertexCount,
259
+ },
260
+ },
261
+ [positions.buffer, dummyTransforms.buffer, dummyTransformIndices.buffer],
262
+ );
263
+ };
264
+
265
+ const handleObjectAdded = (event: Event) => {
266
+ const e = event as ObjectAddedEvent;
267
+
268
+ if (e.object instanceof Splatv) {
269
+ if (this._renderData === null) {
270
+ this._renderData = e.object.data;
271
+ e.object.addEventListener("objectChanged", handleObjectChanged);
272
+ } else {
273
+ console.warn("Splatv not supported by default RenderProgram. Use VideoRenderProgram instead.");
274
+ }
275
+ }
276
+
277
+ this.dispose();
278
+ };
279
+
280
+ const handleObjectRemoved = (event: Event) => {
281
+ const e = event as ObjectRemovedEvent;
282
+
283
+ if (e.object instanceof Splatv) {
284
+ if (this._renderData === e.object.data) {
285
+ this._renderData = null;
286
+ e.object.removeEventListener("objectChanged", handleObjectChanged);
287
+ }
288
+ }
289
+
290
+ this.dispose();
291
+ };
292
+
293
+ const handleObjectChanged = (event: Event) => {
294
+ const e = event as ObjectChangedEvent;
295
+
296
+ if (e.object instanceof Splatv && this._renderData === e.object.data) {
297
+ this.dispose();
298
+ }
299
+ };
300
+
301
+ this._render = () => {
302
+ if (!this._scene || !this._camera) {
303
+ console.error("Cannot render without scene and camera");
304
+ return;
305
+ }
306
+
307
+ if (!this._renderData) {
308
+ console.warn("Cannot render without Splatv object");
309
+ return;
310
+ }
311
+
312
+ this._camera.update();
313
+ worker.postMessage({ viewProj: this._camera.data.viewProj.buffer });
314
+
315
+ gl.viewport(0, 0, canvas.width, canvas.height);
316
+ gl.clearColor(0, 0, 0, 0);
317
+ gl.clear(gl.COLOR_BUFFER_BIT);
318
+
319
+ gl.disable(gl.DEPTH_TEST);
320
+ gl.enable(gl.BLEND);
321
+ gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE);
322
+ gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
323
+
324
+ gl.uniformMatrix4fv(u_projection, false, this._camera.data.projectionMatrix.buffer);
325
+ gl.uniformMatrix4fv(u_view, false, this._camera.data.viewMatrix.buffer);
326
+ gl.uniform1f(u_time, Math.sin(Date.now() / 1000) / 2 + 1 / 2);
327
+
328
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
329
+ gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
330
+
331
+ gl.bindBuffer(gl.ARRAY_BUFFER, indexBuffer);
332
+ gl.bufferData(gl.ARRAY_BUFFER, this._depthIndex, gl.STATIC_DRAW);
333
+ gl.vertexAttribIPointer(indexAttribute, 1, gl.INT, 0, 0);
334
+ gl.vertexAttribDivisor(indexAttribute, 1);
335
+
336
+ gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, this._renderData.vertexCount);
337
+ };
338
+
339
+ this._dispose = () => {
340
+ if (!this._scene || !this._camera) {
341
+ console.error("Cannot dispose without scene and camera");
342
+ return;
343
+ }
344
+
345
+ this._scene.removeEventListener("objectAdded", handleObjectAdded);
346
+ this._scene.removeEventListener("objectRemoved", handleObjectRemoved);
347
+ for (const object of this._scene.objects) {
348
+ if (object instanceof Splatv) {
349
+ if (this._renderData === object.data) {
350
+ this._renderData = null;
351
+ object.removeEventListener("objectChanged", handleObjectChanged);
352
+ }
353
+ }
354
+ }
355
+
356
+ worker?.terminate();
357
+
358
+ gl.deleteTexture(this._splatTexture);
359
+
360
+ gl.deleteBuffer(indexBuffer);
361
+ gl.deleteBuffer(vertexBuffer);
362
+ };
363
+ }
364
+
365
+ get renderData(): SplatvData | null {
366
+ return this._renderData;
367
+ }
368
+
369
+ protected _getVertexSource(): string {
370
+ return vertexShaderSource;
371
+ }
372
+
373
+ protected _getFragmentSource(): string {
374
+ return fragmentShaderSource;
375
+ }
376
+ }
377
+
378
+ export { VideoRenderProgram };
src/renderers/webgl/utils/DataWorker.ts ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import loadWasm from "../../../wasm/data";
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ let wasmModule: any;
5
+
6
+ async function initWasm() {
7
+ wasmModule = await loadWasm();
8
+ }
9
+
10
+ class Splat {
11
+ offset: number = 0;
12
+ position: Float32Array = new Float32Array(3);
13
+ rotation: Float32Array = new Float32Array(4);
14
+ scale: Float32Array = new Float32Array(3);
15
+ selected: boolean = false;
16
+ vertexCount: number = 0;
17
+ positions: Float32Array = new Float32Array(0);
18
+ rotations: Float32Array = new Float32Array(0);
19
+ scales: Float32Array = new Float32Array(0);
20
+ colors: Uint8Array = new Uint8Array(0);
21
+ selection: Uint8Array = new Uint8Array(0);
22
+ }
23
+
24
+ let allocatedVertexCount: number = 0;
25
+ const updateQueue = new Array<Splat>();
26
+ let running = false;
27
+ let loading = false;
28
+
29
+ let positionsPtr: number;
30
+ let rotationsPtr: number;
31
+ let scalesPtr: number;
32
+ let colorsPtr: number;
33
+ let selectionPtr: number;
34
+ let dataPtr: number;
35
+ let worldPositionsPtr: number;
36
+ let worldRotationsPtr: number;
37
+ let worldScalesPtr: number;
38
+
39
+ const pack = async (splat: Splat) => {
40
+ while (loading) {
41
+ await new Promise((resolve) => setTimeout(resolve, 0));
42
+ }
43
+
44
+ if (!wasmModule) {
45
+ loading = true;
46
+ await initWasm();
47
+ loading = false;
48
+ }
49
+
50
+ const targetAllocatedVertexCount = Math.pow(2, Math.ceil(Math.log2(splat.vertexCount)));
51
+ if (targetAllocatedVertexCount > allocatedVertexCount) {
52
+ if (allocatedVertexCount > 0) {
53
+ wasmModule._free(positionsPtr);
54
+ wasmModule._free(rotationsPtr);
55
+ wasmModule._free(scalesPtr);
56
+ wasmModule._free(colorsPtr);
57
+ wasmModule._free(selectionPtr);
58
+ wasmModule._free(dataPtr);
59
+ wasmModule._free(worldPositionsPtr);
60
+ wasmModule._free(worldRotationsPtr);
61
+ wasmModule._free(worldScalesPtr);
62
+ }
63
+
64
+ allocatedVertexCount = targetAllocatedVertexCount;
65
+
66
+ positionsPtr = wasmModule._malloc(3 * allocatedVertexCount * 4);
67
+ rotationsPtr = wasmModule._malloc(4 * allocatedVertexCount * 4);
68
+ scalesPtr = wasmModule._malloc(3 * allocatedVertexCount * 4);
69
+ colorsPtr = wasmModule._malloc(4 * allocatedVertexCount);
70
+ selectionPtr = wasmModule._malloc(allocatedVertexCount);
71
+ dataPtr = wasmModule._malloc(8 * allocatedVertexCount * 4);
72
+ worldPositionsPtr = wasmModule._malloc(3 * allocatedVertexCount * 4);
73
+ worldRotationsPtr = wasmModule._malloc(4 * allocatedVertexCount * 4);
74
+ worldScalesPtr = wasmModule._malloc(3 * allocatedVertexCount * 4);
75
+ }
76
+
77
+ wasmModule.HEAPF32.set(splat.positions, positionsPtr / 4);
78
+ wasmModule.HEAPF32.set(splat.rotations, rotationsPtr / 4);
79
+ wasmModule.HEAPF32.set(splat.scales, scalesPtr / 4);
80
+ wasmModule.HEAPU8.set(splat.colors, colorsPtr);
81
+ wasmModule.HEAPU8.set(splat.selection, selectionPtr);
82
+
83
+ wasmModule._pack(
84
+ splat.selected,
85
+ splat.vertexCount,
86
+ positionsPtr,
87
+ rotationsPtr,
88
+ scalesPtr,
89
+ colorsPtr,
90
+ selectionPtr,
91
+ dataPtr,
92
+ worldPositionsPtr,
93
+ worldRotationsPtr,
94
+ worldScalesPtr,
95
+ );
96
+
97
+ const outData = new Uint32Array(wasmModule.HEAPU32.buffer, dataPtr, splat.vertexCount * 8);
98
+ const detachedData = new Uint32Array(outData.slice().buffer);
99
+
100
+ const worldPositions = new Float32Array(wasmModule.HEAPF32.buffer, worldPositionsPtr, splat.vertexCount * 3);
101
+ const detachedWorldPositions = new Float32Array(worldPositions.slice().buffer);
102
+
103
+ const worldRotations = new Float32Array(wasmModule.HEAPF32.buffer, worldRotationsPtr, splat.vertexCount * 4);
104
+ const detachedWorldRotations = new Float32Array(worldRotations.slice().buffer);
105
+
106
+ const worldScales = new Float32Array(wasmModule.HEAPF32.buffer, worldScalesPtr, splat.vertexCount * 3);
107
+ const detachedWorldScales = new Float32Array(worldScales.slice().buffer);
108
+
109
+ const response = {
110
+ data: detachedData,
111
+ worldPositions: detachedWorldPositions,
112
+ worldRotations: detachedWorldRotations,
113
+ worldScales: detachedWorldScales,
114
+ offset: splat.offset,
115
+ vertexCount: splat.vertexCount,
116
+ positions: splat.positions.buffer,
117
+ rotations: splat.rotations.buffer,
118
+ scales: splat.scales.buffer,
119
+ colors: splat.colors.buffer,
120
+ selection: splat.selection.buffer,
121
+ };
122
+
123
+ self.postMessage({ response: response }, [
124
+ response.data.buffer,
125
+ response.worldPositions.buffer,
126
+ response.worldRotations.buffer,
127
+ response.worldScales.buffer,
128
+ response.positions,
129
+ response.rotations,
130
+ response.scales,
131
+ response.colors,
132
+ response.selection,
133
+ ]);
134
+
135
+ running = false;
136
+ };
137
+
138
+ const packThrottled = () => {
139
+ if (updateQueue.length === 0) return;
140
+ if (!running) {
141
+ running = true;
142
+ const splat = updateQueue.shift() as Splat;
143
+ pack(splat);
144
+ setTimeout(() => {
145
+ running = false;
146
+ packThrottled();
147
+ }, 0);
148
+ }
149
+ };
150
+
151
+ self.onmessage = (e) => {
152
+ if (e.data.splat) {
153
+ const splat = e.data.splat as Splat;
154
+ for (const [index, existing] of updateQueue.entries()) {
155
+ if (existing.offset === splat.offset) {
156
+ updateQueue[index] = splat;
157
+ return;
158
+ }
159
+ }
160
+ updateQueue.push(splat);
161
+ packThrottled();
162
+ }
163
+ };
src/renderers/webgl/utils/IntersectionTester.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Camera } from "../../../cameras/Camera";
2
+ import { Vector3 } from "../../../math/Vector3";
3
+ import { Splat } from "../../../splats/Splat";
4
+ import { RenderProgram } from "../programs/RenderProgram";
5
+ import { Box3 } from "../../../math/Box3";
6
+ import { BVH } from "../../../math/BVH";
7
+ import { RenderData } from "./RenderData";
8
+
9
+ class IntersectionTester {
10
+ testPoint: (x: number, y: number) => Splat | null;
11
+
12
+ constructor(renderProgram: RenderProgram, maxDistance: number = 100, resolution: number = 1.0) {
13
+ let vertexCount = 0;
14
+ let bvh: BVH | null = null;
15
+ let lookup: Splat[] = [];
16
+
17
+ const build = () => {
18
+ if (renderProgram.renderData === null) {
19
+ console.error("IntersectionTester cannot be called before renderProgram has been initialized");
20
+ return;
21
+ }
22
+ lookup = [];
23
+ const renderData = renderProgram.renderData as RenderData;
24
+ const boxes = new Array<Box3>(renderData.offsets.size);
25
+ let i = 0;
26
+ const bounds = new Box3(
27
+ new Vector3(Infinity, Infinity, Infinity),
28
+ new Vector3(-Infinity, -Infinity, -Infinity),
29
+ );
30
+ for (const splat of renderData.offsets.keys()) {
31
+ const splatBounds = splat.bounds;
32
+ boxes[i++] = splatBounds;
33
+ bounds.expand(splatBounds.min);
34
+ bounds.expand(splatBounds.max);
35
+ lookup.push(splat);
36
+ }
37
+ bounds.permute();
38
+ bvh = new BVH(bounds, boxes);
39
+ vertexCount = renderData.vertexCount;
40
+ };
41
+
42
+ this.testPoint = (x: number, y: number) => {
43
+ if (renderProgram.renderData === null || renderProgram.camera === null) {
44
+ console.error("IntersectionTester cannot be called before renderProgram has been initialized");
45
+ return null;
46
+ }
47
+
48
+ build();
49
+
50
+ if (bvh === null) {
51
+ console.error("Failed to build octree for IntersectionTester");
52
+ return null;
53
+ }
54
+
55
+ const renderData = renderProgram.renderData as RenderData;
56
+ const camera = renderProgram.camera as Camera;
57
+
58
+ if (vertexCount !== renderData.vertexCount) {
59
+ console.warn("IntersectionTester has not been rebuilt since the last render");
60
+ }
61
+
62
+ const ray = camera.screenPointToRay(x, y);
63
+ for (let x = 0; x < maxDistance; x += resolution) {
64
+ const point = camera.position.add(ray.multiply(x));
65
+ const minPoint = new Vector3(
66
+ point.x - resolution / 2,
67
+ point.y - resolution / 2,
68
+ point.z - resolution / 2,
69
+ );
70
+ const maxPoint = new Vector3(
71
+ point.x + resolution / 2,
72
+ point.y + resolution / 2,
73
+ point.z + resolution / 2,
74
+ );
75
+ const queryBox = new Box3(minPoint, maxPoint);
76
+ const points = bvh.queryRange(queryBox);
77
+ if (points.length > 0) {
78
+ return lookup[points[0]];
79
+ }
80
+ }
81
+
82
+ return null;
83
+ };
84
+ }
85
+ }
86
+
87
+ export { IntersectionTester };
src/renderers/webgl/utils/RenderData.ts ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Scene } from "../../../core/Scene";
2
+ import { Splat } from "../../../splats/Splat";
3
+ import DataWorker from "web-worker:./DataWorker.ts";
4
+ import loadWasm from "../../../wasm/data";
5
+ import { Matrix4 } from "../../../math/Matrix4";
6
+
7
+ class RenderData {
8
+ public dataChanged = false;
9
+ public transformsChanged = false;
10
+ public colorTransformsChanged = false;
11
+
12
+ private _splatIndices: Map<Splat, number>;
13
+ private _offsets: Map<Splat, number>;
14
+ private _data: Uint32Array;
15
+ private _width: number;
16
+ private _height: number;
17
+ private _transforms: Float32Array;
18
+ private _transformsWidth: number;
19
+ private _transformsHeight: number;
20
+ private _transformIndices: Uint32Array;
21
+ private _transformIndicesWidth: number;
22
+ private _transformIndicesHeight: number;
23
+ private _colorTransforms: Float32Array;
24
+ private _colorTransformsWidth: number;
25
+ private _colorTransformsHeight: number;
26
+ private _colorTransformIndices: Uint32Array;
27
+ private _colorTransformIndicesWidth: number;
28
+ private _colorTransformIndicesHeight: number;
29
+ private _positions: Float32Array;
30
+ private _rotations: Float32Array;
31
+ private _scales: Float32Array;
32
+ private _vertexCount: number;
33
+ private _updating: Set<Splat> = new Set<Splat>();
34
+ private _dirty: Set<Splat> = new Set<Splat>();
35
+ private _worker: Worker;
36
+
37
+ getSplat: (index: number) => Splat | null;
38
+ getLocalIndex: (splat: Splat, index: number) => number;
39
+ markDirty: (splat: Splat) => void;
40
+ rebuild: () => void;
41
+ dispose: () => void;
42
+
43
+ constructor(scene: Scene) {
44
+ let vertexCount = 0;
45
+ let splatIndex = 0;
46
+ this._splatIndices = new Map<Splat, number>();
47
+ this._offsets = new Map<Splat, number>();
48
+ const lookup = new Map<number, Splat>();
49
+ for (const object of scene.objects) {
50
+ if (object instanceof Splat) {
51
+ this._splatIndices.set(object, splatIndex);
52
+ this._offsets.set(object, vertexCount);
53
+ lookup.set(vertexCount, object);
54
+ vertexCount += object.data.vertexCount;
55
+ splatIndex++;
56
+ }
57
+ }
58
+
59
+ this._vertexCount = vertexCount;
60
+ this._width = 2048;
61
+ this._height = Math.ceil((2 * this.vertexCount) / this.width);
62
+ this._data = new Uint32Array(this.width * this.height * 4);
63
+
64
+ this._transformsWidth = 5;
65
+ this._transformsHeight = lookup.size;
66
+ this._transforms = new Float32Array(this._transformsWidth * this._transformsHeight * 4);
67
+
68
+ this._transformIndicesWidth = 1024;
69
+ this._transformIndicesHeight = Math.ceil(this.vertexCount / this._transformIndicesWidth);
70
+ this._transformIndices = new Uint32Array(this._transformIndicesWidth * this._transformIndicesHeight);
71
+
72
+ this._colorTransformsWidth = 4;
73
+ this._colorTransformsHeight = 64;
74
+ this._colorTransforms = new Float32Array(this._colorTransformsWidth * this._colorTransformsHeight * 4);
75
+ this._colorTransforms.fill(0);
76
+ this._colorTransforms[0] = 1;
77
+ this._colorTransforms[5] = 1;
78
+ this._colorTransforms[10] = 1;
79
+ this._colorTransforms[15] = 1;
80
+
81
+ this._colorTransformIndicesWidth = 1024;
82
+ this._colorTransformIndicesHeight = Math.ceil(this.vertexCount / this._colorTransformIndicesWidth);
83
+ this._colorTransformIndices = new Uint32Array(
84
+ this._colorTransformIndicesWidth * this._colorTransformIndicesHeight,
85
+ );
86
+ this.colorTransformIndices.fill(0);
87
+
88
+ this._positions = new Float32Array(this.vertexCount * 3);
89
+ this._rotations = new Float32Array(this.vertexCount * 4);
90
+ this._scales = new Float32Array(this.vertexCount * 3);
91
+
92
+ this._worker = new DataWorker();
93
+
94
+ const updateTransform = (splat: Splat) => {
95
+ const splatIndex = this._splatIndices.get(splat) as number;
96
+ this._transforms.set(splat.transform.buffer, splatIndex * 20);
97
+ this._transforms[splatIndex * 20 + 16] = splat.selected ? 1 : 0;
98
+ splat.positionChanged = false;
99
+ splat.rotationChanged = false;
100
+ splat.scaleChanged = false;
101
+ splat.selectedChanged = false;
102
+ this.transformsChanged = true;
103
+ };
104
+
105
+ const updateColorTransforms = () => {
106
+ let colorTransformsChanged = false;
107
+ for (const splat of this._splatIndices.keys()) {
108
+ if (splat.colorTransformChanged) {
109
+ colorTransformsChanged = true;
110
+ break;
111
+ }
112
+ }
113
+ if (!colorTransformsChanged) {
114
+ return;
115
+ }
116
+ const colorTransformsMap: Matrix4[] = [new Matrix4()];
117
+ this._colorTransformIndices.fill(0);
118
+ let i = 1;
119
+ for (const splat of this._splatIndices.keys()) {
120
+ const offset = this._offsets.get(splat) as number;
121
+ for (const colorTransform of splat.colorTransforms) {
122
+ if (!colorTransformsMap.includes(colorTransform)) {
123
+ colorTransformsMap.push(colorTransform);
124
+ i++;
125
+ }
126
+ }
127
+ for (const index of splat.colorTransformsMap.keys()) {
128
+ const colorTransformIndex = splat.colorTransformsMap.get(index) as number;
129
+ this._colorTransformIndices[index + offset] = colorTransformIndex + i - 1;
130
+ }
131
+ splat.colorTransformChanged = false;
132
+ }
133
+ for (let index = 0; index < colorTransformsMap.length; index++) {
134
+ const colorTransform = colorTransformsMap[index];
135
+ this._colorTransforms.set(colorTransform.buffer, index * 16);
136
+ }
137
+ this.colorTransformsChanged = true;
138
+ };
139
+
140
+ this._worker.onmessage = (e) => {
141
+ if (e.data.response) {
142
+ const response = e.data.response;
143
+ const splat = lookup.get(response.offset) as Splat;
144
+ updateTransform(splat);
145
+ updateColorTransforms();
146
+
147
+ const splatIndex = this._splatIndices.get(splat) as number;
148
+ for (let i = 0; i < splat.data.vertexCount; i++) {
149
+ this._transformIndices[response.offset + i] = splatIndex;
150
+ }
151
+
152
+ this._data.set(response.data, response.offset * 8);
153
+ splat.data.reattach(
154
+ response.positions,
155
+ response.rotations,
156
+ response.scales,
157
+ response.colors,
158
+ response.selection,
159
+ );
160
+
161
+ this._positions.set(response.worldPositions, response.offset * 3);
162
+ this._rotations.set(response.worldRotations, response.offset * 4);
163
+ this._scales.set(response.worldScales, response.offset * 3);
164
+
165
+ this._updating.delete(splat);
166
+
167
+ splat.selectedChanged = false;
168
+
169
+ this.dataChanged = true;
170
+ }
171
+ };
172
+
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ let wasmModule: any;
175
+
176
+ async function initWasm() {
177
+ wasmModule = await loadWasm();
178
+ }
179
+
180
+ initWasm();
181
+
182
+ async function waitForWasm() {
183
+ while (!wasmModule) {
184
+ await new Promise((resolve) => setTimeout(resolve, 0));
185
+ }
186
+ }
187
+
188
+ const buildImmediate = (splat: Splat) => {
189
+ if (!wasmModule) {
190
+ waitForWasm().then(() => {
191
+ buildImmediate(splat);
192
+ });
193
+ return;
194
+ }
195
+
196
+ updateTransform(splat);
197
+
198
+ const positionsPtr = wasmModule._malloc(3 * splat.data.vertexCount * 4);
199
+ const rotationsPtr = wasmModule._malloc(4 * splat.data.vertexCount * 4);
200
+ const scalesPtr = wasmModule._malloc(3 * splat.data.vertexCount * 4);
201
+ const colorsPtr = wasmModule._malloc(4 * splat.data.vertexCount);
202
+ const selectionPtr = wasmModule._malloc(splat.data.vertexCount);
203
+ const dataPtr = wasmModule._malloc(8 * splat.data.vertexCount * 4);
204
+ const worldPositionsPtr = wasmModule._malloc(3 * splat.data.vertexCount * 4);
205
+ const worldRotationsPtr = wasmModule._malloc(4 * splat.data.vertexCount * 4);
206
+ const worldScalesPtr = wasmModule._malloc(3 * splat.data.vertexCount * 4);
207
+
208
+ wasmModule.HEAPF32.set(splat.data.positions, positionsPtr / 4);
209
+ wasmModule.HEAPF32.set(splat.data.rotations, rotationsPtr / 4);
210
+ wasmModule.HEAPF32.set(splat.data.scales, scalesPtr / 4);
211
+ wasmModule.HEAPU8.set(splat.data.colors, colorsPtr);
212
+ wasmModule.HEAPU8.set(splat.data.selection, selectionPtr);
213
+
214
+ wasmModule._pack(
215
+ splat.selected,
216
+ splat.data.vertexCount,
217
+ positionsPtr,
218
+ rotationsPtr,
219
+ scalesPtr,
220
+ colorsPtr,
221
+ selectionPtr,
222
+ dataPtr,
223
+ worldPositionsPtr,
224
+ worldRotationsPtr,
225
+ worldScalesPtr,
226
+ );
227
+
228
+ const outData = new Uint32Array(wasmModule.HEAPU32.buffer, dataPtr, splat.data.vertexCount * 8);
229
+ const worldPositions = new Float32Array(
230
+ wasmModule.HEAPF32.buffer,
231
+ worldPositionsPtr,
232
+ splat.data.vertexCount * 3,
233
+ );
234
+ const worldRotations = new Float32Array(
235
+ wasmModule.HEAPF32.buffer,
236
+ worldRotationsPtr,
237
+ splat.data.vertexCount * 4,
238
+ );
239
+ const worldScales = new Float32Array(wasmModule.HEAPF32.buffer, worldScalesPtr, splat.data.vertexCount * 3);
240
+
241
+ const splatIndex = this._splatIndices.get(splat) as number;
242
+ const offset = this._offsets.get(splat) as number;
243
+ for (let i = 0; i < splat.data.vertexCount; i++) {
244
+ this._transformIndices[offset + i] = splatIndex;
245
+ }
246
+ this._data.set(outData, offset * 8);
247
+ this._positions.set(worldPositions, offset * 3);
248
+ this._rotations.set(worldRotations, offset * 4);
249
+ this._scales.set(worldScales, offset * 3);
250
+
251
+ wasmModule._free(positionsPtr);
252
+ wasmModule._free(rotationsPtr);
253
+ wasmModule._free(scalesPtr);
254
+ wasmModule._free(colorsPtr);
255
+ wasmModule._free(selectionPtr);
256
+ wasmModule._free(dataPtr);
257
+ wasmModule._free(worldPositionsPtr);
258
+ wasmModule._free(worldRotationsPtr);
259
+ wasmModule._free(worldScalesPtr);
260
+
261
+ this.dataChanged = true;
262
+ this.colorTransformsChanged = true;
263
+ };
264
+
265
+ const build = (splat: Splat) => {
266
+ if (splat.positionChanged || splat.rotationChanged || splat.scaleChanged || splat.selectedChanged) {
267
+ updateTransform(splat);
268
+ }
269
+
270
+ if (splat.colorTransformChanged) {
271
+ updateColorTransforms();
272
+ }
273
+
274
+ if (!splat.data.changed || splat.data.detached) return;
275
+
276
+ const serializedSplat = {
277
+ position: new Float32Array(splat.position.flat()),
278
+ rotation: new Float32Array(splat.rotation.flat()),
279
+ scale: new Float32Array(splat.scale.flat()),
280
+ selected: splat.selected,
281
+ vertexCount: splat.data.vertexCount,
282
+ positions: splat.data.positions,
283
+ rotations: splat.data.rotations,
284
+ scales: splat.data.scales,
285
+ colors: splat.data.colors,
286
+ selection: splat.data.selection,
287
+ offset: this._offsets.get(splat) as number,
288
+ };
289
+
290
+ this._worker.postMessage(
291
+ {
292
+ splat: serializedSplat,
293
+ },
294
+ [
295
+ serializedSplat.position.buffer,
296
+ serializedSplat.rotation.buffer,
297
+ serializedSplat.scale.buffer,
298
+ serializedSplat.positions.buffer,
299
+ serializedSplat.rotations.buffer,
300
+ serializedSplat.scales.buffer,
301
+ serializedSplat.colors.buffer,
302
+ serializedSplat.selection.buffer,
303
+ ],
304
+ );
305
+
306
+ this._updating.add(splat);
307
+
308
+ splat.data.detached = true;
309
+ };
310
+
311
+ this.getSplat = (index: number) => {
312
+ let splat = null;
313
+ for (const [key, value] of this._offsets) {
314
+ if (index >= value) {
315
+ splat = key;
316
+ } else {
317
+ break;
318
+ }
319
+ }
320
+ return splat;
321
+ };
322
+
323
+ this.getLocalIndex = (splat: Splat, index: number) => {
324
+ const offset = this._offsets.get(splat) as number;
325
+ return index - offset;
326
+ };
327
+
328
+ this.markDirty = (splat: Splat) => {
329
+ this._dirty.add(splat);
330
+ };
331
+
332
+ this.rebuild = () => {
333
+ for (const splat of this._dirty) {
334
+ build(splat);
335
+ }
336
+
337
+ this._dirty.clear();
338
+ };
339
+
340
+ this.dispose = () => {
341
+ this._worker.terminate();
342
+ };
343
+
344
+ for (const splat of this._splatIndices.keys()) {
345
+ buildImmediate(splat);
346
+ }
347
+
348
+ updateColorTransforms();
349
+ }
350
+
351
+ get offsets() {
352
+ return this._offsets;
353
+ }
354
+
355
+ get data() {
356
+ return this._data;
357
+ }
358
+
359
+ get width() {
360
+ return this._width;
361
+ }
362
+
363
+ get height() {
364
+ return this._height;
365
+ }
366
+
367
+ get transforms() {
368
+ return this._transforms;
369
+ }
370
+
371
+ get transformsWidth() {
372
+ return this._transformsWidth;
373
+ }
374
+
375
+ get transformsHeight() {
376
+ return this._transformsHeight;
377
+ }
378
+
379
+ get transformIndices() {
380
+ return this._transformIndices;
381
+ }
382
+
383
+ get transformIndicesWidth() {
384
+ return this._transformIndicesWidth;
385
+ }
386
+
387
+ get transformIndicesHeight() {
388
+ return this._transformIndicesHeight;
389
+ }
390
+
391
+ get colorTransforms() {
392
+ return this._colorTransforms;
393
+ }
394
+
395
+ get colorTransformsWidth() {
396
+ return this._colorTransformsWidth;
397
+ }
398
+
399
+ get colorTransformsHeight() {
400
+ return this._colorTransformsHeight;
401
+ }
402
+
403
+ get colorTransformIndices() {
404
+ return this._colorTransformIndices;
405
+ }
406
+
407
+ get colorTransformIndicesWidth() {
408
+ return this._colorTransformIndicesWidth;
409
+ }
410
+
411
+ get colorTransformIndicesHeight() {
412
+ return this._colorTransformIndicesHeight;
413
+ }
414
+
415
+ get positions() {
416
+ return this._positions;
417
+ }
418
+
419
+ get rotations() {
420
+ return this._rotations;
421
+ }
422
+
423
+ get scales() {
424
+ return this._scales;
425
+ }
426
+
427
+ get vertexCount() {
428
+ return this._vertexCount;
429
+ }
430
+
431
+ get needsRebuild() {
432
+ return this._dirty.size > 0;
433
+ }
434
+
435
+ get updating() {
436
+ return this._updating.size > 0;
437
+ }
438
+ }
439
+
440
+ export { RenderData };
src/renderers/webgl/utils/SortWorker.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import loadWasm from "../../../wasm/sort";
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ let wasmModule: any;
5
+
6
+ async function initWasm() {
7
+ wasmModule = await loadWasm();
8
+ }
9
+
10
+ let sortData: {
11
+ positions: Float32Array;
12
+ transforms: Float32Array;
13
+ transformIndices: Uint32Array;
14
+ vertexCount: number;
15
+ };
16
+
17
+ let viewProjPtr: number;
18
+ let transformsPtr: number;
19
+ let transformIndicesPtr: number;
20
+ let positionsPtr: number;
21
+ let depthBufferPtr: number;
22
+ let depthIndexPtr: number;
23
+ let startsPtr: number;
24
+ let countsPtr: number;
25
+
26
+ let allocatedVertexCount: number = 0;
27
+ let allocatedTransformCount: number = 0;
28
+ let viewProj: number[] = [];
29
+
30
+ let dirty = true;
31
+ let lock = false;
32
+ let allocationPending = false;
33
+ let sorting = false;
34
+
35
+ const allocateBuffers = async () => {
36
+ if (lock) {
37
+ allocationPending = true;
38
+ return;
39
+ }
40
+ lock = true;
41
+ allocationPending = false;
42
+
43
+ if (!wasmModule) await initWasm();
44
+
45
+ const targetAllocatedVertexCount = Math.pow(2, Math.ceil(Math.log2(sortData.vertexCount)));
46
+ if (allocatedVertexCount < targetAllocatedVertexCount) {
47
+ if (allocatedVertexCount > 0) {
48
+ wasmModule._free(viewProjPtr);
49
+ wasmModule._free(transformIndicesPtr);
50
+ wasmModule._free(positionsPtr);
51
+ wasmModule._free(depthBufferPtr);
52
+ wasmModule._free(depthIndexPtr);
53
+ wasmModule._free(startsPtr);
54
+ wasmModule._free(countsPtr);
55
+ }
56
+
57
+ allocatedVertexCount = targetAllocatedVertexCount;
58
+
59
+ viewProjPtr = wasmModule._malloc(16 * 4);
60
+ transformIndicesPtr = wasmModule._malloc(allocatedVertexCount * 4);
61
+ positionsPtr = wasmModule._malloc(3 * allocatedVertexCount * 4);
62
+ depthBufferPtr = wasmModule._malloc(allocatedVertexCount * 4);
63
+ depthIndexPtr = wasmModule._malloc(allocatedVertexCount * 4);
64
+ startsPtr = wasmModule._malloc(allocatedVertexCount * 4);
65
+ countsPtr = wasmModule._malloc(allocatedVertexCount * 4);
66
+ }
67
+
68
+ if (allocatedTransformCount < sortData.transforms.length) {
69
+ if (allocatedTransformCount > 0) {
70
+ wasmModule._free(transformsPtr);
71
+ }
72
+
73
+ allocatedTransformCount = sortData.transforms.length;
74
+
75
+ transformsPtr = wasmModule._malloc(allocatedTransformCount * 4);
76
+ }
77
+
78
+ lock = false;
79
+ if (allocationPending) {
80
+ allocationPending = false;
81
+ await allocateBuffers();
82
+ }
83
+ };
84
+
85
+ const runSort = () => {
86
+ if (lock || allocationPending || !wasmModule) return;
87
+ lock = true;
88
+
89
+ wasmModule.HEAPF32.set(sortData.positions, positionsPtr / 4);
90
+ wasmModule.HEAPF32.set(sortData.transforms, transformsPtr / 4);
91
+ wasmModule.HEAPU32.set(sortData.transformIndices, transformIndicesPtr / 4);
92
+ wasmModule.HEAPF32.set(new Float32Array(viewProj), viewProjPtr / 4);
93
+
94
+ wasmModule._sort(
95
+ viewProjPtr,
96
+ transformsPtr,
97
+ transformIndicesPtr,
98
+ sortData.vertexCount,
99
+ positionsPtr,
100
+ depthBufferPtr,
101
+ depthIndexPtr,
102
+ startsPtr,
103
+ countsPtr,
104
+ );
105
+
106
+ const depthIndex = new Uint32Array(wasmModule.HEAPU32.buffer, depthIndexPtr, sortData.vertexCount);
107
+ const detachedDepthIndex = new Uint32Array(depthIndex.slice().buffer);
108
+
109
+ self.postMessage({ depthIndex: detachedDepthIndex }, [detachedDepthIndex.buffer]);
110
+
111
+ lock = false;
112
+ dirty = false;
113
+ };
114
+
115
+ const throttledSort = () => {
116
+ if (!sorting) {
117
+ sorting = true;
118
+ if (dirty) runSort();
119
+
120
+ setTimeout(() => {
121
+ sorting = false;
122
+ throttledSort();
123
+ });
124
+ }
125
+ };
126
+
127
+ self.onmessage = (e) => {
128
+ if (e.data.sortData) {
129
+ //Recreating the typed arrays every time, will cause firefox to leak memory
130
+ if (!sortData) {
131
+ sortData = {
132
+ positions: new Float32Array(e.data.sortData.positions),
133
+ transforms: new Float32Array(e.data.sortData.transforms),
134
+ transformIndices: new Uint32Array(e.data.sortData.transformIndices),
135
+ vertexCount: e.data.sortData.vertexCount,
136
+ };
137
+ } else {
138
+ sortData.positions.set(e.data.sortData.positions);
139
+ sortData.transforms.set(e.data.sortData.transforms);
140
+ sortData.transformIndices.set(e.data.sortData.transformIndices);
141
+ sortData.vertexCount = e.data.sortData.vertexCount;
142
+ }
143
+
144
+ dirty = true;
145
+ allocateBuffers();
146
+ }
147
+ if (e.data.viewProj) {
148
+ if ((e.data.viewProj as number[]).every((item) => viewProj.includes(item)) === false) {
149
+ viewProj = e.data.viewProj;
150
+ dirty = true;
151
+ }
152
+
153
+ throttledSort();
154
+ }
155
+ };
src/splats/Splat.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SplatData } from "./SplatData";
2
+ import { Object3D } from "../core/Object3D";
3
+ import { Vector3 } from "../math/Vector3";
4
+ import { Quaternion } from "../math/Quaternion";
5
+ import { Converter } from "../utils/Converter";
6
+ import { Matrix4 } from "../math/Matrix4";
7
+ import { Box3 } from "../math/Box3";
8
+
9
+ class Splat extends Object3D {
10
+ public selectedChanged: boolean = false;
11
+ public colorTransformChanged: boolean = false;
12
+
13
+ private _data: SplatData;
14
+ private _selected: boolean = false;
15
+ private _colorTransforms: Array<Matrix4> = [];
16
+ private _colorTransformsMap: Map<number, number> = new Map();
17
+ private _bounds: Box3;
18
+
19
+ recalculateBounds: () => void;
20
+
21
+ constructor(splat: SplatData | undefined = undefined) {
22
+ super();
23
+
24
+ this._data = splat || new SplatData();
25
+ this._bounds = new Box3(
26
+ new Vector3(Infinity, Infinity, Infinity),
27
+ new Vector3(-Infinity, -Infinity, -Infinity),
28
+ );
29
+
30
+ this.recalculateBounds = () => {
31
+ this._bounds = new Box3(
32
+ new Vector3(Infinity, Infinity, Infinity),
33
+ new Vector3(-Infinity, -Infinity, -Infinity),
34
+ );
35
+ for (let i = 0; i < this._data.vertexCount; i++) {
36
+ this._bounds.expand(
37
+ new Vector3(
38
+ this._data.positions[3 * i],
39
+ this._data.positions[3 * i + 1],
40
+ this._data.positions[3 * i + 2],
41
+ ),
42
+ );
43
+ }
44
+ };
45
+
46
+ this.applyPosition = () => {
47
+ this.data.translate(this.position);
48
+ this.position = new Vector3();
49
+ };
50
+
51
+ this.applyRotation = () => {
52
+ this.data.rotate(this.rotation);
53
+ this.rotation = new Quaternion();
54
+ };
55
+
56
+ this.applyScale = () => {
57
+ this.data.scale(this.scale);
58
+ this.scale = new Vector3(1, 1, 1);
59
+ };
60
+
61
+ this.recalculateBounds();
62
+ }
63
+
64
+ saveToFile(name: string | null = null, format: "splat" | "ply" = "splat") {
65
+ if (!document) return;
66
+
67
+ if (!name) {
68
+ const now = new Date();
69
+ name = `splat-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.${format}`;
70
+ }
71
+
72
+ const splatClone = this.clone();
73
+
74
+ splatClone.applyRotation();
75
+ splatClone.applyScale();
76
+ splatClone.applyPosition();
77
+
78
+ const data = splatClone.data.serialize();
79
+ let blob;
80
+ if (format === "ply") {
81
+ const plyData = Converter.SplatToPLY(data.buffer, splatClone.data.vertexCount);
82
+ blob = new Blob([plyData], { type: "application/octet-stream" });
83
+ } else {
84
+ blob = new Blob([data.buffer], { type: "application/octet-stream" });
85
+ }
86
+
87
+ const link = document.createElement("a");
88
+ link.download = name;
89
+ link.href = URL.createObjectURL(blob);
90
+ link.click();
91
+ }
92
+
93
+ get data() {
94
+ return this._data;
95
+ }
96
+
97
+ get selected() {
98
+ return this._selected;
99
+ }
100
+
101
+ set selected(selected: boolean) {
102
+ if (this._selected !== selected) {
103
+ this._selected = selected;
104
+ this.selectedChanged = true;
105
+ this.dispatchEvent(this._changeEvent);
106
+ }
107
+ }
108
+
109
+ get colorTransforms() {
110
+ return this._colorTransforms;
111
+ }
112
+
113
+ get colorTransformsMap() {
114
+ return this._colorTransformsMap;
115
+ }
116
+
117
+ get bounds() {
118
+ let center = this._bounds.center();
119
+ center = center.add(this.position);
120
+
121
+ let size = this._bounds.size();
122
+ size = size.multiply(this.scale);
123
+
124
+ return new Box3(center.subtract(size.divide(2)), center.add(size.divide(2)));
125
+ }
126
+
127
+ clone() {
128
+ const splat = new Splat(this.data.clone());
129
+ splat.position = this.position.clone();
130
+ splat.rotation = this.rotation.clone();
131
+ splat.scale = this.scale.clone();
132
+ return splat;
133
+ }
134
+ }
135
+
136
+ export { Splat };
src/splats/SplatData.ts ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Vector3 } from "../math/Vector3";
2
+ import { Quaternion } from "../math/Quaternion";
3
+ import { Matrix3 } from "../math/Matrix3";
4
+
5
+ class SplatData {
6
+ static RowLength = 3 * 4 + 3 * 4 + 4 + 4;
7
+
8
+ public changed = false;
9
+ public detached = false;
10
+
11
+ private _vertexCount: number;
12
+ private _positions: Float32Array;
13
+ private _rotations: Float32Array;
14
+ private _scales: Float32Array;
15
+ private _colors: Uint8Array;
16
+ private _selection: Uint8Array;
17
+
18
+ translate: (translation: Vector3) => void;
19
+ rotate: (rotation: Quaternion) => void;
20
+ scale: (scale: Vector3) => void;
21
+ serialize: () => Uint8Array;
22
+ reattach: (
23
+ positions: ArrayBufferLike,
24
+ rotations: ArrayBufferLike,
25
+ scales: ArrayBufferLike,
26
+ colors: ArrayBufferLike,
27
+ selection: ArrayBufferLike,
28
+ ) => void;
29
+
30
+ constructor(
31
+ vertexCount: number = 0,
32
+ positions: Float32Array | null = null,
33
+ rotations: Float32Array | null = null,
34
+ scales: Float32Array | null = null,
35
+ colors: Uint8Array | null = null,
36
+ ) {
37
+ this._vertexCount = vertexCount;
38
+ this._positions = positions || new Float32Array(0);
39
+ this._rotations = rotations || new Float32Array(0);
40
+ this._scales = scales || new Float32Array(0);
41
+ this._colors = colors || new Uint8Array(0);
42
+ this._selection = new Uint8Array(this.vertexCount);
43
+
44
+ this.translate = (translation: Vector3) => {
45
+ for (let i = 0; i < this.vertexCount; i++) {
46
+ this.positions[3 * i + 0] += translation.x;
47
+ this.positions[3 * i + 1] += translation.y;
48
+ this.positions[3 * i + 2] += translation.z;
49
+ }
50
+
51
+ this.changed = true;
52
+ };
53
+
54
+ this.rotate = (rotation: Quaternion) => {
55
+ const R = Matrix3.RotationFromQuaternion(rotation).buffer;
56
+ for (let i = 0; i < this.vertexCount; i++) {
57
+ const x = this.positions[3 * i + 0];
58
+ const y = this.positions[3 * i + 1];
59
+ const z = this.positions[3 * i + 2];
60
+
61
+ this.positions[3 * i + 0] = R[0] * x + R[1] * y + R[2] * z;
62
+ this.positions[3 * i + 1] = R[3] * x + R[4] * y + R[5] * z;
63
+ this.positions[3 * i + 2] = R[6] * x + R[7] * y + R[8] * z;
64
+
65
+ const currentRotation = new Quaternion(
66
+ this.rotations[4 * i + 1],
67
+ this.rotations[4 * i + 2],
68
+ this.rotations[4 * i + 3],
69
+ this.rotations[4 * i + 0],
70
+ );
71
+
72
+ const newRot = rotation.multiply(currentRotation);
73
+ this.rotations[4 * i + 1] = newRot.x;
74
+ this.rotations[4 * i + 2] = newRot.y;
75
+ this.rotations[4 * i + 3] = newRot.z;
76
+ this.rotations[4 * i + 0] = newRot.w;
77
+ }
78
+
79
+ this.changed = true;
80
+ };
81
+
82
+ this.scale = (scale: Vector3) => {
83
+ for (let i = 0; i < this.vertexCount; i++) {
84
+ this.positions[3 * i + 0] *= scale.x;
85
+ this.positions[3 * i + 1] *= scale.y;
86
+ this.positions[3 * i + 2] *= scale.z;
87
+
88
+ this.scales[3 * i + 0] *= scale.x;
89
+ this.scales[3 * i + 1] *= scale.y;
90
+ this.scales[3 * i + 2] *= scale.z;
91
+ }
92
+
93
+ this.changed = true;
94
+ };
95
+
96
+ this.serialize = () => {
97
+ const data = new Uint8Array(this.vertexCount * SplatData.RowLength);
98
+
99
+ const f_buffer = new Float32Array(data.buffer);
100
+ const u_buffer = new Uint8Array(data.buffer);
101
+
102
+ for (let i = 0; i < this.vertexCount; i++) {
103
+ f_buffer[8 * i + 0] = this.positions[3 * i + 0];
104
+ f_buffer[8 * i + 1] = this.positions[3 * i + 1];
105
+ f_buffer[8 * i + 2] = this.positions[3 * i + 2];
106
+
107
+ u_buffer[32 * i + 24 + 0] = this.colors[4 * i + 0];
108
+ u_buffer[32 * i + 24 + 1] = this.colors[4 * i + 1];
109
+ u_buffer[32 * i + 24 + 2] = this.colors[4 * i + 2];
110
+ u_buffer[32 * i + 24 + 3] = this.colors[4 * i + 3];
111
+
112
+ f_buffer[8 * i + 3 + 0] = this.scales[3 * i + 0];
113
+ f_buffer[8 * i + 3 + 1] = this.scales[3 * i + 1];
114
+ f_buffer[8 * i + 3 + 2] = this.scales[3 * i + 2];
115
+
116
+ u_buffer[32 * i + 28 + 0] = (this.rotations[4 * i + 0] * 128 + 128) & 0xff;
117
+ u_buffer[32 * i + 28 + 1] = (this.rotations[4 * i + 1] * 128 + 128) & 0xff;
118
+ u_buffer[32 * i + 28 + 2] = (this.rotations[4 * i + 2] * 128 + 128) & 0xff;
119
+ u_buffer[32 * i + 28 + 3] = (this.rotations[4 * i + 3] * 128 + 128) & 0xff;
120
+ }
121
+
122
+ return data;
123
+ };
124
+
125
+ this.reattach = (
126
+ positions: ArrayBufferLike,
127
+ rotations: ArrayBufferLike,
128
+ scales: ArrayBufferLike,
129
+ colors: ArrayBufferLike,
130
+ selection: ArrayBufferLike,
131
+ ) => {
132
+ console.assert(
133
+ positions.byteLength === this.vertexCount * 3 * 4,
134
+ `Expected ${this.vertexCount * 3 * 4} bytes, got ${positions.byteLength} bytes`,
135
+ );
136
+ this._positions = new Float32Array(positions);
137
+ this._rotations = new Float32Array(rotations);
138
+ this._scales = new Float32Array(scales);
139
+ this._colors = new Uint8Array(colors);
140
+ this._selection = new Uint8Array(selection);
141
+ this.detached = false;
142
+ };
143
+ }
144
+
145
+ static Deserialize(data: Uint8Array): SplatData {
146
+ const vertexCount = data.length / SplatData.RowLength;
147
+ const positions = new Float32Array(3 * vertexCount);
148
+ const rotations = new Float32Array(4 * vertexCount);
149
+ const scales = new Float32Array(3 * vertexCount);
150
+ const colors = new Uint8Array(4 * vertexCount);
151
+
152
+ const f_buffer = new Float32Array(data.buffer);
153
+ const u_buffer = new Uint8Array(data.buffer);
154
+
155
+ for (let i = 0; i < vertexCount; i++) {
156
+ positions[3 * i + 0] = f_buffer[8 * i + 0];
157
+ positions[3 * i + 1] = f_buffer[8 * i + 1];
158
+ positions[3 * i + 2] = f_buffer[8 * i + 2];
159
+
160
+ rotations[4 * i + 0] = (u_buffer[32 * i + 28 + 0] - 128) / 128;
161
+ rotations[4 * i + 1] = (u_buffer[32 * i + 28 + 1] - 128) / 128;
162
+ rotations[4 * i + 2] = (u_buffer[32 * i + 28 + 2] - 128) / 128;
163
+ rotations[4 * i + 3] = (u_buffer[32 * i + 28 + 3] - 128) / 128;
164
+
165
+ scales[3 * i + 0] = f_buffer[8 * i + 3 + 0];
166
+ scales[3 * i + 1] = f_buffer[8 * i + 3 + 1];
167
+ scales[3 * i + 2] = f_buffer[8 * i + 3 + 2];
168
+
169
+ colors[4 * i + 0] = u_buffer[32 * i + 24 + 0];
170
+ colors[4 * i + 1] = u_buffer[32 * i + 24 + 1];
171
+ colors[4 * i + 2] = u_buffer[32 * i + 24 + 2];
172
+ colors[4 * i + 3] = u_buffer[32 * i + 24 + 3];
173
+ }
174
+
175
+ return new SplatData(vertexCount, positions, rotations, scales, colors);
176
+ }
177
+
178
+ get vertexCount() {
179
+ return this._vertexCount;
180
+ }
181
+
182
+ get positions() {
183
+ return this._positions;
184
+ }
185
+
186
+ get rotations() {
187
+ return this._rotations;
188
+ }
189
+
190
+ get scales() {
191
+ return this._scales;
192
+ }
193
+
194
+ get colors() {
195
+ return this._colors;
196
+ }
197
+
198
+ get selection() {
199
+ return this._selection;
200
+ }
201
+
202
+ clone() {
203
+ return new SplatData(
204
+ this.vertexCount,
205
+ new Float32Array(this.positions),
206
+ new Float32Array(this.rotations),
207
+ new Float32Array(this.scales),
208
+ new Uint8Array(this.colors),
209
+ );
210
+ }
211
+ }
212
+
213
+ export { SplatData };
src/splats/Splatv.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Object3D } from "../core/Object3D";
2
+ import { SplatvData } from "./SplatvData";
3
+
4
+ class Splatv extends Object3D {
5
+ private _data: SplatvData;
6
+
7
+ constructor(splat: SplatvData) {
8
+ super();
9
+
10
+ this._data = splat;
11
+ }
12
+
13
+ get data() {
14
+ return this._data;
15
+ }
16
+ }
17
+
18
+ export { Splatv };
src/splats/SplatvData.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class SplatvData {
2
+ static RowLength = 64;
3
+
4
+ private _vertexCount: number;
5
+ private _positions: Float32Array;
6
+ private _data: Uint32Array;
7
+ private _width: number;
8
+ private _height: number;
9
+
10
+ serialize: () => Uint8Array;
11
+
12
+ constructor(vertexCount: number, positions: Float32Array, data: Uint32Array, width: number, height: number) {
13
+ this._vertexCount = vertexCount;
14
+ this._positions = positions;
15
+ this._data = data;
16
+ this._width = width;
17
+ this._height = height;
18
+
19
+ this.serialize = () => {
20
+ return new Uint8Array(this._data.buffer);
21
+ };
22
+ }
23
+
24
+ static Deserialize(data: Uint8Array, width: number, height: number): SplatvData {
25
+ const buffer = new Uint32Array(data.buffer);
26
+ const f_buffer = new Float32Array(data.buffer);
27
+ const vertexCount = Math.floor(f_buffer.byteLength / this.RowLength);
28
+ const positions = new Float32Array(vertexCount * 3);
29
+ for (let i = 0; i < vertexCount; i++) {
30
+ positions[3 * i + 0] = f_buffer[16 * i + 0];
31
+ positions[3 * i + 1] = f_buffer[16 * i + 1];
32
+ positions[3 * i + 2] = f_buffer[16 * i + 2];
33
+ positions[3 * i + 0] = f_buffer[16 * i + 3];
34
+ }
35
+ return new SplatvData(vertexCount, positions, buffer, width, height);
36
+ }
37
+
38
+ get vertexCount() {
39
+ return this._vertexCount;
40
+ }
41
+
42
+ get positions() {
43
+ return this._positions;
44
+ }
45
+
46
+ get data() {
47
+ return this._data;
48
+ }
49
+
50
+ get width() {
51
+ return this._width;
52
+ }
53
+
54
+ get height() {
55
+ return this._height;
56
+ }
57
+ }
58
+
59
+ export { SplatvData };
src/utils/Converter.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Quaternion } from "../math/Quaternion";
2
+
3
+ class Converter {
4
+ public static SH_C0 = 0.28209479177387814;
5
+
6
+ public static SplatToPLY(buffer: ArrayBuffer, vertexCount: number): ArrayBuffer {
7
+ let header = "ply\nformat binary_little_endian 1.0\n";
8
+ header += `element vertex ${vertexCount}\n`;
9
+
10
+ const properties = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2"];
11
+ for (let i = 0; i < 45; i++) {
12
+ properties.push(`f_rest_${i}`);
13
+ }
14
+ properties.push("opacity");
15
+ properties.push("scale_0");
16
+ properties.push("scale_1");
17
+ properties.push("scale_2");
18
+ properties.push("rot_0");
19
+ properties.push("rot_1");
20
+ properties.push("rot_2");
21
+ properties.push("rot_3");
22
+
23
+ for (const property of properties) {
24
+ header += `property float ${property}\n`;
25
+ }
26
+ header += "end_header\n";
27
+
28
+ const headerBuffer = new TextEncoder().encode(header);
29
+
30
+ const plyRowLength = 4 * 3 + 4 * 3 + 4 * 3 + 4 * 45 + 4 + 4 * 3 + 4 * 4;
31
+ const plyLength = vertexCount * plyRowLength;
32
+ const output = new DataView(new ArrayBuffer(headerBuffer.length + plyLength));
33
+ new Uint8Array(output.buffer).set(headerBuffer, 0);
34
+
35
+ const f_buffer = new Float32Array(buffer);
36
+ const u_buffer = new Uint8Array(buffer);
37
+
38
+ const offset = headerBuffer.length;
39
+ const f_dc_offset = 4 * 3 + 4 * 3;
40
+ const opacity_offset = f_dc_offset + 4 * 3 + 4 * 45;
41
+ const scale_offset = opacity_offset + 4;
42
+ const rot_offset = scale_offset + 4 * 3;
43
+ for (let i = 0; i < vertexCount; i++) {
44
+ const pos0 = f_buffer[8 * i + 0];
45
+ const pos1 = f_buffer[8 * i + 1];
46
+ const pos2 = f_buffer[8 * i + 2];
47
+
48
+ const f_dc_0 = (u_buffer[32 * i + 24 + 0] / 255 - 0.5) / this.SH_C0;
49
+ const f_dc_1 = (u_buffer[32 * i + 24 + 1] / 255 - 0.5) / this.SH_C0;
50
+ const f_dc_2 = (u_buffer[32 * i + 24 + 2] / 255 - 0.5) / this.SH_C0;
51
+
52
+ const alpha = u_buffer[32 * i + 24 + 3] / 255;
53
+ const opacity = Math.log(alpha / (1 - alpha));
54
+
55
+ const scale0 = Math.log(f_buffer[8 * i + 3 + 0]);
56
+ const scale1 = Math.log(f_buffer[8 * i + 3 + 1]);
57
+ const scale2 = Math.log(f_buffer[8 * i + 3 + 2]);
58
+
59
+ let q = new Quaternion(
60
+ (u_buffer[32 * i + 28 + 1] - 128) / 128,
61
+ (u_buffer[32 * i + 28 + 2] - 128) / 128,
62
+ (u_buffer[32 * i + 28 + 3] - 128) / 128,
63
+ (u_buffer[32 * i + 28 + 0] - 128) / 128,
64
+ );
65
+ q = q.normalize();
66
+
67
+ const rot0 = q.w;
68
+ const rot1 = q.x;
69
+ const rot2 = q.y;
70
+ const rot3 = q.z;
71
+
72
+ output.setFloat32(offset + plyRowLength * i + 0, pos0, true);
73
+ output.setFloat32(offset + plyRowLength * i + 4, pos1, true);
74
+ output.setFloat32(offset + plyRowLength * i + 8, pos2, true);
75
+
76
+ output.setFloat32(offset + plyRowLength * i + f_dc_offset + 0, f_dc_0, true);
77
+ output.setFloat32(offset + plyRowLength * i + f_dc_offset + 4, f_dc_1, true);
78
+ output.setFloat32(offset + plyRowLength * i + f_dc_offset + 8, f_dc_2, true);
79
+
80
+ output.setFloat32(offset + plyRowLength * i + opacity_offset, opacity, true);
81
+
82
+ output.setFloat32(offset + plyRowLength * i + scale_offset + 0, scale0, true);
83
+ output.setFloat32(offset + plyRowLength * i + scale_offset + 4, scale1, true);
84
+ output.setFloat32(offset + plyRowLength * i + scale_offset + 8, scale2, true);
85
+
86
+ output.setFloat32(offset + plyRowLength * i + rot_offset + 0, rot0, true);
87
+ output.setFloat32(offset + plyRowLength * i + rot_offset + 4, rot1, true);
88
+ output.setFloat32(offset + plyRowLength * i + rot_offset + 8, rot2, true);
89
+ output.setFloat32(offset + plyRowLength * i + rot_offset + 12, rot3, true);
90
+ }
91
+
92
+ return output.buffer;
93
+ }
94
+ }
95
+
96
+ export { Converter };
src/utils/LoaderUtils.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function initiateFetchRequest(url: string, useCache: boolean): Promise<Response> {
2
+ const req = await fetch(url, {
3
+ mode: "cors",
4
+ credentials: "omit",
5
+ cache: useCache ? "force-cache" : "default",
6
+ });
7
+
8
+ if (req.status != 200) {
9
+ throw new Error(req.status + " Unable to load " + req.url);
10
+ }
11
+
12
+ return req;
13
+ }
14
+
15
+ export async function loadDataIntoBuffer(res: Response, onProgress?: (progress: number) => void): Promise<Uint8Array> {
16
+ const reader = res.body!.getReader();
17
+
18
+ const contentLength = parseInt(res.headers.get("content-length") as string);
19
+ const buffer = new Uint8Array(contentLength);
20
+
21
+ let bytesRead = 0;
22
+
23
+ // eslint-disable-next-line no-constant-condition
24
+ while (true) {
25
+ const { done, value } = await reader.read();
26
+ if (done) break;
27
+
28
+ buffer.set(value, bytesRead);
29
+ bytesRead += value.length;
30
+ onProgress?.(bytesRead / contentLength);
31
+ }
32
+
33
+ return buffer;
34
+ }
35
+
36
+ export async function loadChunkedDataIntoBuffer(
37
+ res: Response,
38
+ onProgress?: (progress: number) => void,
39
+ ): Promise<Uint8Array> {
40
+ const reader = res.body!.getReader();
41
+
42
+ const chunks = [];
43
+ let receivedLength = 0;
44
+ // eslint-disable-next-line no-constant-condition
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+
49
+ chunks.push(value);
50
+ receivedLength += value.length;
51
+ }
52
+
53
+ const buffer = new Uint8Array(receivedLength);
54
+ let position = 0;
55
+ for (const chunk of chunks) {
56
+ buffer.set(chunk, position);
57
+ position += chunk.length;
58
+
59
+ onProgress?.(position / receivedLength);
60
+ }
61
+
62
+ return buffer;
63
+ }
64
+
65
+ export async function loadRequestDataIntoBuffer(
66
+ res: Response,
67
+ onProgress?: (progress: number) => void,
68
+ ): Promise<Uint8Array> {
69
+ if (res.headers.has("content-length")) {
70
+ return loadDataIntoBuffer(res, onProgress);
71
+ } else {
72
+ return loadChunkedDataIntoBuffer(res, onProgress);
73
+ }
74
+ }
src/wasm/data.d.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface WasmModule {
2
+ _malloc(size: number): number;
3
+ _free(ptr: number): void;
4
+ _pack(
5
+ selected: boolean,
6
+ vertexCount: number,
7
+ positions: number,
8
+ rotations: number,
9
+ scales: number,
10
+ colors: number,
11
+ selection: number,
12
+ data: number,
13
+ worldPositions: number,
14
+ worldRotations: number,
15
+ worldScales: number,
16
+ ): void;
17
+ }
18
+
19
+ declare const loadWasm: () => Promise<WasmModule>;
20
+ export default loadWasm;