chore: squash and init public branch
32 files changed, 5192 insertions(+), 0 deletions(-) | |||
---|---|---|---|
A | .gitignore | +25 | -0 |
A | README.md | +38 | -0 |
A | index.css | +44 | -0 |
A | index.html | +18 | -0 |
A | package-lock.json | +1262 | -0 |
A | package.json | +27 | -0 |
A | public/Duck.gltf | +219 | -0 |
A | public/Duck0.bin | +0 | -0 |
A | public/DuckCM.png | +0 | -0 |
A | public/EnvironmentTest.gltf | +328 | -0 |
A | public/EnvironmentTest_binary.bin | +0 | -0 |
A | public/LDR_RGBA_0.png | +0 | -0 |
A | public/cornell_empty_rg.bin | +0 | -0 |
A | public/cornell_empty_rg.gltf | +445 | -0 |
A | public/roughness_metallic_0.jpg | +0 | -0 |
A | public/roughness_metallic_1.jpg | +0 | -0 |
A | src/bvh.ts | +331 | -0 |
A | src/camera.ts | +139 | -0 |
A | src/gltf.ts | +394 | -0 |
A | src/main.ts | +643 | -0 |
A | src/pane.ts | +142 | -0 |
A | src/shaders/any_hit.wgsl | +138 | -0 |
A | src/shaders/brdf.wgsl | +154 | -0 |
A | src/shaders/main.wgsl | +593 | -0 |
A | src/shaders/random.wgsl | +34 | -0 |
A | src/shaders/sky.wgsl | +67 | -0 |
A | src/shaders/types.d.ts | +4 | -0 |
A | src/shaders/utils.wgsl | +37 | -0 |
A | src/shaders/viewport.wgsl | +68 | -0 |
A | src/vite-env.d.ts | +2 | -0 |
A | tsconfig.json | +25 | -0 |
A | vite.config.ts | +15 | -0 |
1@@ -0,0 +1,25 @@
2+# Logs
3+logs
4+*.log
5+npm-debug.log*
6+yarn-debug.log*
7+yarn-error.log*
8+pnpm-debug.log*
9+lerna-debug.log*
10+
11+node_modules
12+dist
13+dist-ssr
14+*.local
15+
16+# Editor directories and files
17+.vscode/*
18+!.vscode/extensions.json
19+.idea
20+.DS_Store
21+*.suo
22+*.ntvs*
23+*.njsproj
24+*.sln
25+*.sw?
26+.vs
A · README.md
+38, -0 1@@ -0,0 +1,38 @@
2+### Overview
3+
4+An interactive path tracer impemented in WGSL. Supports multiple sampling methods, physically based materials including microfacets, and realistic light sources. Primarily written to explore WGSL and the WebGPU API. So it takes some shortcuts and is pretty straightforward.
5+
6+This is a GPU "software" path tracer, since there is no hw-accel using RT cores, it contains manual scene intersections and hit tests.
7+
8+- Single megakernel compute pass, that blits the output to a viewport quad texture
9+- A singele static SAH split BVH is constructed on the host
10+- Expects GLTF models, since the base specification for textures and PBR mapped pretty nicely to my goals here. So roughness, metallic, emission, and albedo. Textures and the normal map
11+- A BRDF for these material properties, pretty bog standard. Cosine weighted hemisphere sample for the rougher materials and a GGX distribution based lobe for the specular. A naive 0.5 mix on the to for the material lobe selection and PDF.
12+- There is no support for transmission, ior, or alpha textures
13+- Balanced heuristic based multiple importance sampling along with two NEE rays. One for direct emissives and another for the sun
14+- Uses stratified animaited blue noise for all the screen space level sampling and faster resolves.
15+- Contains a free cam and mouselook, typical `[W][A][S][D]` and `[Q][E]` for +Z, -Z respectively. `[SHIFT]` for a speed up.
16+
17+### Local setup
18+```
19+npm install
20+npm run dev
21+```
22+`/public` should contain the assets. Just compose the scene manually in `main.ts` position, scale, rotate.
23+
24+### To-do
25+- Direct lighting NEE from an HDR equirectangular map
26+
27+### Resources
28+- [WebGPU specification](https://www.w3.org/TR/webgpu/)
29+- [WGSL Specification](https://www.w3.org/TR/WGSL/)
30+- **Jacob Bikker:** [Invaluable BVH resource](https://jacco.ompf2.com/about-me/)
31+- **Christoph Peters:** [Math for importance sampling](https://momentsingraphics.de/)
32+- **Jakub Boksansky:** [Crash Course in BRDF Implementation](https://boksajak.github.io/files/CrashCourseBRDF.pdf)
33+- **Brent Burley:** [Physically Based Shading at Disney](https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
34+- **Möller–Trumbore:** [Ray triangle intersection test](http://www.graphics.cornell.edu/pubs/1997/MT97.pdf)
35+- **Pixar ONB:** [Building an Orthonormal Basis, Revisited
36+](https://www.jcgt.org/published/0006/01/01/paper-lowres.pdf)
37+- **Uncharted 2 tonemap:** [Uncharted 2: HDR Lighting](https://www.gdcvault.com/play/1012351/Uncharted-2-HDR)
38+- **Frostbite BRDF:** [Moving Frostbite to Physically Based Rendering 2.0](https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/course-notes-moving-frostbite-to-pbr-v2.pdf)
39+- **Reference Books**: Ray tracing Gems 1 and 2, Physically based rendering 4.0.
A · index.css
+44, -0 1@@ -0,0 +1,44 @@
2+html,
3+body {
4+ --bs-br: 0px !important;
5+ padding: 0;
6+ height: 100%;
7+ margin: 0;
8+ font-family: 'Courier New', Courier, monospace;
9+ background-color: rgb(82, 82, 82);
10+ overflow: hidden;
11+}
12+
13+.tp-rotv, .tp-rotv * {
14+ border-radius: 0px !important;
15+}
16+
17+#render {
18+ display: block;
19+ position: absolute;
20+ top: 50%;
21+ left: 50%;
22+
23+ transform: translate(-50%, -50%);
24+ margin: 0;
25+ border-radius: 8px;
26+}
27+
28+#pane-container {
29+ position: absolute;
30+ z-index: 9;
31+ background-color:hsl(230, 7%, 17%);
32+ color: #c8cad0;
33+ border: 1px solid black;
34+ border-radius: 8px;
35+ overflow: clip;
36+ text-align: center;
37+}
38+
39+#pane-container-header {
40+ font-size: 12px;
41+ font-weight: 600;
42+ padding: 1px ;
43+ cursor: move;
44+ z-index: 10;
45+}
A · index.html
+18, -0 1@@ -0,0 +1,18 @@
2+<!doctype html>
3+<html lang="en">
4+ <head>
5+ <meta charset="UTF-8" />
6+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+ <link rel="stylesheet" type="text/css" href="index.css" />
8+ <script type="module" src="src/main.ts"></script>
9+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10+ <title>Vite + TS</title>
11+ </head>
12+ <body>
13+ <div id="pane-container">
14+ <div id="pane-container-header">Controls</div>
15+ </div>
16+ <canvas id="render" width="800" height="600"></canvas>
17+
18+</body>
19+</html>
A · package-lock.json
+1262, -0 1@@ -0,0 +1,1262 @@
2+{
3+ "name": "vite-project",
4+ "version": "0.0.0",
5+ "lockfileVersion": 3,
6+ "requires": true,
7+ "packages": {
8+ "": {
9+ "name": "vite-project",
10+ "version": "0.0.0",
11+ "dependencies": {
12+ "@loaders.gl/core": "4.3.3",
13+ "@loaders.gl/gltf": "4.3.3",
14+ "gl-matrix": "^4.0.0-beta.2",
15+ "tweakpane": "^4.0.5"
16+ },
17+ "devDependencies": {
18+ "@tweakpane/core": "^2.0.5",
19+ "@webgpu/types": "^0.1.53",
20+ "typescript": "~5.6.2",
21+ "vite": "^6.0.5",
22+ "vite-plugin-glsl": "^1.4.0"
23+ }
24+ },
25+ "node_modules/@esbuild/aix-ppc64": {
26+ "version": "0.24.2",
27+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
28+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
29+ "cpu": [
30+ "ppc64"
31+ ],
32+ "dev": true,
33+ "license": "MIT",
34+ "optional": true,
35+ "os": [
36+ "aix"
37+ ],
38+ "engines": {
39+ "node": ">=18"
40+ }
41+ },
42+ "node_modules/@esbuild/android-arm": {
43+ "version": "0.24.2",
44+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
45+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
46+ "cpu": [
47+ "arm"
48+ ],
49+ "dev": true,
50+ "license": "MIT",
51+ "optional": true,
52+ "os": [
53+ "android"
54+ ],
55+ "engines": {
56+ "node": ">=18"
57+ }
58+ },
59+ "node_modules/@esbuild/android-arm64": {
60+ "version": "0.24.2",
61+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
62+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
63+ "cpu": [
64+ "arm64"
65+ ],
66+ "dev": true,
67+ "license": "MIT",
68+ "optional": true,
69+ "os": [
70+ "android"
71+ ],
72+ "engines": {
73+ "node": ">=18"
74+ }
75+ },
76+ "node_modules/@esbuild/android-x64": {
77+ "version": "0.24.2",
78+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
79+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
80+ "cpu": [
81+ "x64"
82+ ],
83+ "dev": true,
84+ "license": "MIT",
85+ "optional": true,
86+ "os": [
87+ "android"
88+ ],
89+ "engines": {
90+ "node": ">=18"
91+ }
92+ },
93+ "node_modules/@esbuild/darwin-arm64": {
94+ "version": "0.24.2",
95+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
96+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
97+ "cpu": [
98+ "arm64"
99+ ],
100+ "dev": true,
101+ "license": "MIT",
102+ "optional": true,
103+ "os": [
104+ "darwin"
105+ ],
106+ "engines": {
107+ "node": ">=18"
108+ }
109+ },
110+ "node_modules/@esbuild/darwin-x64": {
111+ "version": "0.24.2",
112+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
113+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
114+ "cpu": [
115+ "x64"
116+ ],
117+ "dev": true,
118+ "license": "MIT",
119+ "optional": true,
120+ "os": [
121+ "darwin"
122+ ],
123+ "engines": {
124+ "node": ">=18"
125+ }
126+ },
127+ "node_modules/@esbuild/freebsd-arm64": {
128+ "version": "0.24.2",
129+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
130+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
131+ "cpu": [
132+ "arm64"
133+ ],
134+ "dev": true,
135+ "license": "MIT",
136+ "optional": true,
137+ "os": [
138+ "freebsd"
139+ ],
140+ "engines": {
141+ "node": ">=18"
142+ }
143+ },
144+ "node_modules/@esbuild/freebsd-x64": {
145+ "version": "0.24.2",
146+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
147+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
148+ "cpu": [
149+ "x64"
150+ ],
151+ "dev": true,
152+ "license": "MIT",
153+ "optional": true,
154+ "os": [
155+ "freebsd"
156+ ],
157+ "engines": {
158+ "node": ">=18"
159+ }
160+ },
161+ "node_modules/@esbuild/linux-arm": {
162+ "version": "0.24.2",
163+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
164+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
165+ "cpu": [
166+ "arm"
167+ ],
168+ "dev": true,
169+ "license": "MIT",
170+ "optional": true,
171+ "os": [
172+ "linux"
173+ ],
174+ "engines": {
175+ "node": ">=18"
176+ }
177+ },
178+ "node_modules/@esbuild/linux-arm64": {
179+ "version": "0.24.2",
180+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
181+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
182+ "cpu": [
183+ "arm64"
184+ ],
185+ "dev": true,
186+ "license": "MIT",
187+ "optional": true,
188+ "os": [
189+ "linux"
190+ ],
191+ "engines": {
192+ "node": ">=18"
193+ }
194+ },
195+ "node_modules/@esbuild/linux-ia32": {
196+ "version": "0.24.2",
197+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
198+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
199+ "cpu": [
200+ "ia32"
201+ ],
202+ "dev": true,
203+ "license": "MIT",
204+ "optional": true,
205+ "os": [
206+ "linux"
207+ ],
208+ "engines": {
209+ "node": ">=18"
210+ }
211+ },
212+ "node_modules/@esbuild/linux-loong64": {
213+ "version": "0.24.2",
214+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
215+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
216+ "cpu": [
217+ "loong64"
218+ ],
219+ "dev": true,
220+ "license": "MIT",
221+ "optional": true,
222+ "os": [
223+ "linux"
224+ ],
225+ "engines": {
226+ "node": ">=18"
227+ }
228+ },
229+ "node_modules/@esbuild/linux-mips64el": {
230+ "version": "0.24.2",
231+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
232+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
233+ "cpu": [
234+ "mips64el"
235+ ],
236+ "dev": true,
237+ "license": "MIT",
238+ "optional": true,
239+ "os": [
240+ "linux"
241+ ],
242+ "engines": {
243+ "node": ">=18"
244+ }
245+ },
246+ "node_modules/@esbuild/linux-ppc64": {
247+ "version": "0.24.2",
248+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
249+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
250+ "cpu": [
251+ "ppc64"
252+ ],
253+ "dev": true,
254+ "license": "MIT",
255+ "optional": true,
256+ "os": [
257+ "linux"
258+ ],
259+ "engines": {
260+ "node": ">=18"
261+ }
262+ },
263+ "node_modules/@esbuild/linux-riscv64": {
264+ "version": "0.24.2",
265+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
266+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
267+ "cpu": [
268+ "riscv64"
269+ ],
270+ "dev": true,
271+ "license": "MIT",
272+ "optional": true,
273+ "os": [
274+ "linux"
275+ ],
276+ "engines": {
277+ "node": ">=18"
278+ }
279+ },
280+ "node_modules/@esbuild/linux-s390x": {
281+ "version": "0.24.2",
282+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
283+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
284+ "cpu": [
285+ "s390x"
286+ ],
287+ "dev": true,
288+ "license": "MIT",
289+ "optional": true,
290+ "os": [
291+ "linux"
292+ ],
293+ "engines": {
294+ "node": ">=18"
295+ }
296+ },
297+ "node_modules/@esbuild/linux-x64": {
298+ "version": "0.24.2",
299+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
300+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
301+ "cpu": [
302+ "x64"
303+ ],
304+ "dev": true,
305+ "license": "MIT",
306+ "optional": true,
307+ "os": [
308+ "linux"
309+ ],
310+ "engines": {
311+ "node": ">=18"
312+ }
313+ },
314+ "node_modules/@esbuild/netbsd-arm64": {
315+ "version": "0.24.2",
316+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
317+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
318+ "cpu": [
319+ "arm64"
320+ ],
321+ "dev": true,
322+ "license": "MIT",
323+ "optional": true,
324+ "os": [
325+ "netbsd"
326+ ],
327+ "engines": {
328+ "node": ">=18"
329+ }
330+ },
331+ "node_modules/@esbuild/netbsd-x64": {
332+ "version": "0.24.2",
333+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
334+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
335+ "cpu": [
336+ "x64"
337+ ],
338+ "dev": true,
339+ "license": "MIT",
340+ "optional": true,
341+ "os": [
342+ "netbsd"
343+ ],
344+ "engines": {
345+ "node": ">=18"
346+ }
347+ },
348+ "node_modules/@esbuild/openbsd-arm64": {
349+ "version": "0.24.2",
350+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
351+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
352+ "cpu": [
353+ "arm64"
354+ ],
355+ "dev": true,
356+ "license": "MIT",
357+ "optional": true,
358+ "os": [
359+ "openbsd"
360+ ],
361+ "engines": {
362+ "node": ">=18"
363+ }
364+ },
365+ "node_modules/@esbuild/openbsd-x64": {
366+ "version": "0.24.2",
367+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
368+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
369+ "cpu": [
370+ "x64"
371+ ],
372+ "dev": true,
373+ "license": "MIT",
374+ "optional": true,
375+ "os": [
376+ "openbsd"
377+ ],
378+ "engines": {
379+ "node": ">=18"
380+ }
381+ },
382+ "node_modules/@esbuild/sunos-x64": {
383+ "version": "0.24.2",
384+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
385+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
386+ "cpu": [
387+ "x64"
388+ ],
389+ "dev": true,
390+ "license": "MIT",
391+ "optional": true,
392+ "os": [
393+ "sunos"
394+ ],
395+ "engines": {
396+ "node": ">=18"
397+ }
398+ },
399+ "node_modules/@esbuild/win32-arm64": {
400+ "version": "0.24.2",
401+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
402+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
403+ "cpu": [
404+ "arm64"
405+ ],
406+ "dev": true,
407+ "license": "MIT",
408+ "optional": true,
409+ "os": [
410+ "win32"
411+ ],
412+ "engines": {
413+ "node": ">=18"
414+ }
415+ },
416+ "node_modules/@esbuild/win32-ia32": {
417+ "version": "0.24.2",
418+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
419+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
420+ "cpu": [
421+ "ia32"
422+ ],
423+ "dev": true,
424+ "license": "MIT",
425+ "optional": true,
426+ "os": [
427+ "win32"
428+ ],
429+ "engines": {
430+ "node": ">=18"
431+ }
432+ },
433+ "node_modules/@esbuild/win32-x64": {
434+ "version": "0.24.2",
435+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
436+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
437+ "cpu": [
438+ "x64"
439+ ],
440+ "dev": true,
441+ "license": "MIT",
442+ "optional": true,
443+ "os": [
444+ "win32"
445+ ],
446+ "engines": {
447+ "node": ">=18"
448+ }
449+ },
450+ "node_modules/@loaders.gl/core": {
451+ "version": "4.3.3",
452+ "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.3.tgz",
453+ "integrity": "sha512-RaQ3uNg4ZaVqDRgvJ2CjaOjeeHdKvbKuzFFgbGnflVB9is5bu+h3EKc3Jke7NGVvLBsZ6oIXzkwHijVsMfxv8g==",
454+ "license": "MIT",
455+ "dependencies": {
456+ "@loaders.gl/loader-utils": "4.3.3",
457+ "@loaders.gl/schema": "4.3.3",
458+ "@loaders.gl/worker-utils": "4.3.3",
459+ "@probe.gl/log": "^4.0.2"
460+ }
461+ },
462+ "node_modules/@loaders.gl/draco": {
463+ "version": "4.3.3",
464+ "resolved": "https://registry.npmjs.org/@loaders.gl/draco/-/draco-4.3.3.tgz",
465+ "integrity": "sha512-f2isxvOoH4Pm5p4mGvNN9gVigUwX84j9gdKNMV1aSo56GS1KE3GS2rXaIoy1qaIHMzkPySUTEcOTwayf0hWU7A==",
466+ "license": "MIT",
467+ "dependencies": {
468+ "@loaders.gl/loader-utils": "4.3.3",
469+ "@loaders.gl/schema": "4.3.3",
470+ "@loaders.gl/worker-utils": "4.3.3",
471+ "draco3d": "1.5.7"
472+ },
473+ "peerDependencies": {
474+ "@loaders.gl/core": "^4.3.0"
475+ }
476+ },
477+ "node_modules/@loaders.gl/gltf": {
478+ "version": "4.3.3",
479+ "resolved": "https://registry.npmjs.org/@loaders.gl/gltf/-/gltf-4.3.3.tgz",
480+ "integrity": "sha512-M7jQ7KIB5itctDmGYuT9gndmjNwk1lwQ+BV4l5CoFp38e4xJESPglj2Kj8csWdm3WJhrxIYEP4GpjXK02n8DSQ==",
481+ "license": "MIT",
482+ "dependencies": {
483+ "@loaders.gl/draco": "4.3.3",
484+ "@loaders.gl/images": "4.3.3",
485+ "@loaders.gl/loader-utils": "4.3.3",
486+ "@loaders.gl/schema": "4.3.3",
487+ "@loaders.gl/textures": "4.3.3",
488+ "@math.gl/core": "^4.1.0"
489+ },
490+ "peerDependencies": {
491+ "@loaders.gl/core": "^4.3.0"
492+ }
493+ },
494+ "node_modules/@loaders.gl/images": {
495+ "version": "4.3.3",
496+ "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.3.3.tgz",
497+ "integrity": "sha512-s4InjIXqEu0T7anZLj4OBUuDBt2BNnAD0GLzSexSkBfQZfpXY0XJNl4mMf5nUKb5NDfXhIKIqv8y324US+I28A==",
498+ "license": "MIT",
499+ "dependencies": {
500+ "@loaders.gl/loader-utils": "4.3.3"
501+ },
502+ "peerDependencies": {
503+ "@loaders.gl/core": "^4.3.0"
504+ }
505+ },
506+ "node_modules/@loaders.gl/loader-utils": {
507+ "version": "4.3.3",
508+ "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.3.tgz",
509+ "integrity": "sha512-8erUIwWLiIsZX36fFa/seZsfTsWlLk72Sibh/YZJrPAefuVucV4mGGzMBZ96LE2BUfJhadn250eio/59TUFbNw==",
510+ "license": "MIT",
511+ "dependencies": {
512+ "@loaders.gl/schema": "4.3.3",
513+ "@loaders.gl/worker-utils": "4.3.3",
514+ "@probe.gl/log": "^4.0.2",
515+ "@probe.gl/stats": "^4.0.2"
516+ },
517+ "peerDependencies": {
518+ "@loaders.gl/core": "^4.3.0"
519+ }
520+ },
521+ "node_modules/@loaders.gl/schema": {
522+ "version": "4.3.3",
523+ "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.3.tgz",
524+ "integrity": "sha512-zacc9/8je+VbuC6N/QRfiTjRd+BuxsYlddLX1u5/X/cg9s36WZZBlU1oNKUgTYe8eO6+qLyYx77yi+9JbbEehw==",
525+ "license": "MIT",
526+ "dependencies": {
527+ "@types/geojson": "^7946.0.7"
528+ },
529+ "peerDependencies": {
530+ "@loaders.gl/core": "^4.3.0"
531+ }
532+ },
533+ "node_modules/@loaders.gl/textures": {
534+ "version": "4.3.3",
535+ "resolved": "https://registry.npmjs.org/@loaders.gl/textures/-/textures-4.3.3.tgz",
536+ "integrity": "sha512-qIo4ehzZnXFpPKl1BGQG4G3cAhBSczO9mr+H/bT7qFwtSirWVlqsvMlx1Q4VpmouDu+tudwwOlq7B3yqU5P5yQ==",
537+ "license": "MIT",
538+ "dependencies": {
539+ "@loaders.gl/images": "4.3.3",
540+ "@loaders.gl/loader-utils": "4.3.3",
541+ "@loaders.gl/schema": "4.3.3",
542+ "@loaders.gl/worker-utils": "4.3.3",
543+ "@math.gl/types": "^4.1.0",
544+ "ktx-parse": "^0.7.0",
545+ "texture-compressor": "^1.0.2"
546+ },
547+ "peerDependencies": {
548+ "@loaders.gl/core": "^4.3.0"
549+ }
550+ },
551+ "node_modules/@loaders.gl/worker-utils": {
552+ "version": "4.3.3",
553+ "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.3.tgz",
554+ "integrity": "sha512-eg45Ux6xqsAfqPUqJkhmbFZh9qfmYuPfA+34VcLtfeXIwAngeP6o4SrTmm9LWLGUKiSh47anCEV1p7borDgvGQ==",
555+ "license": "MIT",
556+ "peerDependencies": {
557+ "@loaders.gl/core": "^4.3.0"
558+ }
559+ },
560+ "node_modules/@math.gl/core": {
561+ "version": "4.1.0",
562+ "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz",
563+ "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==",
564+ "license": "MIT",
565+ "dependencies": {
566+ "@math.gl/types": "4.1.0"
567+ }
568+ },
569+ "node_modules/@math.gl/types": {
570+ "version": "4.1.0",
571+ "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz",
572+ "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==",
573+ "license": "MIT"
574+ },
575+ "node_modules/@probe.gl/env": {
576+ "version": "4.1.0",
577+ "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.0.tgz",
578+ "integrity": "sha512-5ac2Jm2K72VCs4eSMsM7ykVRrV47w32xOGMvcgqn8vQdEMF9PRXyBGYEV9YbqRKWNKpNKmQJVi4AHM/fkCxs9w==",
579+ "license": "MIT"
580+ },
581+ "node_modules/@probe.gl/log": {
582+ "version": "4.1.0",
583+ "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.1.0.tgz",
584+ "integrity": "sha512-r4gRReNY6f+OZEMgfWEXrAE2qJEt8rX0HsDJQXUBMoc+5H47bdB7f/5HBHAmapK8UydwPKL9wCDoS22rJ0yq7Q==",
585+ "license": "MIT",
586+ "dependencies": {
587+ "@probe.gl/env": "4.1.0"
588+ }
589+ },
590+ "node_modules/@probe.gl/stats": {
591+ "version": "4.1.0",
592+ "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.0.tgz",
593+ "integrity": "sha512-EI413MkWKBDVNIfLdqbeNSJTs7ToBz/KVGkwi3D+dQrSIkRI2IYbWGAU3xX+D6+CI4ls8ehxMhNpUVMaZggDvQ==",
594+ "license": "MIT"
595+ },
596+ "node_modules/@rollup/pluginutils": {
597+ "version": "5.1.4",
598+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
599+ "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
600+ "dev": true,
601+ "license": "MIT",
602+ "dependencies": {
603+ "@types/estree": "^1.0.0",
604+ "estree-walker": "^2.0.2",
605+ "picomatch": "^4.0.2"
606+ },
607+ "engines": {
608+ "node": ">=14.0.0"
609+ },
610+ "peerDependencies": {
611+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
612+ },
613+ "peerDependenciesMeta": {
614+ "rollup": {
615+ "optional": true
616+ }
617+ }
618+ },
619+ "node_modules/@rollup/rollup-android-arm-eabi": {
620+ "version": "4.31.0",
621+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
622+ "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
623+ "cpu": [
624+ "arm"
625+ ],
626+ "dev": true,
627+ "license": "MIT",
628+ "optional": true,
629+ "os": [
630+ "android"
631+ ]
632+ },
633+ "node_modules/@rollup/rollup-android-arm64": {
634+ "version": "4.31.0",
635+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
636+ "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
637+ "cpu": [
638+ "arm64"
639+ ],
640+ "dev": true,
641+ "license": "MIT",
642+ "optional": true,
643+ "os": [
644+ "android"
645+ ]
646+ },
647+ "node_modules/@rollup/rollup-darwin-arm64": {
648+ "version": "4.31.0",
649+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
650+ "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
651+ "cpu": [
652+ "arm64"
653+ ],
654+ "dev": true,
655+ "license": "MIT",
656+ "optional": true,
657+ "os": [
658+ "darwin"
659+ ]
660+ },
661+ "node_modules/@rollup/rollup-darwin-x64": {
662+ "version": "4.31.0",
663+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
664+ "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
665+ "cpu": [
666+ "x64"
667+ ],
668+ "dev": true,
669+ "license": "MIT",
670+ "optional": true,
671+ "os": [
672+ "darwin"
673+ ]
674+ },
675+ "node_modules/@rollup/rollup-freebsd-arm64": {
676+ "version": "4.31.0",
677+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
678+ "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
679+ "cpu": [
680+ "arm64"
681+ ],
682+ "dev": true,
683+ "license": "MIT",
684+ "optional": true,
685+ "os": [
686+ "freebsd"
687+ ]
688+ },
689+ "node_modules/@rollup/rollup-freebsd-x64": {
690+ "version": "4.31.0",
691+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
692+ "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
693+ "cpu": [
694+ "x64"
695+ ],
696+ "dev": true,
697+ "license": "MIT",
698+ "optional": true,
699+ "os": [
700+ "freebsd"
701+ ]
702+ },
703+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
704+ "version": "4.31.0",
705+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
706+ "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
707+ "cpu": [
708+ "arm"
709+ ],
710+ "dev": true,
711+ "license": "MIT",
712+ "optional": true,
713+ "os": [
714+ "linux"
715+ ]
716+ },
717+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
718+ "version": "4.31.0",
719+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
720+ "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
721+ "cpu": [
722+ "arm"
723+ ],
724+ "dev": true,
725+ "license": "MIT",
726+ "optional": true,
727+ "os": [
728+ "linux"
729+ ]
730+ },
731+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
732+ "version": "4.31.0",
733+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
734+ "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
735+ "cpu": [
736+ "arm64"
737+ ],
738+ "dev": true,
739+ "license": "MIT",
740+ "optional": true,
741+ "os": [
742+ "linux"
743+ ]
744+ },
745+ "node_modules/@rollup/rollup-linux-arm64-musl": {
746+ "version": "4.31.0",
747+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
748+ "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
749+ "cpu": [
750+ "arm64"
751+ ],
752+ "dev": true,
753+ "license": "MIT",
754+ "optional": true,
755+ "os": [
756+ "linux"
757+ ]
758+ },
759+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
760+ "version": "4.31.0",
761+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
762+ "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
763+ "cpu": [
764+ "loong64"
765+ ],
766+ "dev": true,
767+ "license": "MIT",
768+ "optional": true,
769+ "os": [
770+ "linux"
771+ ]
772+ },
773+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
774+ "version": "4.31.0",
775+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
776+ "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
777+ "cpu": [
778+ "ppc64"
779+ ],
780+ "dev": true,
781+ "license": "MIT",
782+ "optional": true,
783+ "os": [
784+ "linux"
785+ ]
786+ },
787+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
788+ "version": "4.31.0",
789+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
790+ "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
791+ "cpu": [
792+ "riscv64"
793+ ],
794+ "dev": true,
795+ "license": "MIT",
796+ "optional": true,
797+ "os": [
798+ "linux"
799+ ]
800+ },
801+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
802+ "version": "4.31.0",
803+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
804+ "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
805+ "cpu": [
806+ "s390x"
807+ ],
808+ "dev": true,
809+ "license": "MIT",
810+ "optional": true,
811+ "os": [
812+ "linux"
813+ ]
814+ },
815+ "node_modules/@rollup/rollup-linux-x64-gnu": {
816+ "version": "4.31.0",
817+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
818+ "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
819+ "cpu": [
820+ "x64"
821+ ],
822+ "dev": true,
823+ "license": "MIT",
824+ "optional": true,
825+ "os": [
826+ "linux"
827+ ]
828+ },
829+ "node_modules/@rollup/rollup-linux-x64-musl": {
830+ "version": "4.31.0",
831+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
832+ "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
833+ "cpu": [
834+ "x64"
835+ ],
836+ "dev": true,
837+ "license": "MIT",
838+ "optional": true,
839+ "os": [
840+ "linux"
841+ ]
842+ },
843+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
844+ "version": "4.31.0",
845+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
846+ "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
847+ "cpu": [
848+ "arm64"
849+ ],
850+ "dev": true,
851+ "license": "MIT",
852+ "optional": true,
853+ "os": [
854+ "win32"
855+ ]
856+ },
857+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
858+ "version": "4.31.0",
859+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
860+ "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
861+ "cpu": [
862+ "ia32"
863+ ],
864+ "dev": true,
865+ "license": "MIT",
866+ "optional": true,
867+ "os": [
868+ "win32"
869+ ]
870+ },
871+ "node_modules/@rollup/rollup-win32-x64-msvc": {
872+ "version": "4.31.0",
873+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
874+ "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
875+ "cpu": [
876+ "x64"
877+ ],
878+ "dev": true,
879+ "license": "MIT",
880+ "optional": true,
881+ "os": [
882+ "win32"
883+ ]
884+ },
885+ "node_modules/@tweakpane/core": {
886+ "version": "2.0.5",
887+ "resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-2.0.5.tgz",
888+ "integrity": "sha512-punBgD5rKCF5vcNo6BsSOXiDR/NSs9VM7SG65QSLJIxfRaGgj54ree9zQW6bO3pNFf3AogiGgaNODUVQRk9YqQ==",
889+ "dev": true,
890+ "license": "MIT"
891+ },
892+ "node_modules/@types/estree": {
893+ "version": "1.0.6",
894+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
895+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
896+ "dev": true,
897+ "license": "MIT"
898+ },
899+ "node_modules/@types/geojson": {
900+ "version": "7946.0.16",
901+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
902+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
903+ "license": "MIT"
904+ },
905+ "node_modules/@webgpu/types": {
906+ "version": "0.1.53",
907+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.53.tgz",
908+ "integrity": "sha512-x+BLw/opaz9LiVyrMsP75nO1Rg0QfrACUYIbVSfGwY/w0DiWIPYYrpte6us//KZXinxFAOJl0+C17L1Vi2vmDw==",
909+ "dev": true,
910+ "license": "BSD-3-Clause"
911+ },
912+ "node_modules/argparse": {
913+ "version": "1.0.10",
914+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
915+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
916+ "license": "MIT",
917+ "dependencies": {
918+ "sprintf-js": "~1.0.2"
919+ }
920+ },
921+ "node_modules/draco3d": {
922+ "version": "1.5.7",
923+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
924+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
925+ "license": "Apache-2.0"
926+ },
927+ "node_modules/esbuild": {
928+ "version": "0.24.2",
929+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
930+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
931+ "dev": true,
932+ "hasInstallScript": true,
933+ "license": "MIT",
934+ "bin": {
935+ "esbuild": "bin/esbuild"
936+ },
937+ "engines": {
938+ "node": ">=18"
939+ },
940+ "optionalDependencies": {
941+ "@esbuild/aix-ppc64": "0.24.2",
942+ "@esbuild/android-arm": "0.24.2",
943+ "@esbuild/android-arm64": "0.24.2",
944+ "@esbuild/android-x64": "0.24.2",
945+ "@esbuild/darwin-arm64": "0.24.2",
946+ "@esbuild/darwin-x64": "0.24.2",
947+ "@esbuild/freebsd-arm64": "0.24.2",
948+ "@esbuild/freebsd-x64": "0.24.2",
949+ "@esbuild/linux-arm": "0.24.2",
950+ "@esbuild/linux-arm64": "0.24.2",
951+ "@esbuild/linux-ia32": "0.24.2",
952+ "@esbuild/linux-loong64": "0.24.2",
953+ "@esbuild/linux-mips64el": "0.24.2",
954+ "@esbuild/linux-ppc64": "0.24.2",
955+ "@esbuild/linux-riscv64": "0.24.2",
956+ "@esbuild/linux-s390x": "0.24.2",
957+ "@esbuild/linux-x64": "0.24.2",
958+ "@esbuild/netbsd-arm64": "0.24.2",
959+ "@esbuild/netbsd-x64": "0.24.2",
960+ "@esbuild/openbsd-arm64": "0.24.2",
961+ "@esbuild/openbsd-x64": "0.24.2",
962+ "@esbuild/sunos-x64": "0.24.2",
963+ "@esbuild/win32-arm64": "0.24.2",
964+ "@esbuild/win32-ia32": "0.24.2",
965+ "@esbuild/win32-x64": "0.24.2"
966+ }
967+ },
968+ "node_modules/estree-walker": {
969+ "version": "2.0.2",
970+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
971+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
972+ "dev": true,
973+ "license": "MIT"
974+ },
975+ "node_modules/fsevents": {
976+ "version": "2.3.3",
977+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
978+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
979+ "dev": true,
980+ "hasInstallScript": true,
981+ "license": "MIT",
982+ "optional": true,
983+ "os": [
984+ "darwin"
985+ ],
986+ "engines": {
987+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
988+ }
989+ },
990+ "node_modules/gl-matrix": {
991+ "version": "4.0.0-beta.2",
992+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-4.0.0-beta.2.tgz",
993+ "integrity": "sha512-OF6IkQpMkF8p2CZF9EtzYZPlPaW3M41KMsgZGlTKmMv/nWaP6GMJi9V5tI+oPn8FG0io85Q5ZtKpCXP4u6YmDA==",
994+ "license": "MIT"
995+ },
996+ "node_modules/image-size": {
997+ "version": "0.7.5",
998+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz",
999+ "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==",
1000+ "license": "MIT",
1001+ "bin": {
1002+ "image-size": "bin/image-size.js"
1003+ },
1004+ "engines": {
1005+ "node": ">=6.9.0"
1006+ }
1007+ },
1008+ "node_modules/ktx-parse": {
1009+ "version": "0.7.1",
1010+ "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz",
1011+ "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==",
1012+ "license": "MIT"
1013+ },
1014+ "node_modules/nanoid": {
1015+ "version": "3.3.8",
1016+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
1017+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
1018+ "dev": true,
1019+ "funding": [
1020+ {
1021+ "type": "github",
1022+ "url": "https://github.com/sponsors/ai"
1023+ }
1024+ ],
1025+ "license": "MIT",
1026+ "bin": {
1027+ "nanoid": "bin/nanoid.cjs"
1028+ },
1029+ "engines": {
1030+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1031+ }
1032+ },
1033+ "node_modules/picocolors": {
1034+ "version": "1.1.1",
1035+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1036+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1037+ "dev": true,
1038+ "license": "ISC"
1039+ },
1040+ "node_modules/picomatch": {
1041+ "version": "4.0.2",
1042+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
1043+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
1044+ "dev": true,
1045+ "license": "MIT",
1046+ "engines": {
1047+ "node": ">=12"
1048+ },
1049+ "funding": {
1050+ "url": "https://github.com/sponsors/jonschlinkert"
1051+ }
1052+ },
1053+ "node_modules/postcss": {
1054+ "version": "8.5.1",
1055+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
1056+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
1057+ "dev": true,
1058+ "funding": [
1059+ {
1060+ "type": "opencollective",
1061+ "url": "https://opencollective.com/postcss/"
1062+ },
1063+ {
1064+ "type": "tidelift",
1065+ "url": "https://tidelift.com/funding/github/npm/postcss"
1066+ },
1067+ {
1068+ "type": "github",
1069+ "url": "https://github.com/sponsors/ai"
1070+ }
1071+ ],
1072+ "license": "MIT",
1073+ "dependencies": {
1074+ "nanoid": "^3.3.8",
1075+ "picocolors": "^1.1.1",
1076+ "source-map-js": "^1.2.1"
1077+ },
1078+ "engines": {
1079+ "node": "^10 || ^12 || >=14"
1080+ }
1081+ },
1082+ "node_modules/rollup": {
1083+ "version": "4.31.0",
1084+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
1085+ "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
1086+ "dev": true,
1087+ "license": "MIT",
1088+ "dependencies": {
1089+ "@types/estree": "1.0.6"
1090+ },
1091+ "bin": {
1092+ "rollup": "dist/bin/rollup"
1093+ },
1094+ "engines": {
1095+ "node": ">=18.0.0",
1096+ "npm": ">=8.0.0"
1097+ },
1098+ "optionalDependencies": {
1099+ "@rollup/rollup-android-arm-eabi": "4.31.0",
1100+ "@rollup/rollup-android-arm64": "4.31.0",
1101+ "@rollup/rollup-darwin-arm64": "4.31.0",
1102+ "@rollup/rollup-darwin-x64": "4.31.0",
1103+ "@rollup/rollup-freebsd-arm64": "4.31.0",
1104+ "@rollup/rollup-freebsd-x64": "4.31.0",
1105+ "@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
1106+ "@rollup/rollup-linux-arm-musleabihf": "4.31.0",
1107+ "@rollup/rollup-linux-arm64-gnu": "4.31.0",
1108+ "@rollup/rollup-linux-arm64-musl": "4.31.0",
1109+ "@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
1110+ "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
1111+ "@rollup/rollup-linux-riscv64-gnu": "4.31.0",
1112+ "@rollup/rollup-linux-s390x-gnu": "4.31.0",
1113+ "@rollup/rollup-linux-x64-gnu": "4.31.0",
1114+ "@rollup/rollup-linux-x64-musl": "4.31.0",
1115+ "@rollup/rollup-win32-arm64-msvc": "4.31.0",
1116+ "@rollup/rollup-win32-ia32-msvc": "4.31.0",
1117+ "@rollup/rollup-win32-x64-msvc": "4.31.0",
1118+ "fsevents": "~2.3.2"
1119+ }
1120+ },
1121+ "node_modules/source-map-js": {
1122+ "version": "1.2.1",
1123+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1124+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1125+ "dev": true,
1126+ "license": "BSD-3-Clause",
1127+ "engines": {
1128+ "node": ">=0.10.0"
1129+ }
1130+ },
1131+ "node_modules/sprintf-js": {
1132+ "version": "1.0.3",
1133+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
1134+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
1135+ "license": "BSD-3-Clause"
1136+ },
1137+ "node_modules/texture-compressor": {
1138+ "version": "1.0.2",
1139+ "resolved": "https://registry.npmjs.org/texture-compressor/-/texture-compressor-1.0.2.tgz",
1140+ "integrity": "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==",
1141+ "license": "MIT",
1142+ "dependencies": {
1143+ "argparse": "^1.0.10",
1144+ "image-size": "^0.7.4"
1145+ },
1146+ "bin": {
1147+ "texture-compressor": "bin/texture-compressor.js"
1148+ }
1149+ },
1150+ "node_modules/tweakpane": {
1151+ "version": "4.0.5",
1152+ "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.5.tgz",
1153+ "integrity": "sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==",
1154+ "license": "MIT",
1155+ "funding": {
1156+ "url": "https://github.com/sponsors/cocopon"
1157+ }
1158+ },
1159+ "node_modules/typescript": {
1160+ "version": "5.6.3",
1161+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
1162+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
1163+ "dev": true,
1164+ "license": "Apache-2.0",
1165+ "bin": {
1166+ "tsc": "bin/tsc",
1167+ "tsserver": "bin/tsserver"
1168+ },
1169+ "engines": {
1170+ "node": ">=14.17"
1171+ }
1172+ },
1173+ "node_modules/vite": {
1174+ "version": "6.0.9",
1175+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.9.tgz",
1176+ "integrity": "sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==",
1177+ "dev": true,
1178+ "license": "MIT",
1179+ "dependencies": {
1180+ "esbuild": "^0.24.2",
1181+ "postcss": "^8.4.49",
1182+ "rollup": "^4.23.0"
1183+ },
1184+ "bin": {
1185+ "vite": "bin/vite.js"
1186+ },
1187+ "engines": {
1188+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1189+ },
1190+ "funding": {
1191+ "url": "https://github.com/vitejs/vite?sponsor=1"
1192+ },
1193+ "optionalDependencies": {
1194+ "fsevents": "~2.3.3"
1195+ },
1196+ "peerDependencies": {
1197+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1198+ "jiti": ">=1.21.0",
1199+ "less": "*",
1200+ "lightningcss": "^1.21.0",
1201+ "sass": "*",
1202+ "sass-embedded": "*",
1203+ "stylus": "*",
1204+ "sugarss": "*",
1205+ "terser": "^5.16.0",
1206+ "tsx": "^4.8.1",
1207+ "yaml": "^2.4.2"
1208+ },
1209+ "peerDependenciesMeta": {
1210+ "@types/node": {
1211+ "optional": true
1212+ },
1213+ "jiti": {
1214+ "optional": true
1215+ },
1216+ "less": {
1217+ "optional": true
1218+ },
1219+ "lightningcss": {
1220+ "optional": true
1221+ },
1222+ "sass": {
1223+ "optional": true
1224+ },
1225+ "sass-embedded": {
1226+ "optional": true
1227+ },
1228+ "stylus": {
1229+ "optional": true
1230+ },
1231+ "sugarss": {
1232+ "optional": true
1233+ },
1234+ "terser": {
1235+ "optional": true
1236+ },
1237+ "tsx": {
1238+ "optional": true
1239+ },
1240+ "yaml": {
1241+ "optional": true
1242+ }
1243+ }
1244+ },
1245+ "node_modules/vite-plugin-glsl": {
1246+ "version": "1.4.0",
1247+ "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.4.0.tgz",
1248+ "integrity": "sha512-mjT4AaU4qRmlpawgd0M2Qz72tvK4WF0ii2p0WbVRpr7ga6+cRScJUT3oIMv5coT8u/lqUe9u9T5+0zJLZ1uhug==",
1249+ "dev": true,
1250+ "license": "MIT",
1251+ "dependencies": {
1252+ "@rollup/pluginutils": "^5.1.4"
1253+ },
1254+ "engines": {
1255+ "node": ">= 20.17.0",
1256+ "npm": ">= 10.8.3"
1257+ },
1258+ "peerDependencies": {
1259+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
1260+ }
1261+ }
1262+ }
1263+}
A · package.json
+27, -0 1@@ -0,0 +1,27 @@
2+{
3+ "name": "vite-project",
4+ "private": true,
5+ "version": "0.0.0",
6+ "type": "module",
7+ "scripts": {
8+ "dev": "vite",
9+ "build": "tsc && vite build",
10+ "preview": "vite preview"
11+ },
12+ "dependencies": {
13+ "@loaders.gl/core": "4.3.3",
14+ "@loaders.gl/gltf": "4.3.3",
15+ "gl-matrix": "^4.0.0-beta.2",
16+ "tweakpane": "^4.0.5"
17+ },
18+ "devDependencies": {
19+ "@tweakpane/core": "^2.0.5",
20+ "@webgpu/types": "^0.1.53",
21+ "typescript": "~5.6.2",
22+ "vite": "^6.0.5",
23+ "vite-plugin-glsl": "^1.4.0"
24+ },
25+ "unusedDependencies": {
26+ "hdr.js": "^0.2.0"
27+ }
28+}
A · public/Duck.gltf
+219, -0 1@@ -0,0 +1,219 @@
2+{
3+ "asset": {
4+ "generator": "COLLADA2GLTF",
5+ "version": "2.0"
6+ },
7+ "scene": 0,
8+ "scenes": [
9+ {
10+ "nodes": [
11+ 0
12+ ]
13+ }
14+ ],
15+ "nodes": [
16+ {
17+ "children": [
18+ 2,
19+ 1
20+ ],
21+ "matrix": [
22+ 0.009999999776482582,
23+ 0.0,
24+ 0.0,
25+ 0.0,
26+ 0.0,
27+ 0.009999999776482582,
28+ 0.0,
29+ 0.0,
30+ 0.0,
31+ 0.0,
32+ 0.009999999776482582,
33+ 0.0,
34+ 0.0,
35+ 0.0,
36+ 0.0,
37+ 1.0
38+ ]
39+ },
40+ {
41+ "matrix": [
42+ -0.7289686799049377,
43+ 0.0,
44+ -0.6845470666885376,
45+ 0.0,
46+ -0.4252049028873444,
47+ 0.7836934328079224,
48+ 0.4527972936630249,
49+ 0.0,
50+ 0.5364750623703003,
51+ 0.6211478114128113,
52+ -0.571287989616394,
53+ 0.0,
54+ 400.1130065917969,
55+ 463.2640075683594,
56+ -431.0780334472656,
57+ 1.0
58+ ],
59+ "camera": 0
60+ },
61+ {
62+ "mesh": 0
63+ }
64+ ],
65+ "cameras": [
66+ {
67+ "perspective": {
68+ "aspectRatio": 1.5,
69+ "yfov": 0.6605925559997559,
70+ "zfar": 10000.0,
71+ "znear": 1.0
72+ },
73+ "type": "perspective"
74+ }
75+ ],
76+ "meshes": [
77+ {
78+ "primitives": [
79+ {
80+ "attributes": {
81+ "NORMAL": 1,
82+ "POSITION": 2,
83+ "TEXCOORD_0": 3
84+ },
85+ "indices": 0,
86+ "mode": 4,
87+ "material": 0
88+ }
89+ ],
90+ "name": "LOD3spShape"
91+ }
92+ ],
93+ "accessors": [
94+ {
95+ "bufferView": 0,
96+ "byteOffset": 0,
97+ "componentType": 5123,
98+ "count": 12636,
99+ "max": [
100+ 2398
101+ ],
102+ "min": [
103+ 0
104+ ],
105+ "type": "SCALAR"
106+ },
107+ {
108+ "bufferView": 1,
109+ "byteOffset": 0,
110+ "componentType": 5126,
111+ "count": 2399,
112+ "max": [
113+ 0.9995989799499512,
114+ 0.999580979347229,
115+ 0.9984359741210938
116+ ],
117+ "min": [
118+ -0.9990839958190918,
119+ -1.0,
120+ -0.9998319745063782
121+ ],
122+ "type": "VEC3"
123+ },
124+ {
125+ "bufferView": 1,
126+ "byteOffset": 28788,
127+ "componentType": 5126,
128+ "count": 2399,
129+ "max": [
130+ 96.17990112304688,
131+ 163.97000122070313,
132+ 53.92519760131836
133+ ],
134+ "min": [
135+ -69.29850006103516,
136+ 9.929369926452637,
137+ -61.32819747924805
138+ ],
139+ "type": "VEC3"
140+ },
141+ {
142+ "bufferView": 2,
143+ "byteOffset": 0,
144+ "componentType": 5126,
145+ "count": 2399,
146+ "max": [
147+ 0.9833459854125976,
148+ 0.9800369739532472
149+ ],
150+ "min": [
151+ 0.026409000158309938,
152+ 0.01996302604675293
153+ ],
154+ "type": "VEC2"
155+ }
156+ ],
157+ "materials": [
158+ {
159+ "pbrMetallicRoughness": {
160+ "baseColorTexture": {
161+ "index": 0
162+ },
163+ "metallicFactor": 0.0
164+ },
165+ "emissiveFactor": [
166+ 0.0,
167+ 0.0,
168+ 0.0
169+ ],
170+ "name": "blinn3-fx"
171+ }
172+ ],
173+ "textures": [
174+ {
175+ "sampler": 0,
176+ "source": 0
177+ }
178+ ],
179+ "images": [
180+ {
181+ "uri": "DuckCM.png"
182+ }
183+ ],
184+ "samplers": [
185+ {
186+ "magFilter": 9729,
187+ "minFilter": 9986,
188+ "wrapS": 10497,
189+ "wrapT": 10497
190+ }
191+ ],
192+ "bufferViews": [
193+ {
194+ "buffer": 0,
195+ "byteOffset": 76768,
196+ "byteLength": 25272,
197+ "target": 34963
198+ },
199+ {
200+ "buffer": 0,
201+ "byteOffset": 0,
202+ "byteLength": 57576,
203+ "byteStride": 12,
204+ "target": 34962
205+ },
206+ {
207+ "buffer": 0,
208+ "byteOffset": 57576,
209+ "byteLength": 19192,
210+ "byteStride": 8,
211+ "target": 34962
212+ }
213+ ],
214+ "buffers": [
215+ {
216+ "byteLength": 102040,
217+ "uri": "Duck0.bin"
218+ }
219+ ]
220+}
A · public/Duck0.bin
+0, -0
A · public/DuckCM.png
+0, -0
A · public/EnvironmentTest.gltf
+328, -0 1@@ -0,0 +1,328 @@
2+{
3+ "asset": {
4+ "copyright": "2018 (c) Adobe Systems Inc.",
5+ "generator": "Adobe Dimension - b417c10282aa66313155856d4a54e84f3f388647",
6+ "version": "2.0"
7+ },
8+ "accessors": [
9+ {
10+ "bufferView": 0,
11+ "componentType": 5126,
12+ "count": 4598,
13+ "type": "VEC3",
14+ "max": [
15+ 10.647041320800782,
16+ 1.6470409631729127,
17+ 0.6470409631729126
18+ ],
19+ "min": [
20+ -10.647041320800782,
21+ 0.3529590368270874,
22+ -0.6470409631729126
23+ ]
24+ },
25+ {
26+ "bufferView": 1,
27+ "componentType": 5126,
28+ "count": 4598,
29+ "type": "VEC3"
30+ },
31+ {
32+ "bufferView": 2,
33+ "componentType": 5126,
34+ "count": 4598,
35+ "type": "VEC2"
36+ },
37+ {
38+ "bufferView": 3,
39+ "componentType": 5125,
40+ "count": 25344,
41+ "type": "SCALAR",
42+ "max": [
43+ 4597
44+ ],
45+ "min": [
46+ 0
47+ ]
48+ },
49+ {
50+ "bufferView": 4,
51+ "componentType": 5126,
52+ "count": 4598,
53+ "type": "VEC3",
54+ "max": [
55+ 10.647041320800782,
56+ -0.3529590368270874,
57+ 0.6470409631729126
58+ ],
59+ "min": [
60+ -10.647041320800782,
61+ -1.6470409631729127,
62+ -0.6470409631729126
63+ ]
64+ },
65+ {
66+ "bufferView": 5,
67+ "componentType": 5126,
68+ "count": 4598,
69+ "type": "VEC2"
70+ }
71+ ],
72+ "bufferViews": [
73+ {
74+ "buffer": 0,
75+ "byteOffset": 0,
76+ "byteLength": 55176,
77+ "target": 34962
78+ },
79+ {
80+ "buffer": 0,
81+ "byteOffset": 55176,
82+ "byteLength": 55176,
83+ "target": 34962
84+ },
85+ {
86+ "buffer": 0,
87+ "byteOffset": 110352,
88+ "byteLength": 36784,
89+ "target": 34962
90+ },
91+ {
92+ "buffer": 0,
93+ "byteOffset": 147136,
94+ "byteLength": 101376,
95+ "target": 34963
96+ },
97+ {
98+ "buffer": 0,
99+ "byteOffset": 248512,
100+ "byteLength": 55176,
101+ "target": 34962
102+ },
103+ {
104+ "buffer": 0,
105+ "byteOffset": 303688,
106+ "byteLength": 36784,
107+ "target": 34962
108+ }
109+ ],
110+ "buffers": [
111+ {
112+ "byteLength": 340472,
113+ "uri": "EnvironmentTest_binary.bin"
114+ }
115+ ],
116+ "cameras": [
117+ {
118+ "perspective": {
119+ "znear": 0.0010000000474974514,
120+ "yfov": 0.6024156808853149,
121+ "zfar": 200.0,
122+ "aspectRatio": 1.3333333730697632
123+ },
124+ "type": "perspective",
125+ "name": "render_camera"
126+ }
127+ ],
128+ "images": [
129+ {
130+ "name": "tmp_image_pie_dc1e_1a22_fbf9roughness_map_roughness_tmp_image_pie_dc1e_1a22_fbf9metal_map_metallic_0",
131+ "uri": "roughness_metallic_0.jpg",
132+ "mimeType": "image/jpeg"
133+ },
134+ {
135+ "name": "tmp_image_pie_b20b_ebb4_317droughness_map2_roughness_tmp_image_pie_b20b_ebb4_317dmetal_map2_metallic_1",
136+ "uri": "roughness_metallic_1.jpg",
137+ "mimeType": "image/jpeg"
138+ }
139+ ],
140+ "materials": [
141+ {
142+ "pbrMetallicRoughness": {
143+ "metallicRoughnessTexture": {
144+ "index": 0
145+ }
146+ },
147+ "name": "MetallicSpheresMat",
148+ "doubleSided": true
149+ },
150+ {
151+ "pbrMetallicRoughness": {
152+ "metallicRoughnessTexture": {
153+ "index": 1
154+ }
155+ },
156+ "name": "DielectricSpheresMat",
157+ "doubleSided": true
158+ },
159+ {
160+ "pbrMetallicRoughness": {
161+ "baseColorFactor": [
162+ 0.0,
163+ 0.0,
164+ 0.0,
165+ 1.0
166+ ],
167+ "metallicRoughnessTexture": {
168+ "index": 1
169+ }
170+ },
171+ "name": "DielectricSpheresMat",
172+ "doubleSided": true
173+ }
174+ ],
175+ "meshes": [
176+ {
177+ "name": "Metallic0_N3D",
178+ "primitives": [
179+ {
180+ "attributes": {
181+ "POSITION": 0,
182+ "NORMAL": 1,
183+ "TEXCOORD_0": 2
184+ },
185+ "indices": 3,
186+ "material": 0
187+ }
188+ ]
189+ },
190+ {
191+ "name": "Dielectric0_N3D2",
192+ "primitives": [
193+ {
194+ "attributes": {
195+ "TEXCOORD_0": 5,
196+ "NORMAL": 1,
197+ "POSITION": 4
198+ },
199+ "indices": 3,
200+ "material": 1
201+ }
202+ ]
203+ },
204+ {
205+ "name": "Dielectric0_N3D",
206+ "primitives": [
207+ {
208+ "attributes": {
209+ "POSITION": 4,
210+ "NORMAL": 1,
211+ "TEXCOORD_0": 5
212+ },
213+ "indices": 3,
214+ "material": 2
215+ }
216+ ]
217+ }
218+ ],
219+ "nodes": [
220+ {
221+ "matrix": [
222+ 0.9999533295631409,
223+ 3.16067598760128e-8,
224+ 0.009662099182605744,
225+ 0.0,
226+ 0.0014864075928926468,
227+ 0.9880954027175903,
228+ -0.15383504331111909,
229+ 0.0,
230+ -0.009547080844640732,
231+ 0.15384222567081452,
232+ 0.988049328327179,
233+ 0.0,
234+ -0.7599077224731445,
235+ 7.708760738372803,
236+ 27.743375778198243,
237+ 1.0
238+ ],
239+ "camera": 0,
240+ "name": "render_camera_n3d"
241+ },
242+ {
243+ "name": "ground_plane_n3d"
244+ },
245+ {
246+ "children": [
247+ 3,
248+ 4,
249+ 5
250+ ],
251+ "matrix": [
252+ 1.0,
253+ 0.0,
254+ 0.0,
255+ 0.0,
256+ 0.0,
257+ 1.0,
258+ 0.0,
259+ 0.0,
260+ 0.0,
261+ 0.0,
262+ 1.0,
263+ 0.0,
264+ -0.5564079284667969,
265+ 4.774584770202637,
266+ -1.0962677001953126,
267+ 1.0
268+ ],
269+ "name": "ENV_Spheres"
270+ },
271+ {
272+ "mesh": 0,
273+ "name": "Metallic0"
274+ },
275+ {
276+ "mesh": 1,
277+ "name": "Dielectric0"
278+ },
279+ {
280+ "matrix": [
281+ 1.0,
282+ 0.0,
283+ 0.0,
284+ 0.0,
285+ 0.0,
286+ 1.0,
287+ 0.0,
288+ 0.0,
289+ 0.0,
290+ 0.0,
291+ 1.0,
292+ 0.0,
293+ 0.0,
294+ -1.985867977142334,
295+ 0.0,
296+ 1.0
297+ ],
298+ "mesh": 2,
299+ "name": "Dielectric0-Black"
300+ }
301+ ],
302+ "samplers": [
303+ {},
304+ {}
305+ ],
306+ "scenes": [
307+ {
308+ "nodes": [
309+ 0,
310+ 1,
311+ 2
312+ ],
313+ "name": "scene"
314+ }
315+ ],
316+ "textures": [
317+ {
318+ "name": "tmp_image_pie_dc1e_1a22_fbf9roughness_map_roughness_tmp_image_pie_dc1e_1a22_fbf9metal_map_metallic_0_texture",
319+ "sampler": 0,
320+ "source": 0
321+ },
322+ {
323+ "name": "tmp_image_pie_b20b_ebb4_317droughness_map2_roughness_tmp_image_pie_b20b_ebb4_317dmetal_map2_metallic_1_texture",
324+ "sampler": 1,
325+ "source": 1
326+ }
327+ ],
328+ "scene": 0
329+}
A · public/EnvironmentTest_binary.bin
+0, -0
A · public/LDR_RGBA_0.png
+0, -0
A · public/cornell_empty_rg.bin
+0, -0
A · public/cornell_empty_rg.gltf
+445, -0 1@@ -0,0 +1,445 @@
2+{
3+ "asset":{
4+ "generator":"Khronos glTF Blender I/O v4.3.47",
5+ "version":"2.0"
6+ },
7+ "extensionsUsed":[
8+ "KHR_materials_specular",
9+ "KHR_materials_ior"
10+ ],
11+ "scene":0,
12+ "scenes":[
13+ {
14+ "name":"Scene",
15+ "nodes":[
16+ 0
17+ ]
18+ }
19+ ],
20+ "nodes":[
21+ {
22+ "mesh":0,
23+ "name":"CornellBox-Empty-RG",
24+ "rotation":[
25+ 0.7071068286895752,
26+ 0,
27+ 0,
28+ 0.7071068286895752
29+ ]
30+ }
31+ ],
32+ "materials":[
33+ {
34+ "doubleSided":true,
35+ "extensions":{
36+ "KHR_materials_specular":{
37+ "specularFactor":0
38+ },
39+ "KHR_materials_ior":{
40+ "ior":1
41+ }
42+ },
43+ "name":"floor.002",
44+ "pbrMetallicRoughness":{
45+ "baseColorFactor":[
46+ 0.7250000238418579,
47+ 0.7099999785423279,
48+ 0.6800000071525574,
49+ 1
50+ ],
51+ "metallicFactor":0,
52+ "roughnessFactor":0.8999999761581421
53+ }
54+ },
55+ {
56+ "doubleSided":true,
57+ "extensions":{
58+ "KHR_materials_specular":{
59+ "specularFactor":0
60+ },
61+ "KHR_materials_ior":{
62+ "ior":1
63+ }
64+ },
65+ "name":"ceiling.002",
66+ "pbrMetallicRoughness":{
67+ "baseColorFactor":[
68+ 0.7250000238418579,
69+ 0.7099999785423279,
70+ 0.6800000071525574,
71+ 1
72+ ],
73+ "metallicFactor":0,
74+ "roughnessFactor":0.8999999761581421
75+ }
76+ },
77+ {
78+ "doubleSided":true,
79+ "extensions":{
80+ "KHR_materials_specular":{
81+ "specularFactor":0
82+ },
83+ "KHR_materials_ior":{
84+ "ior":1
85+ }
86+ },
87+ "name":"backWall.002",
88+ "pbrMetallicRoughness":{
89+ "baseColorFactor":[
90+ 0.7250000238418579,
91+ 0.7099999785423279,
92+ 0.6800000071525574,
93+ 1
94+ ],
95+ "metallicFactor":0,
96+ "roughnessFactor":0.8999999761581421
97+ }
98+ },
99+ {
100+ "doubleSided":true,
101+ "extensions":{
102+ "KHR_materials_specular":{
103+ "specularFactor":0
104+ }
105+ },
106+ "name":"rightWall.002",
107+ "pbrMetallicRoughness":{
108+ "baseColorFactor":[
109+ 0.14000000059604645,
110+ 0.44999998807907104,
111+ 0.09099999815225601,
112+ 1
113+ ],
114+ "metallicFactor":0,
115+ "roughnessFactor":0.8999999761581421
116+ }
117+ },
118+ {
119+ "doubleSided":true,
120+ "extensions":{
121+ "KHR_materials_specular":{
122+ "specularFactor":0
123+ }
124+ },
125+ "name":"leftWall.002",
126+ "pbrMetallicRoughness":{
127+ "baseColorFactor":[
128+ 0.6299999952316284,
129+ 0.06499999761581421,
130+ 0.05000000074505806,
131+ 1
132+ ],
133+ "metallicFactor":0,
134+ "roughnessFactor":0.8999999761581421
135+ }
136+ },
137+ {
138+ "doubleSided":true,
139+ "emissiveFactor":[
140+ 1,
141+ 1,
142+ 1
143+ ],
144+ "extensions":{
145+ "KHR_materials_specular":{
146+ "specularFactor":0
147+ },
148+ "KHR_materials_ior":{
149+ "ior":1
150+ }
151+ },
152+ "name":"light.002",
153+ "pbrMetallicRoughness":{
154+ "baseColorFactor":[
155+ 0.7799999713897705,
156+ 0.7799999713897705,
157+ 0.7799999713897705,
158+ 1
159+ ],
160+ "metallicFactor":0,
161+ "roughnessFactor":0.8999999761581421
162+ }
163+ }
164+ ],
165+ "meshes":[
166+ {
167+ "name":"CornellBox-Empty-RG.002",
168+ "primitives":[
169+ {
170+ "attributes":{
171+ "POSITION":0,
172+ "NORMAL":1
173+ },
174+ "indices":2,
175+ "material":0
176+ },
177+ {
178+ "attributes":{
179+ "POSITION":3,
180+ "NORMAL":4
181+ },
182+ "indices":2,
183+ "material":1
184+ },
185+ {
186+ "attributes":{
187+ "POSITION":5,
188+ "NORMAL":6
189+ },
190+ "indices":2,
191+ "material":2
192+ },
193+ {
194+ "attributes":{
195+ "POSITION":7,
196+ "NORMAL":8
197+ },
198+ "indices":2,
199+ "material":3
200+ },
201+ {
202+ "attributes":{
203+ "POSITION":9,
204+ "NORMAL":10
205+ },
206+ "indices":2,
207+ "material":4
208+ },
209+ {
210+ "attributes":{
211+ "POSITION":11,
212+ "NORMAL":12
213+ },
214+ "indices":2,
215+ "material":5
216+ }
217+ ]
218+ }
219+ ],
220+ "accessors":[
221+ {
222+ "bufferView":0,
223+ "componentType":5126,
224+ "count":4,
225+ "max":[
226+ 1,
227+ 0.9900000095367432,
228+ 0
229+ ],
230+ "min":[
231+ -1.0099999904632568,
232+ -1.0399999618530273,
233+ 0
234+ ],
235+ "type":"VEC3"
236+ },
237+ {
238+ "bufferView":1,
239+ "componentType":5126,
240+ "count":4,
241+ "type":"VEC3"
242+ },
243+ {
244+ "bufferView":2,
245+ "componentType":5123,
246+ "count":6,
247+ "type":"SCALAR"
248+ },
249+ {
250+ "bufferView":3,
251+ "componentType":5126,
252+ "count":4,
253+ "max":[
254+ 1,
255+ 0.9900000095367432,
256+ -1.9900000095367432
257+ ],
258+ "min":[
259+ -1.0199999809265137,
260+ -1.0399999618530273,
261+ -1.9900000095367432
262+ ],
263+ "type":"VEC3"
264+ },
265+ {
266+ "bufferView":4,
267+ "componentType":5126,
268+ "count":4,
269+ "type":"VEC3"
270+ },
271+ {
272+ "bufferView":5,
273+ "componentType":5126,
274+ "count":4,
275+ "max":[
276+ 1,
277+ -1.0399999618530273,
278+ 0
279+ ],
280+ "min":[
281+ -1.0199999809265137,
282+ -1.0399999618530273,
283+ -1.9900000095367432
284+ ],
285+ "type":"VEC3"
286+ },
287+ {
288+ "bufferView":6,
289+ "componentType":5126,
290+ "count":4,
291+ "type":"VEC3"
292+ },
293+ {
294+ "bufferView":7,
295+ "componentType":5126,
296+ "count":4,
297+ "max":[
298+ 1,
299+ 0.9900000095367432,
300+ 0
301+ ],
302+ "min":[
303+ 1,
304+ -1.0399999618530273,
305+ -1.9900000095367432
306+ ],
307+ "type":"VEC3"
308+ },
309+ {
310+ "bufferView":8,
311+ "componentType":5126,
312+ "count":4,
313+ "type":"VEC3"
314+ },
315+ {
316+ "bufferView":9,
317+ "componentType":5126,
318+ "count":4,
319+ "max":[
320+ -0.9900000095367432,
321+ 0.9900000095367432,
322+ 0
323+ ],
324+ "min":[
325+ -1.0199999809265137,
326+ -1.0399999618530273,
327+ -1.9900000095367432
328+ ],
329+ "type":"VEC3"
330+ },
331+ {
332+ "bufferView":10,
333+ "componentType":5126,
334+ "count":4,
335+ "type":"VEC3"
336+ },
337+ {
338+ "bufferView":11,
339+ "componentType":5126,
340+ "count":4,
341+ "max":[
342+ 0.23000000417232513,
343+ 0.1599999964237213,
344+ -1.9800000190734863
345+ ],
346+ "min":[
347+ -0.23999999463558197,
348+ -0.2199999988079071,
349+ -1.9800000190734863
350+ ],
351+ "type":"VEC3"
352+ },
353+ {
354+ "bufferView":12,
355+ "componentType":5126,
356+ "count":4,
357+ "type":"VEC3"
358+ }
359+ ],
360+ "bufferViews":[
361+ {
362+ "buffer":0,
363+ "byteLength":48,
364+ "byteOffset":0,
365+ "target":34962
366+ },
367+ {
368+ "buffer":0,
369+ "byteLength":48,
370+ "byteOffset":48,
371+ "target":34962
372+ },
373+ {
374+ "buffer":0,
375+ "byteLength":12,
376+ "byteOffset":96,
377+ "target":34963
378+ },
379+ {
380+ "buffer":0,
381+ "byteLength":48,
382+ "byteOffset":108,
383+ "target":34962
384+ },
385+ {
386+ "buffer":0,
387+ "byteLength":48,
388+ "byteOffset":156,
389+ "target":34962
390+ },
391+ {
392+ "buffer":0,
393+ "byteLength":48,
394+ "byteOffset":204,
395+ "target":34962
396+ },
397+ {
398+ "buffer":0,
399+ "byteLength":48,
400+ "byteOffset":252,
401+ "target":34962
402+ },
403+ {
404+ "buffer":0,
405+ "byteLength":48,
406+ "byteOffset":300,
407+ "target":34962
408+ },
409+ {
410+ "buffer":0,
411+ "byteLength":48,
412+ "byteOffset":348,
413+ "target":34962
414+ },
415+ {
416+ "buffer":0,
417+ "byteLength":48,
418+ "byteOffset":396,
419+ "target":34962
420+ },
421+ {
422+ "buffer":0,
423+ "byteLength":48,
424+ "byteOffset":444,
425+ "target":34962
426+ },
427+ {
428+ "buffer":0,
429+ "byteLength":48,
430+ "byteOffset":492,
431+ "target":34962
432+ },
433+ {
434+ "buffer":0,
435+ "byteLength":48,
436+ "byteOffset":540,
437+ "target":34962
438+ }
439+ ],
440+ "buffers":[
441+ {
442+ "byteLength":588,
443+ "uri":"cornell_empty_rg.bin"
444+ }
445+ ]
446+}
A · public/roughness_metallic_0.jpg
+0, -0
A · public/roughness_metallic_1.jpg
+0, -0
A · src/bvh.ts
+331, -0 1@@ -0,0 +1,331 @@
2+import { Vec3 } from "gl-matrix";
3+
4+const MAX_BOUND = 999999;
5+
6+interface Triangle {
7+ cornerA: Vec3;
8+ cornerB: Vec3;
9+ cornerC: Vec3;
10+ centroid: Vec3;
11+}
12+
13+interface Bin {
14+ instanceCount: number;
15+ bounds: AABB;
16+}
17+
18+// just for documentation
19+// interface BVHNode {
20+// min: Vec3; // 12
21+// max: Vec3; // 12
22+// left: number; // 4
23+// instanceCount: number; // 4
24+// }
25+
26+class AABB {
27+ bmin: Vec3;
28+ bmax: Vec3;
29+ constructor() {
30+ this.bmin = Vec3.fromValues(MAX_BOUND, MAX_BOUND, MAX_BOUND);
31+ this.bmax = Vec3.fromValues(-MAX_BOUND, -MAX_BOUND, -MAX_BOUND);
32+ }
33+
34+ grow(p: Vec3) {
35+ Vec3.min(this.bmin, this.bmin, p);
36+ Vec3.max(this.bmax, this.bmax, p);
37+ }
38+
39+ growAABB(aabb: AABB) {
40+ // Only grow if the other AABB is valid.
41+ if (aabb.bmin[0] !== MAX_BOUND) {
42+ this.grow(aabb.bmin);
43+ this.grow(aabb.bmax);
44+ }
45+ }
46+
47+ area() {
48+ const e = Vec3.create();
49+ Vec3.subtract(e, this.bmax, this.bmin);
50+ // standard surface area measure (omitting the factor 2 is acceptable since SAH is relative)
51+ return e[0] * e[1] + e[1] * e[2] + e[2] * e[0];
52+ }
53+}
54+
55+class BVH {
56+ nodesMin: Float32Array; // min x,y,z for each node
57+ nodesMax: Float32Array; // max x,y,z for each node
58+ nodesLeft: Uint32Array;
59+ nodesInstanceCount: Uint32Array;
60+ nodesUsed: number;
61+ triangles: Triangle[];
62+ triIdx: Uint32Array;
63+
64+ constructor(triangles: Triangle[]) {
65+ this.triangles = triangles;
66+ this.triIdx = new Uint32Array(triangles.length);
67+ this.nodesUsed = 0;
68+ const maxNodes = 2 * triangles.length - 1;
69+ this.nodesMin = new Float32Array(maxNodes * 3);
70+ this.nodesMax = new Float32Array(maxNodes * 3);
71+ this.nodesLeft = new Uint32Array(maxNodes);
72+ this.nodesInstanceCount = new Uint32Array(maxNodes);
73+ }
74+
75+ construct() {
76+ for (let i = 0; i < this.triangles.length; i++) {
77+ this.triIdx[i] = i;
78+ }
79+ this.nodesInstanceCount[0] = this.triangles.length;
80+ this.nodesLeft[0] = 0; // root node
81+ this.nodesUsed = 1;
82+ this.bounding(0);
83+ this.subdivide(0);
84+ }
85+
86+ bounding(nodeIdx: number) {
87+ const off = nodeIdx * 3;
88+ // initialize the node's AABB.
89+ this.nodesMin[off + 0] = MAX_BOUND;
90+ this.nodesMin[off + 1] = MAX_BOUND;
91+ this.nodesMin[off + 2] = MAX_BOUND;
92+ this.nodesMax[off + 0] = -MAX_BOUND;
93+ this.nodesMax[off + 1] = -MAX_BOUND;
94+ this.nodesMax[off + 2] = -MAX_BOUND;
95+
96+ const count = this.nodesInstanceCount[nodeIdx];
97+ const start = this.nodesLeft[nodeIdx];
98+
99+ // temp vectors
100+ const minVec = Vec3.create();
101+ const maxVec = Vec3.create();
102+
103+ for (let i = 0; i < count; i++) {
104+ const tri = this.triangles[this.triIdx[start + i]];
105+
106+ // walk through each tri and update the bounds
107+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerA);
108+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerA);
109+ this.nodesMin[off + 0] = minVec[0];
110+ this.nodesMin[off + 1] = minVec[1];
111+ this.nodesMin[off + 2] = minVec[2];
112+ this.nodesMax[off + 0] = maxVec[0];
113+ this.nodesMax[off + 1] = maxVec[1];
114+ this.nodesMax[off + 2] = maxVec[2];
115+
116+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerB);
117+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerB);
118+ this.nodesMin[off + 0] = minVec[0];
119+ this.nodesMin[off + 1] = minVec[1];
120+ this.nodesMin[off + 2] = minVec[2];
121+ this.nodesMax[off + 0] = maxVec[0];
122+ this.nodesMax[off + 1] = maxVec[1];
123+ this.nodesMax[off + 2] = maxVec[2];
124+
125+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerC);
126+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerC);
127+ this.nodesMin[off + 0] = minVec[0];
128+ this.nodesMin[off + 1] = minVec[1];
129+ this.nodesMin[off + 2] = minVec[2];
130+ this.nodesMax[off + 0] = maxVec[0];
131+ this.nodesMax[off + 1] = maxVec[1];
132+ this.nodesMax[off + 2] = maxVec[2];
133+ }
134+ }
135+
136+ subdivide(nodeIdx: number) {
137+ // not enough primitives
138+ if (this.nodesInstanceCount[nodeIdx] <= 2) return;
139+
140+ let [split, axis, cost] = this.findBestPlane(nodeIdx);
141+
142+ // eval the parent node’s extent.
143+ const off = nodeIdx * 3;
144+ const extent = Vec3.create();
145+ Vec3.subtract(
146+ extent,
147+ [this.nodesMax[off + 0], this.nodesMax[off + 1], this.nodesMax[off + 2]],
148+ [this.nodesMin[off + 0], this.nodesMin[off + 1], this.nodesMin[off + 2]]
149+ );
150+ const parentArea = extent[0] * extent[1] + extent[1] * extent[2] + extent[2] * extent[0];
151+ const parentCost = this.nodesInstanceCount[nodeIdx] * parentArea;
152+
153+ // fallback to median split if SAH cost is not better
154+ if (cost >= parentCost) {
155+ let longestAxis = 0;
156+ if (extent[1] > extent[0]) longestAxis = 1;
157+ if (extent[2] > extent[longestAxis]) longestAxis = 2;
158+ const start = this.nodesLeft[nodeIdx];
159+ const count = this.nodesInstanceCount[nodeIdx];
160+ const centroids: number[] = [];
161+ for (let i = 0; i < count; i++) {
162+ centroids.push(this.triangles[this.triIdx[start + i]].centroid[longestAxis]);
163+ }
164+ centroids.sort((a, b) => a - b);
165+ split = centroids[Math.floor(count / 2)];
166+ axis = longestAxis;
167+ }
168+
169+ // partition primitives based on the chosen split
170+ let i = this.nodesLeft[nodeIdx];
171+ let j = i + this.nodesInstanceCount[nodeIdx] - 1;
172+ while (i <= j) {
173+ const tri = this.triangles[this.triIdx[i]];
174+ if (tri.centroid[axis] < split) {
175+ i++;
176+ } else {
177+ const tmp = this.triIdx[i];
178+ this.triIdx[i] = this.triIdx[j];
179+ this.triIdx[j] = tmp;
180+ j--;
181+ }
182+ }
183+ const leftCount = i - this.nodesLeft[nodeIdx];
184+ if (leftCount === 0 || leftCount === this.nodesInstanceCount[nodeIdx]) return;
185+
186+ // construct child nodes.
187+ const leftIdx = this.nodesUsed++;
188+ const rightIdx = this.nodesUsed++;
189+
190+ this.nodesLeft[leftIdx] = this.nodesLeft[nodeIdx];
191+ this.nodesInstanceCount[leftIdx] = leftCount;
192+ this.nodesLeft[rightIdx] = i;
193+ this.nodesInstanceCount[rightIdx] = this.nodesInstanceCount[nodeIdx] - leftCount;
194+
195+ // internal node
196+ this.nodesLeft[nodeIdx] = leftIdx;
197+ this.nodesInstanceCount[nodeIdx] = 0;
198+
199+ // keep going
200+ this.bounding(leftIdx);
201+ this.bounding(rightIdx);
202+ this.subdivide(leftIdx);
203+ this.subdivide(rightIdx);
204+ }
205+
206+ findBestPlane(nodeIdx: number): [number, number, number] {
207+ let bestAxis = -1;
208+ let bestSplit = 0;
209+ let bestCost = Infinity;
210+
211+ const count = this.nodesInstanceCount[nodeIdx];
212+ const start = this.nodesLeft[nodeIdx];
213+
214+ // eval centroid bounds
215+ const centroidMin = [Infinity, Infinity, Infinity];
216+ const centroidMax = [-Infinity, -Infinity, -Infinity];
217+ for (let i = 0; i < count; i++) {
218+ const tri = this.triangles[this.triIdx[start + i]];
219+ for (let axis = 0; axis < 3; axis++) {
220+ centroidMin[axis] = Math.min(centroidMin[axis], tri.centroid[axis]);
221+ centroidMax[axis] = Math.max(centroidMax[axis], tri.centroid[axis]);
222+ }
223+ }
224+
225+ // fallback if this centroid has a degenerate centroid distributions
226+ const EPSILON = 1e-5;
227+ let degenerate = false;
228+ for (let axis = 0; axis < 3; axis++) {
229+ if (Math.abs(centroidMax[axis] - centroidMin[axis]) < EPSILON) {
230+ degenerate = true;
231+ break;
232+ }
233+ }
234+ if (degenerate) {
235+ // use median split along the longest axis of actual bounds
236+ let longestAxis = 0;
237+ const actualMin = [Infinity, Infinity, Infinity];
238+ const actualMax = [-Infinity, -Infinity, -Infinity];
239+ for (let i = 0; i < count; i++) {
240+ const tri = this.triangles[this.triIdx[start + i]];
241+ for (let axis = 0; axis < 3; axis++) {
242+ actualMin[axis] = Math.min(actualMin[axis], tri.cornerA[axis], tri.cornerB[axis], tri.cornerC[axis]);
243+ actualMax[axis] = Math.max(actualMax[axis], tri.cornerA[axis], tri.cornerB[axis], tri.cornerC[axis]);
244+ }
245+ }
246+ const extent = [actualMax[0] - actualMin[0], actualMax[1] - actualMin[1], actualMax[2] - actualMin[2]];
247+ if (extent[1] > extent[0]) longestAxis = 1;
248+ if (extent[2] > extent[longestAxis]) longestAxis = 2;
249+ const centroids: number[] = [];
250+ for (let i = 0; i < count; i++) {
251+ centroids.push(this.triangles[this.triIdx[start + i]].centroid[longestAxis]);
252+ }
253+ centroids.sort((a, b) => a - b);
254+ bestSplit = centroids[Math.floor(count / 2)];
255+ bestAxis = longestAxis;
256+ return [bestSplit, bestAxis, bestCost];
257+ }
258+
259+ // use adaptive binning (bin count based on number of primitives, clamped between 8 and 32)
260+ const BINS = Math.max(8, Math.min(32, count));
261+ const bins: Bin[] = Array.from({ length: BINS }, () => ({
262+ instanceCount: 0,
263+ bounds: new AABB(),
264+ }));
265+
266+ // for each axis, evaluate candidate splits.
267+ for (let axis = 0; axis < 3; axis++) {
268+ const axisMin = centroidMin[axis];
269+ const axisMax = centroidMax[axis];
270+ if (axisMax === axisMin) continue; // skip degenerate axis
271+
272+ const axisScale = BINS / (axisMax - axisMin);
273+ // reset bins for this axis.
274+ for (let i = 0; i < BINS; i++) {
275+ bins[i].instanceCount = 0;
276+ bins[i].bounds = new AABB();
277+ }
278+
279+ // distribute primitives into bins.
280+ for (let i = 0; i < count; i++) {
281+ const tri = this.triangles[this.triIdx[start + i]];
282+ let binIDX = Math.floor((tri.centroid[axis] - axisMin) * axisScale);
283+ if (binIDX >= BINS) binIDX = BINS - 1;
284+ bins[binIDX].instanceCount++;
285+ bins[binIDX].bounds.grow(tri.cornerA);
286+ bins[binIDX].bounds.grow(tri.cornerB);
287+ bins[binIDX].bounds.grow(tri.cornerC);
288+ }
289+
290+
291+ // compute cumulative sums from left and right
292+ const leftCount = new Array(BINS - 1).fill(0);
293+ const leftArea = new Array(BINS - 1).fill(0);
294+ const rightCount = new Array(BINS - 1).fill(0);
295+ const rightArea = new Array(BINS - 1).fill(0);
296+
297+ let leftBox = new AABB();
298+ for (let i = 0, sum = 0; i < BINS - 1; i++) {
299+ sum += bins[i].instanceCount;
300+ leftCount[i] = sum;
301+ leftBox.growAABB(bins[i].bounds);
302+ leftArea[i] = leftBox.area();
303+ }
304+ let rightBox = new AABB();
305+ for (let i = BINS - 1, sum = 0; i > 0; i--) {
306+ sum += bins[i].instanceCount;
307+ rightCount[i - 1] = sum;
308+ rightBox.growAABB(bins[i].bounds);
309+ rightArea[i - 1] = rightBox.area();
310+ }
311+
312+ // highly axis aligned polygons like lucy and sponza fail
313+ // duct taped solution to handle degen cases
314+ const binWidth = (axisMax - axisMin) / BINS;
315+ // eval candidate splits.
316+ for (let i = 0; i < BINS - 1; i++) {
317+ // small epsilon jitter to help break ties.
318+ const jitter = 1e-6;
319+ const candidatePos = axisMin + binWidth * (i + 1) + jitter;
320+ const cost = leftCount[i] * leftArea[i] + rightCount[i] * rightArea[i];
321+ if (cost < bestCost) {
322+ bestCost = cost;
323+ bestAxis = axis;
324+ bestSplit = candidatePos;
325+ }
326+ }
327+ }
328+ return [bestSplit, bestAxis, bestCost];
329+ }
330+}
331+
332+export default BVH;
A · src/camera.ts
+139, -0 1@@ -0,0 +1,139 @@
2+import { Vec3, Mat4 } from "gl-matrix";
3+
4+export type CameraState = {
5+ position: Float32Array; // [x, y, z]
6+ rotation: Float32Array; // [theta, phi]
7+ view: Float32Array;
8+ inverseView: Float32Array;
9+ projection: Float32Array;
10+ currentViewProj: Float32Array;
11+ previousViewProj: Float32Array;
12+ dirty: boolean;
13+};
14+
15+const moveVec = Vec3.create();
16+const lastPosition = Vec3.create();
17+const lastRotation = new Float32Array(2);
18+const rotateDelta = new Float32Array(2);
19+const keyState = new Set<string>();
20+const sprintMultiplier = 2.5;
21+
22+let isPointerLocked = false;
23+let oldTime = 0;
24+
25+export function createCamera(canvas: HTMLCanvasElement): CameraState {
26+ const position = Vec3.fromValues(0, -75, 20);
27+ const view = Mat4.create();
28+ const projection = Mat4.create();
29+ const inverseView = Mat4.create();
30+
31+ const lookAtTarget = Vec3.fromValues(0, 0, 20);
32+ const up = Vec3.fromValues(0, 0, 1);
33+ Mat4.lookAt(view, position, lookAtTarget, up);
34+ Mat4.invert(inverseView, view);
35+ Mat4.perspectiveZO(projection, Math.PI / 4, canvas.width / canvas.height, 0.1, 100);
36+
37+ const forward = Vec3.create();
38+ Vec3.subtract(forward, lookAtTarget, position);
39+ Vec3.normalize(forward, forward);
40+ const phi = Math.asin(forward[2]);
41+ const theta = Math.atan2(forward[0], forward[1]);
42+
43+ Vec3.copy(lastPosition, position);
44+ lastRotation[0] = theta;
45+ lastRotation[1] = phi;
46+
47+ return {
48+ position,
49+ rotation: new Float32Array([theta, phi]),
50+ view,
51+ inverseView,
52+ projection,
53+ currentViewProj: Mat4.create(),
54+ previousViewProj: Mat4.create(),
55+ dirty: true,
56+ };
57+}
58+
59+export function updateCamera(cam: CameraState): void {
60+ const now = performance.now() * 0.001;
61+ const dt = now - oldTime;
62+ oldTime = now;
63+
64+ const theta = cam.rotation[0] += rotateDelta[0] * dt * 60;
65+ const phi = cam.rotation[1] -= rotateDelta[1] * dt * 60;
66+ cam.rotation[1] = Math.min(88, Math.max(-88, cam.rotation[1]));
67+ rotateDelta[0] = rotateDelta[1] = 0;
68+
69+ updateMovementInput();
70+
71+ const scaledMove = Vec3.create();
72+ Vec3.scale(scaledMove, moveVec, dt);
73+
74+ const moved = Vec3.length(scaledMove) > 1e-5;
75+ const rotated = theta !== lastRotation[0] || phi !== lastRotation[1];
76+ const changedPos = !Vec3.exactEquals(cam.position, lastPosition);
77+ cam.dirty = moved || rotated || changedPos;
78+
79+ if (!cam.dirty) return;
80+
81+ const forwards = Vec3.fromValues(
82+ Math.sin(toRad(theta)) * Math.cos(toRad(phi)),
83+ Math.cos(toRad(theta)) * Math.cos(toRad(phi)),
84+ Math.sin(toRad(phi))
85+ );
86+ const right = Vec3.create();
87+ Vec3.cross(right, forwards, [0, 0, 1]);
88+ Vec3.normalize(right, right);
89+
90+ const up = Vec3.create();
91+ Vec3.cross(up, right, forwards);
92+ Vec3.normalize(up, up);
93+
94+ Vec3.scaleAndAdd(cam.position, cam.position, forwards, scaledMove[0]);
95+ Vec3.scaleAndAdd(cam.position, cam.position, right, scaledMove[1]);
96+ Vec3.scaleAndAdd(cam.position, cam.position, up, scaledMove[2]);
97+
98+ const target = Vec3.create();
99+ Vec3.add(target, cam.position, forwards);
100+ Mat4.lookAt(cam.view, cam.position, target, up);
101+ Mat4.invert(cam.inverseView, cam.view);
102+
103+ Vec3.copy(lastPosition, cam.position);
104+ lastRotation[0] = theta;
105+ lastRotation[1] = phi;
106+}
107+
108+export function updateMovementInput(): void {
109+ moveVec[0] = moveVec[1] = moveVec[2] = 0;
110+ const speed = keyState.has("shift") ? 10 * sprintMultiplier : 10;
111+
112+ if (keyState.has("w")) moveVec[0] += speed;
113+ if (keyState.has("s")) moveVec[0] -= speed;
114+ if (keyState.has("d")) moveVec[1] += speed;
115+ if (keyState.has("a")) moveVec[1] -= speed;
116+ if (keyState.has("q")) moveVec[2] += speed;
117+ if (keyState.has("e")) moveVec[2] -= speed;
118+}
119+
120+function toRad(deg: number): number {
121+ return (deg * Math.PI) / 180;
122+}
123+
124+export function setupCameraInput(canvas: HTMLCanvasElement): void {
125+ canvas.addEventListener("click", () => canvas.requestPointerLock());
126+
127+ document.addEventListener("pointerlockchange", () => {
128+ isPointerLocked = document.pointerLockElement != null;
129+ });
130+
131+ window.addEventListener("keydown", e => keyState.add(e.key.toLowerCase()));
132+ window.addEventListener("keyup", e => keyState.delete(e.key.toLowerCase()));
133+
134+ window.addEventListener("mousemove", (e) => {
135+ if (!isPointerLocked) return;
136+ const sensitivity = 0.5;
137+ rotateDelta[0] = e.movementX * sensitivity;
138+ rotateDelta[1] = e.movementY * sensitivity;
139+ });
140+}
A · src/gltf.ts
+394, -0 1@@ -0,0 +1,394 @@
2+import { load } from "@loaders.gl/core";
3+import { GLTFImagePostprocessed, GLTFLoader, GLTFMeshPrimitivePostprocessed, GLTFPostprocessed, postProcessGLTF } from "@loaders.gl/gltf";
4+import { Mat3, Mat4, Mat4Like, Quat, QuatLike, Vec3, Vec3Like } from "gl-matrix";
5+
6+interface Triangle {
7+ centroid: number[];
8+ cornerA: number[];
9+ cornerB: number[];
10+ cornerC: number[];
11+ normalA: number[];
12+ normalB: number[];
13+ normalC: number[];
14+ mat: number;
15+ uvA: number[];
16+ uvB: number[];
17+ uvC: number[];
18+ tangentA: number[];
19+ tangentB: number[];
20+ tangentC: number[];
21+}
22+
23+interface ProcessedMaterial {
24+ baseColorFactor: number[],
25+ baseColorTexture: number, // idx
26+ metallicFactor: number,
27+ roughnessFactor: number,
28+ metallicRoughnessTexture: number, //idx
29+ normalTexture: number, //idx
30+ emissiveFactor: number[],
31+ emissiveTexture: number, //idx
32+ alphaMode: number, // parseAlphaMode
33+ alphaCutoff: number,
34+ doubleSided: number,
35+};
36+
37+interface ProcessedTexture {
38+ id: String,
39+ sampler: GPUSampler,
40+ texture: GPUTexture,
41+ view: GPUTextureView,
42+ source: GLTFImagePostprocessed,
43+ samplerDescriptor: GPUSamplerDescriptor,
44+}
45+
46+export class GLTF2 {
47+ triangles: Array<Triangle>;
48+ materials: Array<ProcessedMaterial>; // could make material more explicit here, like with textures
49+ textures: Array<ProcessedTexture>;
50+ device: GPUDevice;
51+ gltfData!: GLTFPostprocessed;
52+ url: string;
53+ scale: number[];
54+ position: number[];
55+ rotation?: number[];
56+
57+ // pre allocating these here, faster that way? Intuitevely, I could be wrong.
58+ tempVec3_0: Vec3 = Vec3.create();
59+ tempVec3_1: Vec3 = Vec3.create();
60+ tempVec3_2: Vec3 = Vec3.create();
61+ tempVec3_3: Vec3 = Vec3.create();
62+ tempVec3_4: Vec3 = Vec3.create();
63+ tempMat4_0: Mat4 = Mat4.create();
64+ tempMat4_1: Mat4 = Mat4.create();
65+ tempMat3_0: Mat3 = Mat3.create();
66+
67+ constructor(device: GPUDevice, url: string, scale: number[], position: number[], rotation?: number[]) {
68+ this.triangles = [];
69+ this.materials = [];
70+ this.textures = [];
71+ this.device = device;
72+ this.url = url;
73+ this.scale = scale;
74+ this.position = position;
75+ this.rotation = rotation;
76+ }
77+ async initialize() {
78+ const t = await load(this.url, GLTFLoader);
79+ this.gltfData = postProcessGLTF(t);
80+ this.traverseNodes();
81+ return [this.triangles, this.materials, this.textures];
82+ }
83+ // some data swizzling swash buckling utils
84+ transformVec3(inputVec: ArrayLike<number>, matrix: Mat4): number[] {
85+ const v = this.tempVec3_0;
86+ Vec3.set(v, inputVec[0], inputVec[1], inputVec[2]);
87+ Vec3.transformMat4(v, v, matrix);
88+ return [v[0], v[1], v[2]]; // Return new array copy
89+ }
90+ transformNormal(inputNormal: ArrayLike<number>, transformMatrix: Mat4): number[] {
91+ const normalMatrix = this.tempMat3_0; // Reused Mat3
92+ const tempMatrix = this.tempMat4_1; // Use tempMat4_1 to avoid conflict with finalTransform
93+ const transformedNormal = this.tempVec3_0; // Reused Vec3 for result
94+ const inputNormalVec = this.tempVec3_1; // Reused Vec3 for input
95+
96+ // calculate transpose(invert(transformMatrix))
97+ // tempMat4_1 as scratch space to avoid clobbering tempMat4_0 (finalTransform)
98+ Mat4.invert(tempMatrix, transformMatrix);
99+ Mat4.transpose(tempMatrix, tempMatrix);
100+
101+ // upper-left 3x3 submatrix
102+ Mat3.fromMat4(normalMatrix, tempMatrix);
103+
104+ // normal into reusable Vec3
105+ Vec3.set(inputNormalVec, inputNormal[0], inputNormal[1], inputNormal[2]);
106+
107+ // transfrom that normal
108+ Vec3.transformMat3(transformedNormal, inputNormalVec, normalMatrix);
109+ Vec3.normalize(transformedNormal, transformedNormal);
110+
111+ // new array copy
112+ return [transformedNormal[0], transformedNormal[1], transformedNormal[2]];
113+ }
114+ parseAlphaMode(alphaMode: string) {
115+ if (alphaMode === "MASK") { return 1 }
116+ return 2
117+ }
118+
119+
120+ // could break this up
121+ extractTriangles(primitive: GLTFMeshPrimitivePostprocessed, transform: Mat4, targetArray: Array<Triangle>) {
122+ const positions = primitive.attributes["POSITION"].value;
123+ const indicesData = primitive.indices ? primitive.indices.value : null;
124+ const numVertices = positions.length / 3;
125+ const indices = indicesData ?? (() => {
126+ const generatedIndices = new Uint32Array(numVertices);
127+ for (let i = 0; i < numVertices; i++) generatedIndices[i] = i;
128+ return generatedIndices;
129+ })();
130+ const normals = primitive.attributes["NORMAL"]
131+ ? primitive.attributes["NORMAL"].value
132+ : null;
133+ const uvCoords = primitive.attributes["TEXCOORD_0"]
134+ ? primitive.attributes["TEXCOORD_0"].value
135+ : null;
136+ const tangents = primitive.attributes["TANGENT"]
137+ ? primitive.attributes["TANGENT"].value
138+ : null;
139+
140+ const mat = parseInt(primitive.material?.id.match(/\d+$/)?.[0] ?? "-1");
141+
142+ // ensure these don't clash with temps used in transformNormal/transformVec3
143+ // if called within the face normal logic (they aren't).
144+ const vA = this.tempVec3_1; // maybe use distinct temps if needed, but seems ok
145+ const vB = this.tempVec3_2;
146+ const vC = this.tempVec3_3;
147+ const edge1 = this.tempVec3_4;
148+ const edge2 = this.tempVec3_0; // tempVec3_0 reused safely after position transforms
149+ const faceNormal = this.tempVec3_1; // tempVec3_1 reused safely after normal transforms or for input
150+
151+ const defaultUV = [0, 0];
152+ const defaultTangent = [1, 0, 0, 1];
153+
154+ for (let i = 0; i < indices.length; i += 3) {
155+ const ai = indices[i];
156+ const bi = indices[i + 1];
157+ const ci = indices[i + 2];
158+
159+ const posA = [positions[ai * 3], positions[ai * 3 + 1], positions[ai * 3 + 2]];
160+ const posB = [positions[bi * 3], positions[bi * 3 + 1], positions[bi * 3 + 2]];
161+ const posC = [positions[ci * 3], positions[ci * 3 + 1], positions[ci * 3 + 2]];
162+
163+ // transform positions uses tempVec3_0 internally
164+ const cornerA = this.transformVec3(posA, transform);
165+ const cornerB = this.transformVec3(posB, transform);
166+ const cornerC = this.transformVec3(posC, transform);
167+
168+ let normalA: number[], normalB: number[], normalC: number[];
169+ if (normals) {
170+ // transform normals uses tempVec3_0, tempVec3_1 internally
171+ normalA = this.transformNormal([normals[ai * 3], normals[ai * 3 + 1], normals[ai * 3 + 2]], transform);
172+ normalB = this.transformNormal([normals[bi * 3], normals[bi * 3 + 1], normals[bi * 3 + 2]], transform);
173+ normalC = this.transformNormal([normals[ci * 3], normals[ci * 3 + 1], normals[ci * 3 + 2]], transform);
174+ } else {
175+ // compute fallback flat face normal
176+ Vec3.set(vA, cornerA[0], cornerA[1], cornerA[2]);
177+ Vec3.set(vB, cornerB[0], cornerB[1], cornerB[2]);
178+ Vec3.set(vC, cornerC[0], cornerC[1], cornerC[2]);
179+
180+ Vec3.subtract(edge1, vB, vA);
181+ Vec3.subtract(edge2, vC, vA);
182+ Vec3.cross(faceNormal, edge1, edge2);
183+ Vec3.normalize(faceNormal, faceNormal);
184+
185+ const normalArray = [faceNormal[0], faceNormal[1], faceNormal[2]];
186+ normalA = normalArray;
187+ normalB = normalArray;
188+ normalC = normalArray;
189+ }
190+
191+ const uvA = uvCoords ? [uvCoords[ai * 2], uvCoords[ai * 2 + 1]] : defaultUV;
192+ const uvB = uvCoords ? [uvCoords[bi * 2], uvCoords[bi * 2 + 1]] : defaultUV;
193+ const uvC = uvCoords ? [uvCoords[ci * 2], uvCoords[ci * 2 + 1]] : defaultUV;
194+
195+ const tangentA = tangents ? [tangents[ai * 4], tangents[ai * 4 + 1], tangents[ai * 4 + 2], tangents[ai * 4 + 3]] : defaultTangent;
196+ const tangentB = tangents ? [tangents[bi * 4], tangents[bi * 4 + 1], tangents[bi * 4 + 2], tangents[bi * 4 + 3]] : defaultTangent;
197+ const tangentC = tangents ? [tangents[ci * 4], tangents[ci * 4 + 1], tangents[ci * 4 + 2], tangents[ci * 4 + 3]] : defaultTangent;
198+
199+ const centroid = [
200+ (cornerA[0] + cornerB[0] + cornerC[0]) / 3,
201+ (cornerA[1] + cornerB[1] + cornerC[1]) / 3,
202+ (cornerA[2] + cornerB[2] + cornerC[2]) / 3,
203+ ];
204+
205+ targetArray.push({
206+ centroid,
207+ cornerA, cornerB, cornerC,
208+ normalA, normalB, normalC, mat,
209+ uvA, uvB, uvC,
210+ tangentA, tangentB, tangentC,
211+ });
212+ }
213+ }
214+
215+ traverseNodes() {
216+ // texture processing
217+ if (this.gltfData.textures) {
218+ this.gltfData.textures.forEach((texture) => {
219+ if (!texture.source?.image) {
220+ // empty textures are handled on atlas creation
221+ return;
222+ }
223+ const gpuTexture = this.device.createTexture({
224+ size: {
225+ width: texture.source.image.width ?? 0,
226+ height: texture.source.image.height ?? 0,
227+ depthOrArrayLayers: 1,
228+ },
229+ format: "rgba8unorm",
230+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
231+ });
232+
233+ const view = gpuTexture.createView({ format: "rgba8unorm" });
234+
235+ // TODO: Process gltfData.samplers[texture.sampler] if it exists
236+ let samplerDescriptor: GPUSamplerDescriptor = {
237+ magFilter: "linear", minFilter: "linear",
238+ addressModeU: "repeat", addressModeV: "repeat",
239+ };
240+ const sampler = this.device.createSampler(samplerDescriptor);
241+ this.textures.push({
242+ id: texture.id,
243+ texture: gpuTexture,
244+ view: view,
245+ sampler: sampler,
246+ source: texture.source,
247+ samplerDescriptor: samplerDescriptor
248+ });
249+ });
250+ }
251+ if (this.gltfData.materials) {
252+ this.materials = this.gltfData.materials.map(mat => {
253+ return {
254+ baseColorFactor: mat.pbrMetallicRoughness?.baseColorFactor ?? [1.0, 1.0, 1.0, 1.0],
255+ baseColorTexture: mat.pbrMetallicRoughness?.baseColorTexture?.index ?? -1,
256+ metallicFactor: mat.pbrMetallicRoughness?.metallicFactor ?? 1.0,
257+ roughnessFactor: mat.pbrMetallicRoughness?.roughnessFactor ?? 1.0,
258+ metallicRoughnessTexture: mat.pbrMetallicRoughness?.metallicRoughnessTexture?.index ?? -1,
259+ normalTexture: mat.normalTexture?.index ?? -1,
260+ emissiveFactor: mat.emissiveFactor ?? [0.0, 0.0, 0.0],
261+ emissiveTexture: mat.emissiveTexture?.index ?? -1,
262+ alphaMode: mat.alphaMode ? this.parseAlphaMode(mat.alphaMode) : 0,
263+ alphaCutoff: mat.alphaCutoff ?? 0.5,
264+ doubleSided: mat.doubleSided ? 1 : 0,
265+ };
266+ });
267+ }
268+
269+ // initial node transforms
270+ const finalTransform = this.tempMat4_0; // reused for final calc per node
271+ const nodeLocalTransform = this.tempMat4_1; // reused for local calc per node
272+ const tMat = Mat4.create();
273+ const rMat = Mat4.create();
274+ const sMat = Mat4.create();
275+ const tMatCustom = Mat4.create();
276+ const rMatCustom = Mat4.create();
277+ const sMatCustom = Mat4.create();
278+ const yToZUp = Mat4.fromValues(
279+ 1, 0, 0, 0,
280+ 0, 0, 1, 0,
281+ 0, -1, 0, 0,
282+ 0, 0, 0, 1
283+ );
284+ // scene transforms
285+ const sceneTransform = Mat4.create();
286+ const sc_translation = this.position || [0, 0, 0];
287+ const sc_rotation = this.rotation || [0, 0, 0, 1];
288+ const sc_scale = this.scale || [1, 1, 1];
289+ Mat4.fromTranslation(tMatCustom, sc_translation as Vec3Like);
290+ Quat.normalize(rMatCustom, sc_rotation as QuatLike);
291+ Mat4.fromQuat(rMatCustom, rMatCustom);
292+ Mat4.fromScaling(sMatCustom, sc_scale as Vec3Like);
293+ Mat4.multiply(sceneTransform, rMatCustom, sMatCustom);
294+ Mat4.multiply(sceneTransform, tMatCustom, sceneTransform);
295+
296+ const meshMap = new Map(this.gltfData.meshes.map(m => [m.id, m]));
297+
298+ for (const node of this.gltfData.nodes) {
299+ if (!node.mesh?.id) continue;
300+ const mesh = meshMap.get(node.mesh.id);
301+ if (!mesh) continue;
302+ Mat4.identity(nodeLocalTransform);
303+ if (node.matrix) {
304+ Mat4.copy(nodeLocalTransform, node.matrix as Mat4Like);
305+ } else {
306+ const nodeTranslation = node.translation || [0, 0, 0];
307+ const nodeRotation = node.rotation || [0, 0, 0, 1];
308+ const nodeScale = node.scale || [1, 1, 1];
309+ Mat4.fromTranslation(tMat, nodeTranslation as Vec3Like);
310+ Mat4.fromQuat(rMat, nodeRotation as QuatLike);
311+ Mat4.fromScaling(sMat, nodeScale as Vec3Like);
312+ Mat4.multiply(nodeLocalTransform, rMat, sMat);
313+ Mat4.multiply(nodeLocalTransform, tMat, nodeLocalTransform);
314+ }
315+
316+ // finalTransform = sceneTransform * yToZUp * nodeLocalTransform
317+ Mat4.multiply(finalTransform, yToZUp, nodeLocalTransform);
318+ Mat4.multiply(finalTransform, sceneTransform, finalTransform);
319+
320+ mesh.primitives.forEach((primitive: GLTFMeshPrimitivePostprocessed) => {
321+ this.extractTriangles(primitive, finalTransform, this.triangles);
322+ });
323+ }
324+ }
325+}
326+
327+export function combineGLTFs(gltfs: GLTF2[]) {
328+ const triangles = [];
329+ const materials = [];
330+ const textures = [];
331+
332+ let textureOffset = 0;
333+ let materialOffset = 0;
334+ let largestTextureDimensions = { width: 0, height: 0 };
335+
336+ // offset idx
337+ const offsetIdx = (idx: any) => {
338+ return (typeof idx === 'number' && idx >= 0) ? idx + textureOffset : idx; // Keep original if invalid index
339+ };
340+
341+ for (let i = 0; i < gltfs.length; i++) {
342+ const gltf = gltfs[i];
343+ const texCount = gltf.textures ? gltf.textures.length : 0;
344+ const matCount = gltf.materials ? gltf.materials.length : 0;
345+ // just append the textures for now
346+ if (gltf.textures && texCount > 0) {
347+ for (let t = 0; t < texCount; t++) {
348+ const texture = gltf.textures[t];
349+ let texHeight = texture.source.image.height as number;
350+ let texWidth = texture.source.image.width as number;
351+ textures.push(texture);
352+ if (texWidth > largestTextureDimensions.width) {
353+ largestTextureDimensions.width = texWidth;
354+ }
355+ if (texHeight > largestTextureDimensions.height) {
356+ largestTextureDimensions.height = texHeight;
357+ }
358+ }
359+ }
360+ if (gltf.materials && matCount > 0) {
361+ for (let m = 0; m < matCount; m++) {
362+ const src = gltf.materials[m];
363+ materials.push({
364+ alphaCutoff: src.alphaCutoff,
365+ alphaMode: src.alphaMode,
366+ baseColorFactor: src.baseColorFactor,
367+ baseColorTexture: offsetIdx(src.baseColorTexture),
368+ doubleSided: src.doubleSided,
369+ emissiveFactor: src.emissiveFactor,
370+ emissiveTexture: offsetIdx(src.emissiveTexture),
371+ metallicFactor: src.metallicFactor,
372+ metallicRoughnessTexture: offsetIdx(src.metallicRoughnessTexture),
373+ normalTexture: offsetIdx(src.normalTexture),
374+ roughnessFactor: src.roughnessFactor
375+ });
376+ }
377+ }
378+ // update idx if needed
379+ if (gltf.triangles) {
380+ for (let t = 0; t < gltf.triangles.length; t++) {
381+ const tri = gltf.triangles[t];
382+ const triCopy = Object.create(tri);
383+ if (tri.mat >= 0) {
384+ triCopy.mat = tri.mat + materialOffset;
385+ } else {
386+ triCopy.mat = -1;
387+ }
388+ triangles.push(triCopy);
389+ }
390+ }
391+ textureOffset += texCount;
392+ materialOffset += matCount;
393+ }
394+ return { triangles, materials, textures, largestTextureDimensions };
395+}
A · src/main.ts
+643, -0 1@@ -0,0 +1,643 @@
2+import { Vec3 } from "gl-matrix";
3+import BVH from "./bvh.ts";
4+import { createCamera, setupCameraInput, updateCamera, updateMovementInput } from "./camera.ts";
5+import { GLTF2, combineGLTFs } from "./gltf.ts";
6+import { InitPane } from "./pane.ts";
7+import kernel from "./shaders/main.wgsl";
8+import viewport from "./shaders/viewport.wgsl";
9+
10+declare global {
11+ interface Window {
12+ framecount: number;
13+ }
14+}
15+
16+// init device
17+const canvas = document.querySelector("canvas") as HTMLCanvasElement;
18+const adapter = (await navigator.gpu.requestAdapter()) as GPUAdapter;
19+if (!navigator.gpu) {
20+ throw new Error("WebGPU not supported on this browser.");
21+}
22+if (!adapter) {
23+ throw new Error("No appropriate GPUAdapter found.");
24+}
25+const device = await adapter.requestDevice({
26+ requiredFeatures: ['timestamp-query']
27+});
28+
29+const width = canvas.clientWidth;
30+const height = canvas.clientHeight;
31+canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
32+canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
33+const context = canvas.getContext("webgpu") as GPUCanvasContext;
34+const format = navigator.gpu.getPreferredCanvasFormat();
35+context.configure({
36+ device,
37+ format: format,
38+});
39+
40+// compose scene -> triangles -> BVH -> textures
41+const x = new GLTF2(device, "Duck.gltf", [0.1, 0.1, 0.1], [-13, -1, -0.34], [0, 0, -1.25, 1]);
42+const y = new GLTF2(device, "cornell_empty_rg.gltf", [20, 20, 20], [0, 0, 0.01], [0,0,0,0])
43+const z = new GLTF2(device, "EnvironmentTest.gltf", [1.8, 1.8, 1.8], [0, 15, 25], [0, 0, 0, 0]);
44+
45+await x.initialize()
46+await y.initialize()
47+await z.initialize()
48+const t = combineGLTFs([x,y,z]);
49+let ab = new BVH(t.triangles);
50+ab.construct();
51+const hasTextures = t.textures && t.textures.length > 0;
52+const textureCount = hasTextures ? t.textures.length : 0;
53+const textureSizes = new Float32Array(textureCount * 4); // [width, height, invWidth, invHeight] per texture
54+console.log(t.triangles)
55+// viewport texture, rgba32float; we store full fat HDR and tonemap it in this
56+const viewportTexture = device.createTexture({
57+ size: {
58+ width: canvas.width,
59+ height: canvas.height,
60+ },
61+ format: "rgba32float",
62+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
63+});
64+const viewportTextureColorBuffer = viewportTexture.createView();
65+
66+
67+// offsets for buffer data
68+const MaterialSize = 64;
69+const materialData = new Float32Array(t.materials.length * (MaterialSize / 4));
70+const MaterialInfo = {
71+ albedo: { type: Float32Array, byteOffset: 0, length: 4 },
72+ metallic: { type: Float32Array, byteOffset: 16, length: 1 },
73+ alphaMode: { type: Float32Array, byteOffset: 20, length: 1 },
74+ alphaCutoff: { type: Float32Array, byteOffset: 24, length: 1 },
75+ doubleSided: { type: Float32Array, byteOffset: 28, length: 1 },
76+ emission: { type: Float32Array, byteOffset: 32, length: 3 },
77+ roughness: { type: Float32Array, byteOffset: 44, length: 1 },
78+ baseColorTexture: { type: Float32Array, byteOffset: 48, length: 1 },
79+ normalTexture: { type: Float32Array, byteOffset: 52, length: 1 },
80+ metallicRoughnessTexture: { type: Float32Array, byteOffset: 56, length: 1 },
81+ emissiveTexture: { type: Float32Array, byteOffset: 60, length: 1 },
82+};
83+
84+// NB: Very fat. Trimming these to vert should be (4*3) * 3 + 12 = 48.
85+// at the point if it's just 48, might be cheaper to get rid of the triangle indicies. Skip the BVH -> tri_index lookup.
86+// and then resolve the shading data with tri indicies? This would still need material index thought? To do alpha tests in trace()?
87+// TODO: Trim these to only verts, the material index, and the shading index. Move the rest to shadingData[]
88+// Could also, let this be. Skip the verts here and move verts directly into the BVH. And make sure the indices line up before passing it in.
89+const TriangleSize = 176;
90+const TriangleData = new Float32Array(t.triangles.length * (TriangleSize / 4));
91+const TriangleInfo = {
92+ corner_a: { type: Float32Array, byteOffset: 0, length: 3 },
93+ corner_b: { type: Float32Array, byteOffset: 16, length: 3 },
94+ corner_c: { type: Float32Array, byteOffset: 32, length: 3 },
95+ normal_a: { type: Float32Array, byteOffset: 48, length: 3 },
96+ normal_b: { type: Float32Array, byteOffset: 64, length: 3 },
97+ normal_c: { type: Float32Array, byteOffset: 80, length: 3 },
98+ material: { type: Float32Array, byteOffset: 92, length: 1 },
99+ uVA: { type: Float32Array, byteOffset: 96, length: 2 },
100+ uVB: { type: Float32Array, byteOffset: 104, length: 2 },
101+ uVC: { type: Float32Array, byteOffset: 112, length: 2 },
102+ tangentA: { type: Float32Array, byteOffset: 128, length: 4 },
103+ tangentB: { type: Float32Array, byteOffset: 144, length: 4 },
104+ tangentC: { type: Float32Array, byteOffset: 160, length: 4 },
105+};
106+
107+// init scene buffers
108+const triangleBuffer = device.createBuffer({
109+ label: "Triangle Storage",
110+ size: t.triangles.length * TriangleSize,
111+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
112+});
113+const materialBuffer = device.createBuffer({
114+ label: "Material storage",
115+ size: 8 * materialData.length,
116+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
117+});
118+const emissiveTrianglesBuffer = device.createBuffer({
119+ label: "Emissive triangles",
120+ size: t.triangles.length * TriangleSize,
121+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
122+});
123+const nodeBuffer = device.createBuffer({
124+ size: 32 * ab.nodesUsed,
125+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
126+});
127+const triangleIndexBuffer = device.createBuffer({
128+ size: 4 * t.triangles.length,
129+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
130+});
131+const accumulationBuffer = device.createBuffer({
132+ size: canvas.width * canvas.height * 16,
133+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
134+});
135+const uniformBuffer0 = device.createBuffer({
136+ label: "Camera Transform Buffer",
137+ size: 512,
138+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
139+});
140+const textureSizeBuffer = device.createBuffer({
141+ size: Math.max(textureSizes.byteLength, 2048),
142+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
143+});
144+
145+// populate buffers
146+const emissiveMaterialIndices: number[] = []
147+const emissiveTriangleIndices: number[] = []
148+const bvhPrimitiveTriangleIndices: Float32Array = new Float32Array(ab.triIdx.length);
149+
150+type MaterialPropertyName = keyof typeof MaterialInfo;
151+t.materials.forEach((mat, i) => {
152+ const materialOffset = i * (MaterialSize / 4); // Base offset for the current material
153+ const setData = (propertyName: MaterialPropertyName, value: number[]) => {
154+ const info = MaterialInfo[propertyName];
155+ materialData.set(value, materialOffset + info.byteOffset / 4);
156+ };
157+ const setFloat = (propertyName: MaterialPropertyName, value: number) => {
158+ const info = MaterialInfo[propertyName];
159+ materialData[materialOffset + info.byteOffset / 4] = value;
160+ };
161+ setData("albedo", mat.baseColorFactor); // Now sets 4 floats instead of 3
162+ setFloat("metallic", mat.metallicFactor);
163+ setFloat("alphaMode", mat.alphaMode);
164+ setFloat("alphaCutoff", mat.alphaCutoff);
165+ setFloat("doubleSided", mat.doubleSided);
166+ setData("emission", mat.emissiveFactor);
167+ setFloat("roughness", mat.roughnessFactor);
168+ setFloat("baseColorTexture", mat.baseColorTexture);
169+ setFloat("normalTexture", mat.normalTexture);
170+ setFloat("metallicRoughnessTexture", mat.metallicRoughnessTexture);
171+ setFloat("emissiveTexture", mat.emissiveTexture);
172+ if (mat.emissiveFactor[0] !== 0 || mat.emissiveFactor[1] !== 0 || mat.emissiveFactor[2] !== 0) {
173+ emissiveMaterialIndices.push(i);
174+ }
175+});
176+device.queue.writeBuffer(materialBuffer, 0, materialData);
177+
178+type TrianglePropertyName = keyof typeof TriangleInfo;
179+t.triangles.forEach((tri, i) => {
180+ const triOffset = i * (TriangleSize / 4);
181+ const setData = (propertyName: TrianglePropertyName, value: number[]) => {
182+ const info = TriangleInfo[propertyName];
183+ TriangleData.set(value, triOffset + info.byteOffset / 4);
184+ };
185+ const setFloat = (propertyName: TrianglePropertyName, value: number) => {
186+ const info = TriangleInfo[propertyName];
187+ TriangleData[triOffset + info.byteOffset / 4] = value;
188+ };
189+ setData("corner_a", tri.cornerA);
190+ setData("corner_b", tri.cornerB);
191+ setData("corner_c", tri.cornerC);
192+ setData("normal_a", tri.normalA);
193+ setData("normal_b", tri.normalB);
194+ setData("normal_c", tri.normalC);
195+ setFloat("material", tri.mat);
196+ setData("uVA", tri.uvA);
197+ setData("uVB", tri.uvB);
198+ setData("uVC", tri.uvC);
199+ setData("tangentA", tri.tangentA);
200+ setData("tangentB", tri.tangentB);
201+ setData("tangentC", tri.tangentC);
202+ if (emissiveMaterialIndices.includes(tri.mat)) {
203+ emissiveTriangleIndices.push(i); // Push the triangle's index
204+ }
205+});
206+device.queue.writeBuffer(triangleBuffer, 0, TriangleData);
207+device.queue.writeBuffer(emissiveTrianglesBuffer, 0, new Float32Array(emissiveTriangleIndices));
208+
209+const nodeData: Float32Array = new Float32Array(8 * ab.nodesUsed);
210+for (let i = 0; i < ab.nodesUsed; i++) {
211+ const minOffset = i * 3;
212+ const maxOffset = i * 3;
213+ nodeData[8 * i] = ab.nodesMin[minOffset + 0];
214+ nodeData[8 * i + 1] = ab.nodesMin[minOffset + 1];
215+ nodeData[8 * i + 2] = ab.nodesMin[minOffset + 2];
216+ nodeData[8 * i + 3] = ab.nodesLeft[i];
217+ nodeData[8 * i + 4] = ab.nodesMax[maxOffset + 0];
218+ nodeData[8 * i + 5] = ab.nodesMax[maxOffset + 1];
219+ nodeData[8 * i + 6] = ab.nodesMax[maxOffset + 2];
220+ nodeData[8 * i + 7] = ab.nodesInstanceCount[i];
221+}
222+device.queue.writeBuffer(nodeBuffer, 0, nodeData, 0, 8 * ab.nodesUsed);
223+
224+for (let i = 0; i < ab.triIdx.length; i++) {
225+ bvhPrimitiveTriangleIndices[i] = ab.triIdx[i];
226+}
227+device.queue.writeBuffer(triangleIndexBuffer, 0, bvhPrimitiveTriangleIndices, 0, ab.triIdx.length);
228+
229+// bluenoise texture 2. 3.24: Bluenoise
230+// bluenoise texture form https://momentsingraphics.de/BlueNoise.html
231+async function loadImageBitmap(url: string) {
232+ const res = await fetch(url);
233+ const blob = await res.blob();
234+ return await createImageBitmap(blob, { colorSpaceConversion: 'none' });
235+}
236+const bnnoiseSource = await loadImageBitmap("LDR_RGBA_0.png")
237+const blueNoiseTexture = device.createTexture({
238+ label: 'bluenoise-texture',
239+ format: 'rgba8unorm',
240+ size: [bnnoiseSource.width, bnnoiseSource.height],
241+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
242+});
243+device.queue.copyExternalImageToTexture(
244+ { source: bnnoiseSource },
245+ { texture: blueNoiseTexture },
246+ { width: bnnoiseSource.width, height: bnnoiseSource.height },
247+);
248+
249+
250+// construct the texture atlas
251+const emptySampler = device.createSampler({
252+ addressModeU: "clamp-to-edge",
253+ addressModeV: "clamp-to-edge",
254+ addressModeW: "clamp-to-edge",
255+ magFilter: "nearest",
256+ minFilter: "nearest",
257+ mipmapFilter: "nearest",
258+});
259+const emptyTexture = device.createTexture({
260+ size: [1, 1, 1],
261+ format: "rgba8unorm",
262+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
263+});
264+const emptyView = emptyTexture.createView({
265+ dimension: "2d-array",
266+});
267+let maxWidth = 1, maxHeight = 1;
268+let textureSampler = emptySampler;
269+let textureArray = emptyTexture;
270+let textureViewArray = emptyView;
271+if (hasTextures) {
272+ maxWidth = t.largestTextureDimensions.width
273+ maxHeight = t.largestTextureDimensions.height
274+ textureSampler = t.textures[0].sampler // lazy, store the samplers correctly
275+ textureArray = device.createTexture({
276+ size: [maxWidth, maxHeight, t.textures.length],
277+ format: "rgba8unorm",
278+ usage:
279+ GPUTextureUsage.TEXTURE_BINDING |
280+ GPUTextureUsage.COPY_DST |
281+ GPUTextureUsage.RENDER_ATTACHMENT,
282+ dimension: "2d",
283+ })
284+ textureViewArray = textureArray.createView({ dimension: "2d-array" });
285+}
286+
287+// rather wasteful (and sometimes incorrect, but whatever. Fine for now)
288+// 1. get each texture's dimension
289+// 2. pad it to the largest one
290+// 3. store the original h w in textureSizes[]
291+// 4. stack the padded texture
292+if (t.textures.length) {
293+ for (let i = 0; i < t.textures.length; i++) {
294+ const source = t.textures[i].source;
295+ // @ts-ignore / Poorly defined type for the original GLTFImagePostprocessed
296+ const bitmap = source.image as ImageBitmap;
297+ textureSizes[i * 4] = bitmap.width;
298+ textureSizes[i * 4 + 1] = bitmap.height;
299+ textureSizes[i * 4 + 2] = 0.0;
300+ textureSizes[i * 4 + 3] = 0.0;
301+
302+ device.queue.copyExternalImageToTexture(
303+ { source: bitmap },
304+ { texture: textureArray, origin: [0, 0, i] },
305+ [bitmap.width, bitmap.height, 1]
306+ );
307+ }
308+ device.queue.writeBuffer(textureSizeBuffer, 0, textureSizes);
309+}
310+
311+// bind groups and layouts
312+const geometryBindgroupLayout = device.createBindGroupLayout({
313+ label: 'geometry-bind-group-layout',
314+ entries: [
315+ {
316+ binding: 0,
317+ visibility: GPUShaderStage.COMPUTE,
318+ storageTexture: {
319+ access: "write-only",
320+ format: "rgba32float",
321+ viewDimension: "2d",
322+ },
323+ },
324+ {
325+ binding: 1,
326+ visibility: GPUShaderStage.COMPUTE,
327+ buffer: {
328+ type: "read-only-storage",
329+ hasDynamicOffset: false,
330+ },
331+ },
332+ {
333+ binding: 2,
334+ visibility: GPUShaderStage.COMPUTE,
335+ buffer: {
336+ type: "uniform",
337+ },
338+ },
339+
340+ {
341+ binding: 5,
342+ visibility: GPUShaderStage.COMPUTE,
343+ buffer: {
344+ type: "read-only-storage",
345+ hasDynamicOffset: false,
346+ },
347+ },
348+ {
349+ binding: 6,
350+ visibility: GPUShaderStage.COMPUTE,
351+ buffer: {
352+ type: "read-only-storage",
353+ hasDynamicOffset: false,
354+ },
355+ },
356+ {
357+ binding: 7,
358+ visibility: GPUShaderStage.COMPUTE,
359+ buffer: {
360+ type: "storage",
361+ },
362+ },
363+ ],
364+});
365+
366+const geometryBindgroup = device.createBindGroup({
367+ label: 'geometry-bind-group',
368+ layout: geometryBindgroupLayout,
369+ entries: [
370+ {
371+ binding: 0,
372+ resource: viewportTextureColorBuffer,
373+ },
374+ {
375+ binding: 1,
376+ resource: {
377+ buffer: triangleBuffer,
378+ },
379+ },
380+ {
381+ binding: 2,
382+ resource: {
383+ buffer: uniformBuffer0,
384+ },
385+ },
386+ {
387+ binding: 5,
388+ resource: { buffer: nodeBuffer },
389+ },
390+ {
391+ binding: 6,
392+ resource: { buffer: triangleIndexBuffer },
393+ },
394+ {
395+ binding: 7,
396+ resource: { buffer: accumulationBuffer },
397+ },
398+ ],
399+});
400+
401+const shadingBindGroupLayout = device.createBindGroupLayout({
402+ label: 'shading-bind-group-layout',
403+ entries: [
404+ {
405+ binding: 0,
406+ visibility: GPUShaderStage.COMPUTE,
407+ buffer: {
408+ type: "read-only-storage",
409+ hasDynamicOffset: false,
410+ },
411+ },
412+ {
413+ binding: 1,
414+ visibility: GPUShaderStage.COMPUTE,
415+ texture: {
416+ viewDimension: "2d-array",
417+ },
418+ },
419+ {
420+ binding: 2,
421+ visibility: GPUShaderStage.COMPUTE,
422+ sampler: {},
423+ },
424+ {
425+ binding: 4,
426+ visibility: GPUShaderStage.COMPUTE,
427+ buffer: {
428+ type: "uniform",
429+ hasDynamicOffset: false,
430+ },
431+ },
432+ {
433+ binding: 6,
434+ visibility: GPUShaderStage.COMPUTE,
435+ storageTexture: {
436+ access: "read-only",
437+ format: "rgba8unorm",
438+ viewDimension: "2d",
439+ },
440+ },
441+ {
442+ binding: 7,
443+ visibility: GPUShaderStage.COMPUTE,
444+ buffer: {
445+ type: "read-only-storage",
446+ hasDynamicOffset: false,
447+ },
448+ },
449+ ],
450+});
451+
452+const shadingBindGroup = device.createBindGroup({
453+ label: 'shading-bind-group',
454+ layout: shadingBindGroupLayout,
455+ entries: [
456+ {
457+ binding: 0,
458+ resource: {
459+ buffer: materialBuffer,
460+ },
461+ },
462+ { binding: 1, resource: textureViewArray },
463+ { binding: 2, resource: textureSampler },
464+ {
465+ binding: 4,
466+ resource: {
467+ buffer: textureSizeBuffer,
468+ },
469+ },
470+ {
471+ binding: 6,
472+ resource: blueNoiseTexture.createView(),
473+ },
474+ {
475+ binding: 7,
476+ resource: {
477+ buffer: emissiveTrianglesBuffer,
478+ },
479+ },
480+ ],
481+});
482+
483+const viewportBindgroupLayout = device.createBindGroupLayout({
484+ entries: [
485+ {
486+ binding: 0,
487+ visibility: GPUShaderStage.FRAGMENT,
488+ texture: {
489+ sampleType: 'unfilterable-float',
490+ viewDimension: '2d',
491+ multisampled: false,
492+ },
493+
494+ },
495+ ],
496+});
497+
498+const viewportBindgroup = device.createBindGroup({
499+ layout: viewportBindgroupLayout,
500+ entries: [
501+ {
502+ binding: 0,
503+ resource: viewportTextureColorBuffer,
504+ },
505+ ],
506+});
507+
508+// pipelines
509+const kernelPipelineLayout = device.createPipelineLayout({
510+ bindGroupLayouts: [geometryBindgroupLayout, shadingBindGroupLayout],
511+});
512+
513+const kernelPipeline = device.createComputePipeline({
514+ layout: kernelPipelineLayout,
515+ compute: {
516+ module: device.createShaderModule({
517+ code: kernel,
518+ }),
519+ entryPoint: "main",
520+ },
521+});
522+
523+const viewportPipelineLayout = device.createPipelineLayout({
524+ bindGroupLayouts: [viewportBindgroupLayout],
525+});
526+
527+const viewportPipeline = device.createRenderPipeline({
528+ layout: viewportPipelineLayout,
529+ vertex: {
530+ module: device.createShaderModule({
531+ code: viewport,
532+ }),
533+ entryPoint: "vert_main",
534+ },
535+ fragment: {
536+ module: device.createShaderModule({
537+ code: viewport,
538+ }),
539+ entryPoint: "frag_main",
540+ targets: [
541+ {
542+ format: format,
543+ },
544+ ],
545+ },
546+ primitive: {
547+ topology: "triangle-list",
548+ },
549+});
550+
551+
552+var frametime = 0;
553+window.framecount = 0;
554+const UNIFORMS = {
555+ sample_count: 1.0,
556+ bounce_count: 3.0,
557+ aperture: 0.1,
558+ focal_length: 4.0,
559+ frameTimeMs: 0,
560+ fps: frametime / 1000,
561+ sun_angle: { x: 0.3, y: -0.7, z: 0.3 },
562+ sun_color: { r: 1.0, g: 0.96, b: 0.85 },
563+ scale: 22000.0, // sun_color rgb -> lux scale
564+ albedo_factor: z.materials[0].baseColorFactor,
565+ metallicFactor: z.materials[0].metallicFactor,
566+ roughnessFactor: z.materials[0].roughnessFactor,
567+ thin_lens: false,
568+};
569+// initialize values based on UNIFORMS, updates are createPane()
570+device.queue.writeBuffer(uniformBuffer0, 208, Vec3.fromValues(UNIFORMS.sun_angle.x, UNIFORMS.sun_angle.y, UNIFORMS.sun_angle.z));
571+device.queue.writeBuffer(uniformBuffer0, 220, new Float32Array([0.53 * (Math.PI / 180.0)])); // ~0.5332 degrees / 32.15 arcminutes
572+device.queue.writeBuffer(uniformBuffer0, 224, Vec3.fromValues(UNIFORMS.sun_color.r * UNIFORMS.scale, UNIFORMS.sun_color.g * UNIFORMS.scale, UNIFORMS.sun_color.b * UNIFORMS.scale));
573+device.queue.writeBuffer(uniformBuffer0, 236, new Float32Array([UNIFORMS.sample_count, UNIFORMS.bounce_count, UNIFORMS.aperture, UNIFORMS.focal_length]));
574+device.queue.writeBuffer(uniformBuffer0, 252, new Float32Array([emissiveTriangleIndices.length - 1, UNIFORMS.thin_lens ? 1 : 0]));
575+
576+let camera = createCamera(canvas);
577+InitPane(device, UNIFORMS, uniformBuffer0)
578+setupCameraInput(canvas)
579+
580+device.queue.writeBuffer(uniformBuffer0, 0, camera.position);
581+device.queue.writeBuffer(uniformBuffer0, 16, camera.view);
582+device.queue.writeBuffer(uniformBuffer0, 80, camera.inverseView);
583+device.queue.writeBuffer(uniformBuffer0, 144, camera.projection);
584+
585+let cpuStart = 0;
586+let cpuEnd = 0;
587+let frametimeMs;
588+const framedata = new Float32Array(1);
589+
590+const workgroupSize = 16;
591+const dispatchX = Math.ceil(width / workgroupSize);
592+const dispatchY = Math.ceil(height / workgroupSize);
593+
594+async function renderFrame() {
595+ cpuStart = performance.now();
596+ window.framecount++;
597+ framedata[0] = window.framecount;
598+ updateMovementInput();
599+ updateCamera(camera);
600+ if (camera.dirty) {
601+ window.framecount = 0; // reset accumulation
602+ device.queue.writeBuffer(uniformBuffer0, 0, camera.position);
603+ device.queue.writeBuffer(uniformBuffer0, 16, camera.view);
604+ device.queue.writeBuffer(uniformBuffer0, 80, camera.inverseView);
605+ device.queue.writeBuffer(uniformBuffer0, 144, camera.projection);
606+ }
607+ device.queue.writeBuffer(uniformBuffer0, 12, framedata);
608+ const commandEncoder = device.createCommandEncoder();
609+ // compute pass
610+ var computePass = commandEncoder.beginComputePass();
611+ computePass.setPipeline(kernelPipeline);
612+ computePass.setBindGroup(0, geometryBindgroup);
613+ computePass.setBindGroup(1, shadingBindGroup);
614+ computePass.dispatchWorkgroups(dispatchX, dispatchY);
615+ computePass.end();
616+ // blitt pass
617+ const renderPass = commandEncoder.beginRenderPass({
618+ label: "main",
619+ colorAttachments: [
620+ {
621+ view: context.getCurrentTexture().createView(),
622+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, // rgba
623+ loadOp: "clear",
624+ storeOp: "store",
625+ },
626+ ],
627+ });
628+ renderPass.setPipeline(viewportPipeline);
629+ renderPass.setBindGroup(0, viewportBindgroup);
630+ renderPass.draw(6, 1, 0, 0);
631+ renderPass.end();
632+ device.queue.submit([commandEncoder.finish()]);
633+ device.queue.onSubmittedWorkDone().then(
634+ () => {
635+ cpuEnd = performance.now();
636+ frametimeMs = cpuEnd - cpuStart;
637+ frametime = parseInt(frametimeMs.toFixed(2));
638+ UNIFORMS.frameTimeMs = frametime;
639+ }
640+ );
641+ requestAnimationFrame(renderFrame);
642+}
643+
644+requestAnimationFrame(renderFrame);
A · src/pane.ts
+142, -0 1@@ -0,0 +1,142 @@
2+import { Vec3 } from 'gl-matrix';
3+import { Pane } from 'tweakpane';
4+
5+export const InitPane = (device: GPUDevice, UNIFORMS: any, uniformBuffer0: GPUBuffer) => {
6+ const container = document.getElementById("pane-container") as HTMLElement;
7+ const header = document.getElementById("pane-container-header") as HTMLElement;
8+
9+ const pane = new Pane({
10+ container: container,
11+ });
12+ let offsetX = 0;
13+ let offsetY = 0;
14+ let isDragging = false;
15+ header.addEventListener('mousedown', (e) => {
16+ isDragging = true;
17+ offsetX = e.clientX - container.offsetLeft;
18+ offsetY = e.clientY - container.offsetTop;
19+ container.style.cursor = 'move';
20+ });
21+ document.addEventListener('mousemove', (e) => {
22+ if (isDragging) {
23+ let x = e.clientX - offsetX;
24+ let y = e.clientY - offsetY;
25+
26+ const viewportWidth = window.innerWidth;
27+ const viewportHeight = window.innerHeight;
28+
29+ const containerWidth = container.offsetWidth;
30+ const containerHeight = container.offsetHeight;
31+
32+ const minX = 0;
33+ const maxX = viewportWidth - containerWidth;
34+ const minY = 0;
35+ const maxY = viewportHeight - containerHeight;
36+
37+ x = Math.max(minX, Math.min(x, maxX));
38+ y = Math.max(minY, Math.min(y, maxY));
39+
40+ container.style.left = `${x}px`;
41+ container.style.top = `${y}px`;
42+ }
43+ });
44+ document.addEventListener('mouseup', () => {
45+ isDragging = false;
46+ container.style.cursor = 'default';
47+ });
48+
49+ //packed vars: samp_count, bounce, aperture, focal_length]
50+ let packed0 = new Float32Array([UNIFORMS.sample_count, UNIFORMS.bounce_count, UNIFORMS.aperture, UNIFORMS.focal_length])
51+ pane.addBinding(UNIFORMS, 'frameTimeMs', {
52+ readonly: true,
53+ label: "Frame Time ",
54+ view: 'number',
55+ min: 0,
56+ max: 120.00,
57+ });
58+ pane.addBinding(UNIFORMS, 'frameTimeMs', {
59+ readonly: true,
60+ label: "Frame Time Graph",
61+ view: 'graph',
62+ min: 0,
63+ max: 120.00,
64+ });
65+ const cameraFolder = pane.addFolder({ title: 'Camera' })
66+ cameraFolder.addBinding(UNIFORMS, 'thin_lens', {
67+ label: "Enable thin lens approx",
68+ }).on('change', (e) => {
69+ device.queue.writeBuffer(uniformBuffer0, 256, new Float32Array([e.value]));
70+ window.framecount = 0;
71+ });
72+ cameraFolder.addBinding(UNIFORMS, 'sample_count', {
73+ view: 'slider',
74+ type: 'number',
75+ label: 'Sample Count',
76+ min: 1,
77+ max: 5,
78+ value: 1,
79+ step: 1,
80+ }).on('change', (e) => {
81+ packed0[0] = e.value;
82+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
83+ window.framecount = 0;
84+ });
85+ cameraFolder.addBinding(UNIFORMS, 'bounce_count', {
86+ view: 'slider',
87+ type: 'number',
88+ label: 'Max Bounces',
89+ min: 1,
90+ max: 10,
91+ value: 1,
92+ step: 1,
93+ }).on('change', (e) => {
94+ packed0[1] = e.value;
95+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
96+ window.framecount = 0;
97+ });
98+ cameraFolder.addBinding(UNIFORMS, 'aperture', {
99+ view: 'slider',
100+ type: 'number',
101+ label: 'Aperture',
102+ min: 0.1,
103+ max: 1,
104+ value: 0.1,
105+ }).on('change', (e) => {
106+ packed0[2] = e.value;
107+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
108+ window.framecount = 0;
109+ });
110+ cameraFolder.addBinding(UNIFORMS, 'focal_length', {
111+ view: 'slider',
112+ type: 'number',
113+ label: 'Focal Length',
114+ min: 1,
115+ max: 200,
116+ value: 1,
117+ }).on('change', (e) => {
118+ packed0[3] = e.value;
119+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
120+ window.framecount = 0;
121+ });
122+ const dirFolder = pane.addFolder({ title: 'Environment' });
123+ dirFolder.addBinding(UNIFORMS, 'sun_angle', {
124+ label: 'Sun Angle',
125+ x: { min: -1, max: 1, step: 0.1 },
126+ y: { min: -1, max: 1, step: 0.1 },
127+ z: { min: -1, max: 1, step: 0.1 },
128+ }).on('change', () => {
129+ device.queue.writeBuffer(uniformBuffer0, 208, Vec3.fromValues(UNIFORMS.sun_angle.x, UNIFORMS.sun_angle.y, UNIFORMS.sun_angle.z));
130+ window.framecount = 0;
131+ });
132+ dirFolder.addBinding(UNIFORMS, 'sun_color', {
133+ color: { type: 'float' },
134+ picker: 'inline',
135+ label: 'Sun Color',
136+ x: { min: -1, max: 1, step: 0.1 },
137+ y: { min: -1, max: 1, step: 0.1 },
138+ z: { min: -1, max: 1, step: 0.1 },
139+ }).on('change', (e) => {
140+ device.queue.writeBuffer(uniformBuffer0, 224, Vec3.fromValues(e.value.r * UNIFORMS.scale, e.value.g * UNIFORMS.scale, e.value.b * UNIFORMS.scale));
141+ window.framecount = 0;
142+ });
143+}
A · src/shaders/any_hit.wgsl
+138, -0 1@@ -0,0 +1,138 @@
2+fn is_occluded(pos: vec3f, normal: vec3f, light_dir: vec3f, light_distance: f32) -> bool {
3+ var shadow_ray: Ray;
4+ shadow_ray.origin = offset_ray(pos, normal);
5+ shadow_ray.direction = light_dir;
6+ var shadow_hit = trace_any(shadow_ray, light_distance);
7+ return shadow_hit;
8+}
9+
10+fn trace_any(ray: Ray, t_max: f32) -> bool {
11+ var node_idx_stack: array<u32, 64>;
12+ var stack_ptr: i32 = 0;
13+ var current_node_idx: u32 = 0;
14+
15+ while (true) {
16+ let node = node_tree.nodes[current_node_idx];
17+
18+ let primitive_count = u32(node.primitive_count);
19+ let child_or_prim_idx = u32(node.left_child);
20+ if (primitive_count == 0u) {
21+ // internal node
22+ let left_child_idx = child_or_prim_idx;
23+ let right_child_idx = child_or_prim_idx + 1u;
24+
25+ // use t_max for pruning
26+ let hit1 = any_hit_aabb(ray, node_tree.nodes[left_child_idx].min_corner, node_tree.nodes[left_child_idx].max_corner, t_max);
27+ let hit2 = any_hit_aabb(ray, node_tree.nodes[right_child_idx].min_corner, node_tree.nodes[right_child_idx].max_corner, t_max);
28+
29+ var near_child_idx = left_child_idx;
30+ var far_child_idx = right_child_idx;
31+ var dist1_hit = hit1;
32+ var dist2_hit = hit2;
33+
34+ if (!hit1 && hit2) {
35+ near_child_idx = right_child_idx;
36+ far_child_idx = left_child_idx;
37+ dist1_hit = hit2;
38+ dist2_hit = hit1;
39+ }
40+
41+ if (dist1_hit) {
42+ current_node_idx = near_child_idx;
43+ if (dist2_hit) {
44+ if (stack_ptr >= 64) {
45+ break;
46+ }
47+ // overflow
48+ node_idx_stack[stack_ptr] = far_child_idx;
49+ stack_ptr += 1;
50+ }
51+ continue;
52+ // descend into near child
53+ }
54+ // neither child is relevant, fall through to pop
55+
56+ }
57+ else {
58+ // leaf node
59+ for (var i = 0u; i < primitive_count; i += 1u) {
60+ let prim_index = tri_lut.primitive_indices[child_or_prim_idx + i];
61+ let triangle = objects.triangles[i32(prim_index)];
62+
63+ // any_hit_triangle returns true if hit within range
64+ if (any_hit_triangle(ray, triangle, 0.001, t_max)) {
65+ return true;
66+ // found an occlusion, exit immediately
67+ }
68+ }
69+ // finished leaf without finding occlusion, fall through to pop
70+ }
71+
72+ // pop from stack or break if empty
73+ if (stack_ptr == 0) {
74+ break;
75+ // traversal finished without finding occlusion
76+ }
77+ else {
78+ stack_ptr -= 1;
79+ current_node_idx = node_idx_stack[stack_ptr];
80+ }
81+ }
82+ // kill sunlight 0.0
83+ let floor_z = 0.0;
84+ let denom = ray.direction.z;
85+ if (abs(denom) > 1e-6) {
86+ let t = (floor_z - ray.origin.z) / denom;
87+ if (t > 0.001 && t < t_max) {
88+ return true;
89+ // hit floor within range
90+ }
91+ }
92+ return false;
93+ // no occlusion found
94+}
95+
96+fn any_hit_aabb(ray: Ray, aabb_min: vec3f, aabb_max: vec3f, t_max: f32) -> bool {
97+ var inverse_dir: vec3<f32> = vec3(1.0) / ray.direction;
98+ var tmin = (aabb_min - ray.origin) * inverse_dir;
99+ var tmax = (aabb_max - ray.origin) * inverse_dir;
100+ var t1 = min(tmin, tmax);
101+ var t2 = max(tmin, tmax);
102+ var t_near = max(max(t1.x, t1.y), t1.z);
103+ var t_far = min(min(t2.x, t2.y), t2.z);
104+ return t_near <= t_far && t_far >= 0.001 && t_near <= t_max;
105+}
106+
107+// lazy, just clean this up to only use hit_triangle and move all shading data post hit out
108+fn any_hit_triangle(ray: Ray, tri: Triangle, dist_min: f32, dist_max: f32) -> bool {
109+ let edge1 = tri.corner_b - tri.corner_a;
110+ let edge2 = tri.corner_c - tri.corner_a;
111+
112+ let pvec = cross(ray.direction, edge2);
113+ let determinant = dot(edge1, pvec);
114+
115+ // reject nearly parallel rays.
116+ if abs(determinant) < EPSILON {
117+ return false;
118+ }
119+
120+ let inv_det = 1.0 / determinant;
121+ let tvec = ray.origin - tri.corner_a;
122+
123+ // compute barycentric coordinate u.
124+ let u = dot(tvec, pvec) * inv_det;
125+ if (u < 0.0 || u > 1.0) {
126+ return false;
127+ }
128+
129+ // compute barycentric coordinate v.
130+ let qvec = cross(tvec, edge1);
131+ let v = dot(ray.direction, qvec) * inv_det;
132+ if (v < 0.0 || (u + v) > 1.0) {
133+ return false;
134+ }
135+
136+ // calculate ray parameter (distance).
137+ let dist = dot(edge2, qvec) * inv_det;
138+ return dist > dist_min && dist < dist_max;
139+}
A · src/shaders/brdf.wgsl
+154, -0 1@@ -0,0 +1,154 @@
2+// computes the geometry (shadowing/masking) term using smith's method.
3+fn smith_geometry(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, roughness: f32) -> f32 {
4+ let alpha = roughness * roughness;
5+ let n_dot_v = max(dot(normal, view_dir), 0.0);
6+ let n_dot_l = max(dot(normal, light_dir), 0.0);
7+ let k = (alpha + 1.0) * (alpha + 1.0) / 8.0;
8+ let geom_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
9+ let geom_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
10+ return geom_v * geom_l;
11+}
12+
13+// computes the ggx normal distribution function (ndf) d.
14+fn ggx_distribution(normal: vec3<f32>, half_vec: vec3<f32>, roughness: f32) -> f32 {
15+ let rgh = max(0.03, roughness);
16+ let alpha = rgh * rgh;
17+ let alpha2 = alpha * alpha;
18+ let n_dot_h = max(dot(normal, half_vec), 0.001);
19+ let denom = (n_dot_h * n_dot_h) * (alpha2 - 1.0) + 1.0;
20+ return alpha2 / (PI * denom * denom);
21+}
22+
23+// samples a half–vector (h) from the ggx distribution in tangent space.
24+fn ggx_sample_vndf(view_dir: vec3<f32>, normal: vec3<f32>, roughness: f32, noise: vec2<f32>) -> vec3<f32> {
25+ // build local frame (t, b, n)
26+ let up: vec3<f32> = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.z) < 0.999);
27+ let tangent = normalize(cross(up, normal));
28+ let bitangent = cross(normal, tangent);
29+
30+ // transform view direction to local space
31+ let v = normalize(vec3<f32>(dot(view_dir, tangent), dot(view_dir, bitangent), dot(view_dir, normal)));
32+
33+ // stretch view vector
34+ let a = roughness;
35+ let vh = normalize(vec3<f32>(a * v.x, a * v.y, v.z));
36+
37+ // orthonormal basis
38+ let lensq = vh.x * vh.x + vh.y * vh.y;
39+ let t1 = select(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(- vh.y, vh.x, 0.0) / sqrt(lensq), lensq > 1e-6);
40+ let t2 = cross(vh, t1);
41+
42+ // sample point with polar coordinates
43+ let r = sqrt(noise.x);
44+ let phi = 2.0 * PI * noise.y;
45+ let r1 = r * cos(phi);
46+ let r2 = r * sin(phi);
47+ let s = 0.5 * (1.0 + vh.z);
48+ let t2_ = mix(sqrt(1.0 - r1 * r1), r2, s);
49+
50+ // sampled halfway vector in local space
51+ let nh = r1 * t1 + t2_ * t2 + sqrt(max(0.0, 1.0 - r1 * r1 - t2_ * t2_)) * vh;
52+
53+ // unstretch
54+ let h = normalize(vec3<f32>(a * nh.x, a * nh.y, max(0.0, nh.z)));
55+
56+ // transform back to world space
57+ return normalize(h.x * tangent + h.y * bitangent + h.z * normal);
58+}
59+
60+fn ggx_specular_sample(view_dir: vec3<f32>, normal: vec3<f32>, seed: vec2<f32>, roughness: f32) -> vec3<f32> {
61+ let h = ggx_sample_vndf(view_dir, normal, roughness, seed);
62+ let r = reflect(- view_dir, h);
63+ return select(normalize(r), vec3<f32>(0.0), dot(r, normal) <= EPSILON);
64+}
65+
66+// cosine-weighted hemisphere sample in local space, then converted to world space.
67+fn cosine_hemisphere_sample(normal: vec3<f32>, noise: vec2<f32>) -> vec3<f32> {
68+ // var current_seed = seed;
69+ let r1 = noise.x;
70+ let r2 = noise.y;
71+
72+ // let r1 = uniform_float(state);
73+ // let r2 = uniform_float(state);
74+
75+ let phi = 2.0 * PI * r2;
76+ let cos_theta = sqrt(1.0 - r1);
77+ let sin_theta = sqrt(r1);
78+
79+ // sample in local coordinates (with z as the normal)
80+ let local_sample = vec3<f32>(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
81+
82+ // build tangent space and transform sample to world space.
83+ // let up = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.z) > 0.999);
84+
85+ let tangent = normalize(cross(select(vec3<f32>(0.0, 1.0, 0.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.y) > 0.99), normal));
86+ let bitangent = cross(normal, tangent);
87+
88+ // let tangent = normalize(cross(up, normal));
89+ // let bitangent = cross(normal, tangent);
90+ let world_dir = normalize(local_sample.x * tangent + local_sample.y * bitangent + local_sample.z * normal);
91+ return world_dir;
92+}
93+
94+fn eval_f0(metallic: f32, albedo: vec3<f32>) -> vec3<f32> {
95+ let dielectric_f0 = vec3<f32>(0.04);
96+ return mix(dielectric_f0, albedo, metallic);
97+}
98+
99+fn fresnel_schlick_roughness(cos_theta: f32, f0: vec3<f32>, roughness: f32) -> vec3<f32> {
100+ let one_minus_cos = 1.0 - cos_theta;
101+ let factor = pow(one_minus_cos, 5.0);
102+ let fresnel = f0 + (max(vec3f(1.0 - roughness), f0) - f0) * factor;
103+ return fresnel;
104+}
105+
106+fn disney_diffuse(albedo: vec3<f32>, roughness: f32, n_dot_l: f32, n_dot_v: f32, l_dot_h: f32) -> vec3<f32> {
107+ let fd90 = 0.5 + 2.0 * l_dot_h * l_dot_h * roughness;
108+ let light_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_l, 5.0);
109+ let view_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_v, 5.0);
110+ return albedo * light_scatter * view_scatter * (1.0 / PI);
111+}
112+
113+fn eval_brdf(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, material: Material) -> vec3<f32> {
114+ let n = normal;
115+ let v = view_dir;
116+ let l = light_dir;
117+ let h = normalize(v + l);
118+
119+ let ndot_l = max(dot(n, l), EPSILON);
120+ let ndot_v = max(dot(n, v), EPSILON);
121+ let ndot_h = max(dot(n, h), EPSILON);
122+ let vdot_h = max(dot(v, h), EPSILON);
123+ let ldot_h = max(dot(l, h), EPSILON);
124+
125+ let f0 = mix(vec3f(0.04), material.albedo.rgb, material.metallic);
126+ let f = fresnel_schlick_roughness(vdot_h, f0, material.roughness);
127+
128+ // frostbite specular
129+ let d = ggx_distribution(n, h, material.roughness);
130+ let g = smith_geometry(n, v, l, material.roughness);
131+ let spec = (d * g * f) / (4.0 * ndot_v * ndot_l + 0.001);
132+
133+ let diffuse = (1.0 - material.metallic) * disney_diffuse(material.albedo.rgb, material.roughness, ndot_l, ndot_v, ldot_h);
134+ return diffuse + spec;
135+}
136+
137+fn ggx_pdf(view_dir: vec3<f32>, normal: vec3<f32>, h: vec3<f32>, roughness: f32) -> f32 {
138+ let d = ggx_distribution(normal, h, roughness);
139+ let n_dot_h = max(dot(normal, h), EPSILON);
140+ let v_dot_h = max(dot(view_dir, h), EPSILON);
141+ // change–of–variables: pdf(r) = d(h) * n_dot_h / (4 * v_dot_h)
142+ return (d * n_dot_h) / (4.0 * v_dot_h);
143+}
144+
145+fn cosine_pdf(normal: vec3<f32>, dir: vec3<f32>) -> f32 {
146+ // cosine weighted density: d = cos(theta) / PI.
147+ let cos_theta = max(dot(normal, dir), EPSILON);
148+ if cos_theta <= 0.0 {
149+ return 0.0;
150+ // no contribution when direction is opposite or perpendicular to the normal.
151+ }
152+ return cos_theta / PI;
153+}
154+
155+
A · src/shaders/main.wgsl
+593, -0 1@@ -0,0 +1,593 @@
2+#include random.wgsl
3+#include brdf.wgsl
4+#include sky.wgsl
5+#include any_hit.wgsl
6+#include utils.wgsl
7+
8+@group(0) @binding(0) var output_buffer : texture_storage_2d<rgba32float, write>;
9+@group(0) @binding(1) var<storage, read> objects: Objects;
10+@group(0) @binding(2) var<uniform> uniforms: UniformLayout;
11+@group(0) @binding(5) var<storage, read> node_tree: BVH;
12+@group(0) @binding(6) var<storage, read> tri_lut: ObjectIndices;
13+@group(0) @binding(7) var<storage, read_write> input_buffer:array<vec3f>;
14+
15+@group(1) @binding(0) var<storage, read> materials:array<Material>;
16+@group(1) @binding(1) var textures: texture_2d_array<f32>;
17+@group(1) @binding(2) var t_sampler: sampler;
18+@group(1) @binding(4) var<uniform> textureSizes: array<vec4<f32>, 128>;
19+@group(1) @binding(6) var blueNoiseTexture : texture_storage_2d<rgba8unorm, read>;
20+@group(1) @binding(7) var<storage, read> emissiveTriangleIndices : array<f32>;
21+
22+// @group(0) @binding(3) var skybox: texture_2d<f32>;
23+// @group(0) @binding(4) var skybox_sampler: sampler;
24+// @group(1) @binding(5) var skyboxCDF: texture_storage_2d<rg32float, read>;
25+// @group(1) @binding(3) var<storage, read> areaLights:array<AreaLight>;
26+
27+struct Triangle {
28+ corner_a: vec3<f32>,
29+ corner_b: vec3<f32>,
30+ corner_c: vec3<f32>,
31+ normal_a: vec3<f32>,
32+ normal_b: vec3<f32>,
33+ normal_c: vec3<f32>,
34+ material_idx: f32,
35+ uv_a: vec2<f32>,
36+ uv_b: vec2<f32>,
37+ uv_c: vec2<f32>,
38+ tangent_a: vec4f,
39+ tangent_b: vec4f,
40+ tangent_c: vec4f,
41+}
42+
43+// struct AreaLight {
44+// center: vec3<f32>,
45+// u: vec3<f32>,
46+// v: vec3<f32>,
47+// normal: vec3<f32>,
48+// emission: vec3<f32>,
49+// };
50+
51+struct Ray {
52+ direction: vec3<f32>,
53+ origin: vec3<f32>,
54+}
55+
56+struct HitInfo {
57+ dist: f32,
58+ hit: bool,
59+ position: vec3<f32>,
60+ normal: vec3<f32>,
61+ material_idx: i32,
62+ geo_normal: vec3f,
63+ tri: Triangle,
64+ uv: vec2f,
65+ tangent: vec3<f32>,
66+ bitangent: vec3<f32>,
67+}
68+
69+struct UniformLayout {
70+ position: vec3<f32>,
71+ frame_idx: f32,
72+ view: mat4x4<f32>,
73+ inverse_view: mat4x4<f32>,
74+ projection: mat4x4<f32>,
75+ sun_direction: vec3<f32>,
76+ sun_angular_size: f32,
77+ sun_radiance: vec3<f32>,
78+ sample_count: f32,
79+ max_depth: f32,
80+ aperture: f32,
81+ focus_distance: f32,
82+ emissive_triangle_count: f32,
83+ thin_lens: f32,
84+}
85+
86+struct Node {
87+ min_corner: vec3<f32>,
88+ left_child: f32,
89+ max_corner: vec3<f32>,
90+ primitive_count: f32,
91+}
92+
93+struct BVH {
94+ nodes: array<Node>,
95+}
96+
97+struct ObjectIndices {
98+ primitive_indices: array<f32>,
99+}
100+
101+struct Objects {
102+ triangles: array<Triangle>,
103+}
104+
105+struct Material {
106+ albedo: vec4<f32>,
107+ metallic: f32,
108+ alpha_mode: f32,
109+ alpha_cutoff: f32,
110+ double_sided: f32,
111+ emission: vec3<f32>,
112+ roughness: f32,
113+ base_color_texture: f32,
114+ normal_texture: f32,
115+ metallic_roughness_texture: f32,
116+ emissive_texture: f32,
117+}
118+
119+
120+
121+const EPSILON :f32 = 0.00001f;
122+const PI :f32 = 3.1415927f;
123+// ray tracing gems part 1 chapter 6
124+const FLOAT_SCALE = 1.0 / 65536.0;
125+const INT_SCALE = 256.0;
126+const ORIGIN = 1.0 / 32.0;
127+
128+// Slightly offsets a ray to prevent self intersection artifacts
129+// Ray tracing gems part 1 chapter 6
130+fn offset_ray(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
131+ let of_i = vec3<i32>(
132+ i32(INT_SCALE * n.x),
133+ i32(INT_SCALE * n.y),
134+ i32(INT_SCALE * n.z)
135+ );
136+
137+ let p_i = vec3<f32>(
138+ int_to_float(float_to_int(p.x) + select(of_i.x, -of_i.x, p.x < 0.0)),
139+ int_to_float(float_to_int(p.y) + select(of_i.y, -of_i.y, p.y < 0.0)),
140+ int_to_float(float_to_int(p.z) + select(of_i.z, -of_i.z, p.z < 0.0))
141+ );
142+
143+ return vec3<f32>(
144+ select(p.x + FLOAT_SCALE * n.x, p_i.x, abs(p.x) >= ORIGIN),
145+ select(p.y + FLOAT_SCALE * n.y, p_i.y, abs(p.y) >= ORIGIN),
146+ select(p.z + FLOAT_SCALE * n.z, p_i.z, abs(p.z) >= ORIGIN)
147+ );
148+}
149+
150+fn sample_material_texture(uv: vec2<f32>, texture_index: u32) -> vec4<f32> {
151+ let tex_size = textureSizes[texture_index].xy;
152+ let max_tex_size = vec2<f32>(textureDimensions(textures).xy);
153+ // let scaled_uv = uv * tex_size / max_tex_size;
154+ // let clamped_uv = clamp(scaled_uv, vec2<f32>(0.0), vec2<f32>(1.0));
155+ // compute the valid uv bounds inside the texture array
156+ let tex_uv_min = vec2<f32>(0.0); // always starts at (0,0)
157+ let tex_uv_max = tex_size / max_tex_size; // upper-right boundary in the atlas
158+ // remap u_vs to this valid range
159+ let mapped_uv = mix(tex_uv_min, tex_uv_max, uv);
160+ return textureSampleLevel(textures, t_sampler, mapped_uv, texture_index, 1.0).rgba;
161+}
162+
163+
164+fn parse_textures(curr_material: Material, result: HitInfo) -> Material {
165+ var material = curr_material;
166+ if material.base_color_texture > -1.0 {
167+ material.albedo *= sample_material_texture(result.uv, u32(curr_material.base_color_texture)).rgba;
168+ }
169+ if material.metallic_roughness_texture > -1.0 {
170+ let metallic_roughness_texture = sample_material_texture(result.uv, u32(curr_material.metallic_roughness_texture));
171+ material.roughness *= metallic_roughness_texture.g;
172+ material.metallic *= metallic_roughness_texture.b;
173+ }
174+ if material.emissive_texture > -1.0 {
175+ material.emission = sample_material_texture(result.uv, u32(curr_material.emissive_texture)).rgb;
176+ }
177+ return material;
178+}
179+
180+
181+fn point_in_unit_disk(u: vec2f) -> vec2f {
182+ let r = sqrt(u.x);
183+ let theta = 2f * PI * u.y;
184+ return vec2f(r * cos(theta), r * sin(theta));
185+}
186+
187+fn generate_pinhole_camera_ray(ndc: vec2<f32>, noise: vec2f) -> Ray {
188+ var ray : Ray;
189+ let aspect = uniforms.projection[1][1] / uniforms.projection[0][0]; // same as 1/tan_half_fov_y divided by 1/tan_half_fov_x
190+ let tan_half_fov_y = 1.0 / uniforms.projection[1][1];
191+
192+ let x = ndc.x * aspect * tan_half_fov_y;
193+ let y = ndc.y * tan_half_fov_y;
194+
195+ // camera basis vectors from the view matrix
196+ let right = uniforms.inverse_view[0].xyz;
197+ let up = uniforms.inverse_view[1].xyz;
198+ let forward = -uniforms.inverse_view[2].xyz;
199+ let origin = uniforms.position;
200+
201+ let pinhole_dir = normalize(x * right + y * up + forward);
202+
203+ let focus_dist = uniforms.focus_distance;
204+ let aperture = uniforms.aperture;
205+ let focus_point = origin + pinhole_dir * focus_dist;
206+
207+ // sample lens (in local right-up plane)
208+ let lens_sample = point_in_unit_disk(noise) * aperture;
209+ let lens_offset = lens_sample.x * right + lens_sample.y * up;
210+
211+ if (uniforms.thin_lens == 0.0){
212+ ray.origin = origin;
213+ ray.direction = pinhole_dir;
214+ } else {
215+ ray.origin = origin + lens_offset;
216+ ray.direction = normalize(focus_point - ray.origin);
217+ }
218+ return ray;
219+}
220+
221+
222+@compute @workgroup_size(16, 16)
223+fn main(
224+ @builtin(global_invocation_id) GlobalInvocationID: vec3<u32>,
225+ @builtin(local_invocation_id) LocalInvocationID: vec3<u32>,
226+ @builtin(workgroup_id) GroupIndex: vec3<u32>) {
227+ // https://www.w3.org/TR/webgpu/#coordinate-systems
228+ let output_dimension: vec2<i32> = vec2<i32>(textureDimensions(output_buffer));
229+ let pixel_position: vec2<i32> = vec2<i32>(i32(GlobalInvocationID.x), i32(GlobalInvocationID.y));
230+ let pixel_idx: i32 = pixel_position.y * output_dimension.x + pixel_position.x;
231+
232+ let pixel_center: vec2<f32> = vec2<f32>(pixel_position) + vec2f(0.5);
233+ let uv: vec2<f32> = pixel_center / vec2f(output_dimension);
234+ let ndc: vec2<f32> = uv * 2.0 - vec2f(1.0);
235+
236+ let noise = animated_blue_noise(pixel_position, u32(uniforms.frame_idx), u32(64));
237+ var rnd_state = u32(0);
238+ init_random(&rnd_state, u32(uniforms.frame_idx));
239+ init_random(&rnd_state, u32(pixel_position.x));
240+ init_random(&rnd_state, u32(pixel_position.y));
241+
242+ let jitter_scale: f32 = 1;
243+ // Apply blue noise instead of uniformFloat
244+ let jitter_x: f32 = (noise.x - 0.5) / f32(output_dimension.x) * jitter_scale;
245+ let jitter_y: f32 = (noise.y - 0.5) / f32(output_dimension.y) * jitter_scale;
246+
247+ let n2 = (ndc.x + jitter_x);
248+ let n3 = ndc.y + jitter_y;
249+ let ray = generate_pinhole_camera_ray(vec2f(n2, n3), noise);
250+
251+ var accumulated_color: vec3<f32> = vec3<f32>(0.0);
252+ let frame_weight: f32 = 1.0 / (uniforms.frame_idx + 1);
253+ let samples_per_pixel: i32 = i32(uniforms.sample_count);
254+ for (var i: i32 = 0; i < samples_per_pixel; i ++) {
255+ var pixel_color: vec3<f32> = shade_hit(ray, rnd_state, noise);
256+ var r = pixel_color.x;
257+ var g = pixel_color.y;
258+ var b = pixel_color.z;
259+ // lazy NaN catching
260+ if (r != r){ pixel_color.r = 0.0;};
261+ if (g != g){ pixel_color.g = 0.0;};
262+ if (b != b){ pixel_color.b = 0.0;};
263+ accumulated_color += pixel_color;
264+ }
265+
266+ accumulated_color = accumulated_color / f32(samples_per_pixel);
267+ var prev_color: vec3<f32> = input_buffer[pixel_idx];
268+ var final_output : vec3f = (prev_color * uniforms.frame_idx + accumulated_color) / (uniforms.frame_idx + 1.0);
269+ input_buffer[pixel_idx] = final_output;
270+ textureStore(output_buffer, pixel_position, vec4f(final_output, 1.0));
271+}
272+
273+fn trace(ray: Ray) -> HitInfo {
274+ var render_state: HitInfo;
275+ render_state.hit = false;
276+ var nearest_hit: f32 = 999.0;
277+
278+ // set up for bvh traversal
279+ var node: Node = node_tree.nodes[0];
280+ var stack: array<Node, 32>;
281+ var stack_location: i32 = 0;
282+
283+ while true {
284+ var primitive_count: u32 = u32(node.primitive_count);
285+ var contents: u32 = u32(node.left_child);
286+
287+ if primitive_count == 0 {
288+ var child1: Node = node_tree.nodes[contents];
289+ var child2: Node = node_tree.nodes[contents + 1];
290+
291+ var distance1: f32 = hit_aabb(ray, child1);
292+ var distance2: f32 = hit_aabb(ray, child2);
293+
294+ if distance1 > distance2 {
295+ var temp_dist: f32 = distance1;
296+ distance1 = distance2;
297+ distance2 = temp_dist;
298+
299+ var temp_child: Node = child1;
300+ child1 = child2;
301+ child2 = temp_child;
302+ }
303+
304+ if distance1 > nearest_hit {
305+ if stack_location == 0 {
306+ break;
307+ } else {
308+ stack_location -= 1;
309+ node = stack[stack_location];
310+ }
311+ } else {
312+ node = child1;
313+ if distance1 < nearest_hit {
314+ stack[stack_location] = child2;
315+ stack_location += 1;
316+ }
317+ }
318+ } else {
319+ for (var i: u32 = 0; i < primitive_count; i++) {
320+ var new_render_state: HitInfo = hit_triangle(
321+ ray,
322+ objects.triangles[u32(tri_lut.primitive_indices[i + contents])],
323+ 0.001,
324+ nearest_hit,
325+ render_state,
326+ );
327+ if new_render_state.hit {
328+ nearest_hit = new_render_state.dist;
329+ render_state = new_render_state;
330+ }
331+ }
332+ if stack_location == 0 {
333+ break;
334+ } else {
335+ stack_location -= 1;
336+ node = stack[stack_location];
337+ }
338+ }
339+ }
340+ return render_state;
341+}
342+
343+fn shade_hit(ray: Ray, seed: u32, noise: vec2f) -> vec3<f32> {
344+ var current_seed = seed;
345+ var radiance = vec3f(0.0);
346+ var throughput = vec3f(1.0);
347+ var result: HitInfo;
348+
349+ var temp_ray = ray;
350+ let bounces: u32 = u32(uniforms.max_depth);
351+
352+ var pdf: f32;
353+ var env_pdf: f32;
354+ var mis_weight : f32 = 1.0;
355+
356+ var sun_solid_angle = 2.0 * PI * (1.0 - cos(uniforms.sun_angular_size));
357+ let sun_pdf = 1.0 / sun_solid_angle;
358+ let sky_pdf = 1.0 / PI;
359+
360+ for (var bounce: u32 = 0; bounce < bounces; bounce++) {
361+ result = trace(temp_ray);
362+ if (!result.hit) {
363+ // We hit the environment; skip the sun for now. Atleast till this rudimentry temporal accmulation exists.
364+ // let to_sun = dot(temp_ray.direction, uniforms.sun_direction) > cos(uniforms.sun_angular_size);
365+ // let sun_radiance = sun_glow(temp_ray.direction, uniforms.sun_direction);
366+ // if (to_sun) {
367+ // radianceOut += sun_radiance;
368+ // }
369+ // if (to_sun) {
370+ // env_pdf_eval = 0.5 * sun_pdf;
371+ // }
372+ let viewZenith = abs(temp_ray.direction.z);
373+ let extinction = exp(-2.0 * pow(1.0 - viewZenith, 3.0));
374+ let skyRadiance = sky_glow(temp_ray.direction, uniforms.sun_direction) * extinction;
375+ let radianceOut = skyRadiance;
376+ if (bounce == 0) {
377+ radiance += throughput * radianceOut;
378+ break;
379+ }
380+ // bsdf generated ray carries the PDF forward to this bounce
381+ var env_pdf_eval = 0.5 * sky_pdf;
382+ let env_mis_weight = pdf / (pdf + env_pdf_eval);
383+ radiance += clamp_hdr(throughput * radianceOut * env_mis_weight, 10.0);
384+ break;
385+ }
386+
387+ let rand = vec2f(uniform_float(¤t_seed), uniform_float(¤t_seed));
388+ var material: Material = parse_textures(materials[result.material_idx], result);
389+ if (material.emission.x > 0.0 || material.emission.y > 0.0 || material.emission.z > 0.0) {
390+ radiance += throughput * material.emission;
391+ // break;
392+ }
393+
394+ // sun nee, mis weight based on prior bounce brdf
395+ let env_dir = sample_sun_cone_dir(rand);
396+ let env_color = sun_glow(env_dir, uniforms.sun_direction);
397+ let env_pdf = sun_pdf;
398+ let n_dot_env = dot(result.normal, env_dir);
399+ if (n_dot_env > 0.0 && !is_occluded(result.position, result.geo_normal, env_dir, 99999.9)) {
400+ let env_brdf = eval_brdf(result.normal, -temp_ray.direction, env_dir, material);
401+ let diffuse_density = cosine_pdf(result.normal, env_dir);
402+ let specular_density = ggx_pdf(-temp_ray.direction, result.normal, normalize(-temp_ray.direction + env_dir), material.roughness);
403+ let bsdf_pdf = 0.5 * specular_density + 0.5 * diffuse_density;
404+ let weight = env_pdf / (env_pdf + bsdf_pdf);
405+ radiance += clamp_hdr(throughput * env_brdf * env_color * n_dot_env * weight / env_pdf, 10.0);
406+ }
407+
408+ // TODO: Better selection, and also move this out.
409+ // emissive nee, uniformly sample emissives
410+ let light_index = min(u32(floor(rand.x * f32(uniforms.emissive_triangle_count))), u32(uniforms.emissive_triangle_count - 1.0));
411+ let tri_index = emissiveTriangleIndices[light_index];
412+ let tri = objects.triangles[i32(tri_index)];
413+ // uniformly sample point on triangle
414+ let u = 1.0 - rand.x;
415+ let v = rand.x * (1.0 - rand.y);
416+ let w = rand.x * rand.y;
417+ let light_pos = u * tri.corner_a + v * tri.corner_b + w * tri.corner_c;
418+ let light_normal = normalize(cross(tri.corner_b - tri.corner_a, tri.corner_c - tri.corner_a));
419+ let to_light = light_pos - result.position;
420+ let dist2 = dot(to_light, to_light);
421+ let dist = sqrt(dist2);
422+ let light_dir = to_light / dist;
423+ let cos_surf = dot(result.normal, light_dir);
424+ let cos_light = dot(light_normal, -light_dir);
425+
426+ if (cos_surf > 0.0 && cos_light > 0.0 && !is_occluded(result.position, result.geo_normal, light_dir, dist)) {
427+ var mat = materials[i32(tri.material_idx)];
428+ let direct_light_emissive_brdf = eval_brdf(result.normal, -temp_ray.direction, light_dir, material);
429+ // compute area of the triangle
430+ let edge1 = tri.corner_b - tri.corner_a;
431+ let edge2 = tri.corner_c - tri.corner_a;
432+ let area = 0.5 * length(cross(edge1, edge2));
433+ let light_power = area * mat.emission;
434+ // area to solid angle PDF conversion
435+ let pdf_solid_angle = dist2 / ( area);
436+
437+ let diffuse_pdf = cosine_pdf(result.normal, light_dir);
438+ let specular_pdf = ggx_pdf(-temp_ray.direction, result.normal, normalize(-temp_ray.direction + light_dir), material.roughness);
439+ let bsdf_pdf = 0.5 * diffuse_pdf + 0.5 * specular_pdf;
440+ let mis_weight = pdf_solid_angle / (pdf_solid_angle + bsdf_pdf);
441+ let contrib = (throughput * direct_light_emissive_brdf * light_power * mis_weight) / pdf_solid_angle;
442+ radiance += clamp_hdr(contrib, 10.0);
443+ }
444+
445+ // rr
446+ if (bounce > u32(2)) {
447+ let rrProbability = min(0.9, luminance(throughput));
448+ if (rrProbability < rand.y) {
449+ break;
450+ } else {
451+ throughput /= rrProbability;
452+ }
453+ }
454+
455+ var view_dir = -temp_ray.direction;
456+ var new_dir: vec3<f32>;
457+ var specular_density: f32;
458+ var diffuse_density: f32;
459+
460+ if (uniform_float(¤t_seed) < 0.5) {
461+ new_dir = ggx_specular_sample(view_dir, result.normal, rand, material.roughness);
462+ } else {
463+ new_dir = cosine_hemisphere_sample(result.normal, vec2f(rand.y, rand.x));
464+ }
465+ let n_dot_l = dot(result.normal, new_dir);
466+ if (n_dot_l <= 0.0) { break; }
467+ specular_density = ggx_pdf(view_dir, result.normal, normalize(view_dir + new_dir), material.roughness);
468+ diffuse_density = cosine_pdf(result.normal, normalize(new_dir));
469+ pdf = 0.5 * specular_density + 0.5 * diffuse_density;
470+
471+ let indirect_brdf = eval_brdf(result.normal, view_dir, new_dir, material);
472+ throughput *= (indirect_brdf * n_dot_l) / pdf;
473+
474+ temp_ray.origin = offset_ray(result.position, result.geo_normal);
475+ temp_ray.direction = new_dir;
476+ }
477+
478+ return radiance;
479+}
480+
481+fn hit_triangle(ray: Ray, tri: Triangle, dist_min: f32, dist_max: f32, prevRay: HitInfo) -> HitInfo {
482+ var hit: HitInfo;
483+ hit.hit = false;
484+
485+ let edge1 = tri.corner_b - tri.corner_a;
486+ let edge2 = tri.corner_c - tri.corner_a;
487+
488+ let pvec = cross(ray.direction, edge2);
489+ let determinant = dot(edge1, pvec);
490+
491+ // reject nearly parallel rays.
492+ if abs(determinant) < EPSILON {
493+ return hit;
494+ }
495+
496+ let inv_det = 1.0 / determinant;
497+ let tvec = ray.origin - tri.corner_a;
498+
499+ // compute barycentric coordinate u.
500+ let u = dot(tvec, pvec) * inv_det;
501+ if (u < 0.0 || u > 1.0) {
502+ return hit;
503+ }
504+
505+ // compute barycentric coordinate v.
506+ let qvec = cross(tvec, edge1);
507+ let v = dot(ray.direction, qvec) * inv_det;
508+ if (v < 0.0 || (u + v) > 1.0) {
509+ return hit;
510+ }
511+
512+ // calculate ray parameter (distance).
513+ let dist = dot(edge2, qvec) * inv_det;
514+ if (dist < dist_min || dist > dist_max) {
515+ return hit;
516+ }
517+
518+ // no early outs; valid hit
519+ hit.hit = true;
520+ hit.dist = dist;
521+ hit.position = ray.origin + ray.direction * dist;
522+ hit.tri = tri;
523+ hit.material_idx = i32(tri.material_idx);
524+
525+ var geo_normal = normalize(cross(edge1, edge2));
526+ var shading_normal = normalize((1.0 - u - v) * tri.normal_a + u * tri.normal_b + v * tri.normal_c);
527+ let tangent = normalize((1.0 - u - v) * tri.tangent_a + u * tri.tangent_b + v * tri.tangent_c);
528+
529+ // shadow terminator fix: warp the hit position based on vertex normals
530+ // normal aware EPSILON on hit position basically
531+ let w = 1.0 - u - v;
532+ let tmpu = hit.position - tri.corner_a;
533+ let tmpv = hit.position - tri.corner_b;
534+ let tmpw = hit.position - tri.corner_c;
535+
536+ let dotu = min(0.0, dot(tmpu, tri.normal_a));
537+ let dotv = min(0.0, dot(tmpv, tri.normal_b));
538+ let dotw = min(0.0, dot(tmpw, tri.normal_c));
539+
540+ let pu = tmpu - dotu * tri.normal_a;
541+ let pv = tmpv - dotv * tri.normal_b;
542+ let pw = tmpw - dotw * tri.normal_c;
543+
544+ let warped_offset = w * pu + u * pv + v * pw;
545+ // Move the hit point slightly along the warped vector field
546+ hit.position = hit.position + warped_offset;
547+
548+ // TBN
549+ let T = normalize(tangent.xyz);
550+ let N = normalize(shading_normal);
551+ let B = normalize(cross(N, T)) * tangent.w;
552+
553+ hit.tangent = cross(B, N);
554+ hit.normal = shading_normal;
555+ hit.uv = (1.0 - u - v) * tri.uv_a + u * tri.uv_b + v * tri.uv_c;
556+
557+ // If a normal map is present, perturb the shading normal.
558+ let material = materials[i32(tri.material_idx)];
559+ if (material.normal_texture > -1.0) {
560+ var normal_map = sample_material_texture(hit.uv, u32(material.normal_texture));
561+ var normalized_map = normalize(normal_map * 2.0 - 1.0);
562+ normalized_map.y = -normalized_map.y;
563+ let world_normal = normalize(
564+ normalized_map.x * T +
565+ normalized_map.y * B +
566+ normalized_map.z * N
567+ );
568+ hit.normal = world_normal;
569+ }
570+ var ray_dot_tri: f32 = dot(ray.direction, geo_normal);
571+ if (ray_dot_tri > 0.0) {
572+ hit.geo_normal = -hit.geo_normal;
573+ hit.normal = -hit.normal;
574+ }
575+ return hit;
576+}
577+
578+fn hit_aabb(ray: Ray, node: Node) -> f32 {
579+ var reciprocal : vec3<f32> = vec3f(1.0) / ray.direction;
580+ var t_near: vec3<f32> = (node.min_corner - ray.origin) * reciprocal;
581+ var t_far: vec3<f32> = (node.max_corner - ray.origin) * reciprocal;
582+ var t_min: vec3<f32> = min(t_near, t_far);
583+ var t_max: vec3<f32> = max(t_near, t_far);
584+
585+ var min_intersection: f32 = max(max(t_min.x, t_min.y), t_min.z); // t0
586+ var max_intersection: f32 = min(min(t_max.x, t_max.y), t_max.z); // t1
587+
588+ var mask: f32 = step(max_intersection, min_intersection) + step(max_intersection, 0.0);
589+ if min_intersection > max_intersection || max_intersection < 0 {
590+ return 9999.0;
591+ } else {
592+ return min_intersection;
593+ }
594+}
A · src/shaders/random.wgsl
+34, -0 1@@ -0,0 +1,34 @@
2+fn init_random(state: ptr<function, u32>, value: u32) {
3+ * state ^= value;
4+ * state = pcg(*state);
5+}
6+
7+fn pcg(n: u32) -> u32 {
8+ var h = n * 747796405u + 2891336453u;
9+ h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u;
10+ return (h >> 22u) ^ h;
11+}
12+
13+fn uniform_uint(state: ptr<function, u32>, max: u32) -> u32 {
14+ * state = pcg(*state);
15+ return * state % max;
16+}
17+
18+fn uniform_float(state: ptr<function, u32>) -> f32 {
19+ * state = pcg(*state);
20+ return f32(*state) / 4294967295.0;
21+}
22+
23+fn animated_blue_noise(coord: vec2<i32>, frame_count: u32, frame_count_cycle: u32) -> vec2f {
24+ // spatial
25+ let tex_size = vec2<u32>(textureDimensions(blueNoiseTexture).xy);
26+ let wrapped_coord = vec2<i32>((coord.x % i32(tex_size.x) + i32(tex_size.x)) % i32(tex_size.x), (coord.y % i32(tex_size.y) + i32(tex_size.y)) % i32(tex_size.y));
27+ let blue_noise = textureLoad(blueNoiseTexture, wrapped_coord).xy;
28+ let idx = (f32(wrapped_coord.y) % blue_noise.y) * blue_noise.x + (f32(wrapped_coord.x) % blue_noise.x);
29+ // temporal
30+ let n = frame_count % frame_count_cycle;
31+ let a1 = 0.7548776662466927f;
32+ let a2 = 0.5698402909980532f;
33+ let r2_seq = fract(vec2(a1 * f32(n), a2 * f32(n)));
34+ return fract(blue_noise + r2_seq);
35+}
A · src/shaders/sky.wgsl
+67, -0 1@@ -0,0 +1,67 @@
2+const sun_angular_size = 1.0 * (PI / 180.0);
3+
4+fn dir_in_cone(u: vec2f) -> vec3<f32> {
5+ let sun_cos_theta_max = cos(0.255f * PI / 180f);
6+ let cos_theta = 1f - u.x * (1f - sun_cos_theta_max);
7+ let sin_theta = sqrt(1f - cos_theta * cos_theta);
8+ let phi = 2f * PI * u.y;
9+ let x = cos(phi) * sin_theta;
10+ let y = sin(phi) * sin_theta;
11+ let z = cos_theta;
12+ return vec3(x, y, z);
13+}
14+
15+fn sample_sun_cone_dir(u: vec2f) -> vec3<f32> {
16+ let v = dir_in_cone(u);
17+ let onb = pixar_onb(normalize(uniforms.sun_direction));
18+ return normalize(onb * v);
19+}
20+
21+// https://www.jcgt.org/published/0006/01/01/paper-lowres.pdf
22+fn pixar_onb(n: vec3f) -> mat3x3<f32> {
23+ let s = select(- 1f, 1f, n.z >= 0f);
24+ let a = - 1f / (s + n.z);
25+ let b = n.x * n.y * a;
26+ let u = vec3(1f + s * n.x * n.x * a, s * b, - s * n.x);
27+ let v = vec3(b, s + n.y * n.y * a, - n.y);
28+ return mat3x3(u, v, n);
29+}
30+
31+// simplified approximation of preetham
32+fn sky_glow(dir: vec3f, sun_dir: vec3f) -> vec3f {
33+ let view_dir = normalize(dir);
34+ let sun_dir_n = normalize(sun_dir);
35+ let cos_theta = dot(view_dir, sun_dir_n);
36+ if sun_dir_n.z <= 0.0 {
37+ return vec3f(0.0);
38+ }
39+ // sun altitude still helps modulate overall warmth
40+ let sun_altitude = clamp(sun_dir_n.z, 0.0, 1.0);
41+ // more saturated warm tone for horizon, and deeper blue for zenith
42+ let horizon_color = vec3f(1.1, 0.4, 0.2);
43+ // rich orange
44+ let zenith_color = vec3f(0.05, 0.2, 0.6);
45+ // deep blue
46+ // exaggerated curve to preserve saturation
47+ let sky_color = mix(horizon_color, zenith_color, pow(sun_altitude, 0.1));
48+ // rayleigh-like gradient with vertical bias
49+ let rayleigh = sky_color * (0.6 + 0.4 * cos_theta * cos_theta);
50+ // warm sun glow with stronger color (no gray falloff)
51+ let mie = vec3f(1.3, 0.6, 0.3) * pow(max(cos_theta, 0.0), 12.0) * 0.6;
52+ return clamp(rayleigh + mie, vec3f(0.0), vec3f(100.0));
53+}
54+
55+fn sun_glow(dir: vec3f, sun_dir: vec3f) -> vec3f {
56+ let view_dir = normalize(dir);
57+ let sun_n = normalize(sun_dir);
58+ let cos_theta = dot(view_dir, sun_n);
59+ // angular radius (half the angular size)
60+ let angular_radius = 0.5 * uniforms.sun_angular_size;
61+ let inner = cos(angular_radius * 0.9);
62+ let outer = cos(angular_radius * 1.1);
63+ let sun_disk = smoothstep(outer, inner, max(cos_theta, 0.0));
64+ // compute sun altitude (z-up): 0 = horizon, 1 = overhead
65+ let sun_altitude = clamp(sun_n.z, 0.0, 1.0);
66+ return uniforms.sun_radiance * /*tint*/
67+ sun_disk;
68+}
A · src/shaders/types.d.ts
+4, -01@@ -0,0 +1,4 @@
2+declare module "*.wgsl" {
3+ const shader: "string";
4+ export default shader;
5+}
A · src/shaders/utils.wgsl
+37, -0 1@@ -0,0 +1,37 @@
2+fn is_nan(x: f32) -> bool {
3+ return x != x;
4+}
5+
6+fn is_inf(x: f32) -> bool {
7+ return abs(x) > 1e20;
8+}
9+
10+fn is_nan3(v: vec3<f32>) -> bool {
11+ return is_nan(v.x) || is_nan(v.y) || is_nan(v.z);
12+}
13+
14+fn is_inf3(v: vec3<f32>) -> bool {
15+ return abs(v.x) > 1e20 || abs(v.y) > 1e20 || abs(v.z) > 1e20;
16+}
17+
18+fn luminance(color: vec3<f32>) -> f32 {
19+ return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
20+}
21+
22+fn float_to_int(f: f32) -> i32 {
23+ return bitcast<i32>(f);
24+}
25+
26+fn int_to_float(i: i32) -> f32 {
27+ return bitcast<f32>(i);
28+}
29+
30+fn srgb_to_linear(rgb: vec3<f32>) -> vec3<f32> {
31+ return select(pow((rgb + 0.055) * (1.0 / 1.055), vec3<f32>(2.4)), rgb * (1.0 / 12.92), rgb <= vec3<f32>(0.04045));
32+}
33+
34+fn clamp_hdr(color: vec3<f32>, max_luminance: f32) -> vec3<f32> {
35+ let lum = dot(color, vec3f(0.2126, 0.7152, 0.0722));
36+ return color * min(1.0, max_luminance / max(lum, 1e-6));
37+}
38+
A · src/shaders/viewport.wgsl
+68, -0 1@@ -0,0 +1,68 @@
2+#include utils.wgsl
3+
4+@group(0) @binding(0) var output_buffer: texture_2d<f32>;
5+
6+struct Interpolator {
7+ @builtin(position) position: vec4<f32>,
8+ @location(0) tex_coord: vec2<f32>,
9+}
10+
11+
12+fn uncharted2_tonemap_base(x : vec3f) -> vec3f
13+{
14+ let a = 0.15;
15+ let b = 0.50;
16+ let c = 0.10;
17+ let d = 0.20;
18+ let e = 0.02;
19+ let f = 0.30;
20+ return ((x*(a*x+c*b)+d*e)/(x*(a*x+b)+d*f))-e/f;
21+}
22+
23+fn uncharted2_tonemap(color: vec3f) -> vec3f {
24+ let exposure = 2.0; // adjustable
25+ let white_point = uncharted2_tonemap_base(vec3f(11.2));
26+ let mapped = uncharted2_tonemap_base(color * exposure);
27+ return mapped / white_point;
28+}
29+
30+fn gamma_correct(color: vec3<f32>) -> vec3<f32> {
31+ return pow(color, vec3<f32>(1.0 / 2.2));
32+}
33+
34+// fn reinhard_tonemap(color: vec3<f32>) -> vec3<f32> {
35+// return color / (color + vec3f(1.0));
36+// }
37+
38+
39+@vertex
40+fn vert_main(@builtin(vertex_index) vertex_index: u32) -> Interpolator {
41+ var positions = array<vec2<f32>, 6>(
42+ vec2<f32>(1.0, 1.0),
43+ vec2<f32>(1.0, -1.0),
44+ vec2<f32>(-1.0, -1.0),
45+ vec2<f32>(1.0, 1.0),
46+ vec2<f32>(-1.0, -1.0),
47+ vec2<f32>(-1.0, 1.0)
48+ );
49+ var tex_coords = array<vec2<f32>, 6>(
50+ vec2<f32>(1.0, 1.0),
51+ vec2<f32>(1.0, 0.0),
52+ vec2<f32>(0.0, 0.0),
53+ vec2<f32>(1.0, 1.0),
54+ vec2<f32>(0.0, 0.0),
55+ vec2<f32>(0.0, 1.0)
56+ );
57+ var output: Interpolator;
58+ output.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
59+ output.tex_coord = tex_coords[vertex_index];
60+ return output;
61+}
62+
63+@fragment
64+fn frag_main(@location(0) tex_coord: vec2<f32>) -> @location(0) vec4<f32> {
65+ let dims = textureDimensions(output_buffer);
66+ let uv = vec2<u32>(tex_coord * vec2<f32>(dims));
67+ let linear_color = textureLoad(output_buffer, uv, 0).rgb;
68+ return vec4f(gamma_correct(uncharted2_tonemap(linear_color)), 1.0);
69+}
A · src/vite-env.d.ts
+2, -01@@ -0,0 +1,2 @@
2+/// <reference types="vite/client" />
3+/// <reference types="@webgpu/types" />
A · tsconfig.json
+25, -0 1@@ -0,0 +1,25 @@
2+{
3+ "compilerOptions": {
4+ "target": "ES2022",
5+ "useDefineForClassFields": true,
6+ "module": "ESNext",
7+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8+ "skipLibCheck": true,
9+
10+ /* Bundler mode */
11+ "moduleResolution": "bundler",
12+ "allowImportingTsExtensions": true,
13+ "isolatedModules": true,
14+ "moduleDetection": "force",
15+ "noEmit": true,
16+
17+ /* Linting */
18+ "strict": true,
19+ "noUnusedLocals": true,
20+ "noUnusedParameters": true,
21+ "noFallthroughCasesInSwitch": true,
22+ "noUncheckedSideEffectImports": true,
23+ "types": ["@webgpu/types", "vite-plugin-glsl/ext"]
24+ },
25+ "include": ["src"]
26+}
A · vite.config.ts
+15, -0 1@@ -0,0 +1,15 @@
2+import { defineConfig } from "vite";
3+import glsl from 'vite-plugin-glsl';
4+
5+
6+export default defineConfig({
7+ plugins: [glsl()],
8+ build: {
9+ target: "es2022",
10+ modulePreload: true,
11+ outDir: "dist",
12+ },
13+ esbuild: {
14+ target: "es2022",
15+ },
16+});