chore: squash and init public branch
| 32 files changed, 5199 insertions(+), 0 deletions(-) | |||
|---|---|---|---|
| A | .gitignore | +25 | -0 |
| A | README.md | +45 | -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
+45, -0 1@@ -0,0 +1,45 @@
2+## Overview
3+
4+An interactive path tracer implemented in WGSL. Supports multiple sampling methods, physically based materials including micro-facets, 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+- All primitives are extracted from the model -> a SAH split BVH is constructed from that 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.
12+- There is no support for transmission, IOR, or alpha textures
13+- Balanced heuristic based multiple importance sampling; two NEE rays. One for direct emissives and another for the sun
14+- Uses stratified animated blue noise for all the screen space level sampling and faster resolves.
15+- Contains a free cam and mouse look, typical `[W][A][S][D]` and `[Q][E]` for +Z, -Z respectively. `[SHIFT]` for a speed up. The camera also contains a basic thin lens approximation.
16+- Since WebGPU doesn't have bindless textures, the suggested way of doing textures is a storage buffer. This would require generating mipmaps on the host. I just stack the textures and call it a day here.
17+- All shader internals are full fat linear color space, that is then tonemapped and clamped to SRGB upon blitting.
18+
19+## Local setup
20+
21+```
22+pnpm install
23+pnpm run dev
24+```
25+
26+`/public` should contain the assets. Just compose the scene manually in `main.ts` position, scale, rotate.
27+All the included models are licensed under Creative Commons Attribtution 4.0.
28+
29+## To-do
30+
31+- Direct lighting NEE from an HDR equirectangular map
32+
33+### Resources
34+
35+- [WebGPU specification](https://www.w3.org/TR/webgpu/)
36+- [WGSL Specification](https://www.w3.org/TR/WGSL/)
37+- **Jacob Bikker:** [Invaluable BVH resource](https://jacco.ompf2.com/about-me/)
38+- **Christoph Peters:** [Math for importance sampling](https://momentsingraphics.de/)
39+- **Jakub Boksansky:** [Crash Course in BRDF Implementation](https://boksajak.github.io/files/CrashCourseBRDF.pdf)
40+- **Brent Burley:** [Physically Based Shading at Disney](https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
41+- **Möller–Trumbore:** [Ray triangle intersection test](http://www.graphics.cornell.edu/pubs/1997/MT97.pdf)
42+- **Pixar ONB:** [Building an Orthonormal Basis, Revisited
43+ ](https://www.jcgt.org/published/0006/01/01/paper-lowres.pdf)
44+- **Uncharted 2 tonemap:** [Uncharted 2: HDR Lighting](https://www.gdcvault.com/play/1012351/Uncharted-2-HDR)
45+- **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)
46+- **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, -0A · public/DuckCM.png
+0, -0A · 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, -0A · public/LDR_RGBA_0.png
+0, -0A · public/cornell_empty_rg.bin
+0, -0A · 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, -0A · public/roughness_metallic_1.jpg
+0, -0A · 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+});