Spaces:
Running
Running
Upload 56 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .eslintrc.json +21 -0
- .gitattributes +1 -35
- .gitignore +134 -0
- .prettierrc +5 -0
- LICENSE +21 -0
- README.md +106 -12
- compile_wasm.sh +18 -0
- jest.config.js +5 -0
- package-lock.json +0 -0
- package.json +58 -0
- rollup.config.js +27 -0
- src/cameras/Camera.ts +42 -0
- src/cameras/CameraData.ts +127 -0
- src/controls/FPSControls.ts +122 -0
- src/controls/OrbitControls.ts +310 -0
- src/core/Object3D.ts +101 -0
- src/core/Scene.ts +117 -0
- src/custom.d.ts +6 -0
- src/events/EventDispatcher.ts +46 -0
- src/events/Events.ts +21 -0
- src/index.ts +28 -0
- src/loaders/Loader.ts +46 -0
- src/loaders/PLYLoader.ts +223 -0
- src/loaders/SplatvLoader.ts +135 -0
- src/math/BVH.ts +56 -0
- src/math/Box3.ts +52 -0
- src/math/Color32.ts +36 -0
- src/math/Matrix3.ts +110 -0
- src/math/Matrix4.ts +176 -0
- src/math/Plane.ts +29 -0
- src/math/Quaternion.ts +177 -0
- src/math/Vector3.ts +163 -0
- src/math/Vector4.ts +111 -0
- src/renderers/WebGLRenderer.ts +110 -0
- src/renderers/webgl/passes/FadeInPass.ts +50 -0
- src/renderers/webgl/passes/ShaderPass.ts +10 -0
- src/renderers/webgl/programs/RenderProgram.ts +574 -0
- src/renderers/webgl/programs/ShaderProgram.ts +136 -0
- src/renderers/webgl/programs/VideoRenderProgram.ts +378 -0
- src/renderers/webgl/utils/DataWorker.ts +163 -0
- src/renderers/webgl/utils/IntersectionTester.ts +87 -0
- src/renderers/webgl/utils/RenderData.ts +440 -0
- src/renderers/webgl/utils/SortWorker.ts +155 -0
- src/splats/Splat.ts +136 -0
- src/splats/SplatData.ts +213 -0
- src/splats/Splatv.ts +18 -0
- src/splats/SplatvData.ts +59 -0
- src/utils/Converter.ts +96 -0
- src/utils/LoaderUtils.ts +74 -0
- 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 |
-
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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;
|