pathtracer
webgpu-based path tracerchore: squash and init public branch
| 32 files changed, 5199 insertions(+), 0 deletions(-) | |||
|---|---|---|---|
| ADD | .gitignore | +25 | -0 |
| ADD | README.md | +45 | -0 |
| ADD | index.css | +44 | -0 |
| ADD | index.html | +18 | -0 |
| ADD | package-lock.json | +1262 | -0 |
| ADD | package.json | +27 | -0 |
| ADD | public/Duck.gltf | +219 | -0 |
| ADD | public/Duck0.bin | +0 | -0 |
| ADD | public/DuckCM.png | +0 | -0 |
| ADD | public/EnvironmentTest.gltf | +328 | -0 |
| ADD | public/EnvironmentTest_binary.bin | +0 | -0 |
| ADD | public/LDR_RGBA_0.png | +0 | -0 |
| ADD | public/cornell_empty_rg.bin | +0 | -0 |
| ADD | public/cornell_empty_rg.gltf | +445 | -0 |
| ADD | public/roughness_metallic_0.jpg | +0 | -0 |
| ADD | public/roughness_metallic_1.jpg | +0 | -0 |
| ADD | src/bvh.ts | +331 | -0 |
| ADD | src/camera.ts | +139 | -0 |
| ADD | src/gltf.ts | +394 | -0 |
| ADD | src/main.ts | +643 | -0 |
| ADD | src/pane.ts | +142 | -0 |
| ADD | src/shaders/any_hit.wgsl | +138 | -0 |
| ADD | src/shaders/brdf.wgsl | +154 | -0 |
| ADD | src/shaders/main.wgsl | +593 | -0 |
| ADD | src/shaders/random.wgsl | +34 | -0 |
| ADD | src/shaders/sky.wgsl | +67 | -0 |
| ADD | src/shaders/types.d.ts | +4 | -0 |
| ADD | src/shaders/utils.wgsl | +37 | -0 |
| ADD | src/shaders/viewport.wgsl | +68 | -0 |
| ADD | src/vite-env.d.ts | +2 | -0 |
| ADD | tsconfig.json | +25 | -0 |
| ADD | vite.config.ts | +15 | -0 |
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.vs<
\ No newline at end of fileADD · README.md +45 -0--- a/README.md
+++ b/README.md
@@ -0,0 +1,45 @@
+## Overview
+
+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.
+
+This is a GPU "software" path tracer, since there is no HW-accel using RT cores, it contains manual scene intersections and hit tests.
+
+- Single megakernel compute pass, that blits the output to a viewport quad texture
+- All primitives are extracted from the model -> a SAH split BVH is constructed from that on the host
+- 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
+- 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.
+- There is no support for transmission, IOR, or alpha textures
+- Balanced heuristic based multiple importance sampling; two NEE rays. One for direct emissives and another for the sun
+- Uses stratified animated blue noise for all the screen space level sampling and faster resolves.
+- 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.
+- 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.
+- All shader internals are full fat linear color space, that is then tonemapped and clamped to SRGB upon blitting.
+
+## Local setup
+
+```
+pnpm install
+pnpm run dev
+```
+
+`/public` should contain the assets. Just compose the scene manually in `main.ts` position, scale, rotate.
+All the included models are licensed under Creative Commons Attribtution 4.0.
+
+## To-do
+
+- Direct lighting NEE from an HDR equirectangular map
+
+### Resources
+
+- [WebGPU specification](https://www.w3.org/TR/webgpu/)
+- [WGSL Specification](https://www.w3.org/TR/WGSL/)
+- **Jacob Bikker:** [Invaluable BVH resource](https://jacco.ompf2.com/about-me/)
+- **Christoph Peters:** [Math for importance sampling](https://momentsingraphics.de/)
+- **Jakub Boksansky:** [Crash Course in BRDF Implementation](https://boksajak.github.io/files/CrashCourseBRDF.pdf)
+- **Brent Burley:** [Physically Based Shading at Disney](https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
+- **Möller–Trumbore:** [Ray triangle intersection test](http://www.graphics.cornell.edu/pubs/1997/MT97.pdf)
+- **Pixar ONB:** [Building an Orthonormal Basis, Revisited
+ ](https://www.jcgt.org/published/0006/01/01/paper-lowres.pdf)
+- **Uncharted 2 tonemap:** [Uncharted 2: HDR Lighting](https://www.gdcvault.com/play/1012351/Uncharted-2-HDR)
+- **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)
+- **Reference Books**: Ray tracing Gems 1 and 2, Physically based rendering 4.0.<
\ No newline at end of fileADD · index.css +44 -0--- a/index.css
+++ b/index.css
@@ -0,0 +1,44 @@
+html,
+body {
+ --bs-br: 0px !important;
+ padding: 0;
+ height: 100%;
+ margin: 0;
+ font-family: 'Courier New', Courier, monospace;
+ background-color: rgb(82, 82, 82);
+ overflow: hidden;
+}
+
+.tp-rotv, .tp-rotv * {
+ border-radius: 0px !important;
+}
+
+#render {
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+
+ transform: translate(-50%, -50%);
+ margin: 0;
+ border-radius: 8px;
+}
+
+#pane-container {
+ position: absolute;
+ z-index: 9;
+ background-color:hsl(230, 7%, 17%);
+ color: #c8cad0;
+ border: 1px solid black;
+ border-radius: 8px;
+ overflow: clip;
+ text-align: center;
+}
+
+#pane-container-header {
+ font-size: 12px;
+ font-weight: 600;
+ padding: 1px ;
+ cursor: move;
+ z-index: 10;
+}<
\ No newline at end of fileADD · index.html +18 -0--- a/index.html
+++ b/index.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <link rel="stylesheet" type="text/css" href="index.css" />
+ <script type="module" src="src/main.ts"></script>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Vite + TS</title>
+ </head>
+ <body>
+ <div id="pane-container">
+ <div id="pane-container-header">Controls</div>
+ </div>
+ <canvas id="render" width="800" height="600"></canvas>
+
+</body>
+</html>ADD · package-lock.json +1262 -0--- a/package-lock.json
+++ b/package-lock.json
@@ -0,0 +1,1262 @@
+{
+ "name": "vite-project",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vite-project",
+ "version": "0.0.0",
+ "dependencies": {
+ "@loaders.gl/core": "4.3.3",
+ "@loaders.gl/gltf": "4.3.3",
+ "gl-matrix": "^4.0.0-beta.2",
+ "tweakpane": "^4.0.5"
+ },
+ "devDependencies": {
+ "@tweakpane/core": "^2.0.5",
+ "@webgpu/types": "^0.1.53",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5",
+ "vite-plugin-glsl": "^1.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@loaders.gl/core": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.3.tgz",
+ "integrity": "sha512-RaQ3uNg4ZaVqDRgvJ2CjaOjeeHdKvbKuzFFgbGnflVB9is5bu+h3EKc3Jke7NGVvLBsZ6oIXzkwHijVsMfxv8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/loader-utils": "4.3.3",
+ "@loaders.gl/schema": "4.3.3",
+ "@loaders.gl/worker-utils": "4.3.3",
+ "@probe.gl/log": "^4.0.2"
+ }
+ },
+ "node_modules/@loaders.gl/draco": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/draco/-/draco-4.3.3.tgz",
+ "integrity": "sha512-f2isxvOoH4Pm5p4mGvNN9gVigUwX84j9gdKNMV1aSo56GS1KE3GS2rXaIoy1qaIHMzkPySUTEcOTwayf0hWU7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/loader-utils": "4.3.3",
+ "@loaders.gl/schema": "4.3.3",
+ "@loaders.gl/worker-utils": "4.3.3",
+ "draco3d": "1.5.7"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/gltf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/gltf/-/gltf-4.3.3.tgz",
+ "integrity": "sha512-M7jQ7KIB5itctDmGYuT9gndmjNwk1lwQ+BV4l5CoFp38e4xJESPglj2Kj8csWdm3WJhrxIYEP4GpjXK02n8DSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/draco": "4.3.3",
+ "@loaders.gl/images": "4.3.3",
+ "@loaders.gl/loader-utils": "4.3.3",
+ "@loaders.gl/schema": "4.3.3",
+ "@loaders.gl/textures": "4.3.3",
+ "@math.gl/core": "^4.1.0"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/images": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.3.3.tgz",
+ "integrity": "sha512-s4InjIXqEu0T7anZLj4OBUuDBt2BNnAD0GLzSexSkBfQZfpXY0XJNl4mMf5nUKb5NDfXhIKIqv8y324US+I28A==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/loader-utils": "4.3.3"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/loader-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.3.tgz",
+ "integrity": "sha512-8erUIwWLiIsZX36fFa/seZsfTsWlLk72Sibh/YZJrPAefuVucV4mGGzMBZ96LE2BUfJhadn250eio/59TUFbNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/schema": "4.3.3",
+ "@loaders.gl/worker-utils": "4.3.3",
+ "@probe.gl/log": "^4.0.2",
+ "@probe.gl/stats": "^4.0.2"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/schema": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.3.tgz",
+ "integrity": "sha512-zacc9/8je+VbuC6N/QRfiTjRd+BuxsYlddLX1u5/X/cg9s36WZZBlU1oNKUgTYe8eO6+qLyYx77yi+9JbbEehw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "^7946.0.7"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/textures": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/textures/-/textures-4.3.3.tgz",
+ "integrity": "sha512-qIo4ehzZnXFpPKl1BGQG4G3cAhBSczO9mr+H/bT7qFwtSirWVlqsvMlx1Q4VpmouDu+tudwwOlq7B3yqU5P5yQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@loaders.gl/images": "4.3.3",
+ "@loaders.gl/loader-utils": "4.3.3",
+ "@loaders.gl/schema": "4.3.3",
+ "@loaders.gl/worker-utils": "4.3.3",
+ "@math.gl/types": "^4.1.0",
+ "ktx-parse": "^0.7.0",
+ "texture-compressor": "^1.0.2"
+ },
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@loaders.gl/worker-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.3.tgz",
+ "integrity": "sha512-eg45Ux6xqsAfqPUqJkhmbFZh9qfmYuPfA+34VcLtfeXIwAngeP6o4SrTmm9LWLGUKiSh47anCEV1p7borDgvGQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@loaders.gl/core": "^4.3.0"
+ }
+ },
+ "node_modules/@math.gl/core": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz",
+ "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@math.gl/types": "4.1.0"
+ }
+ },
+ "node_modules/@math.gl/types": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz",
+ "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==",
+ "license": "MIT"
+ },
+ "node_modules/@probe.gl/env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.0.tgz",
+ "integrity": "sha512-5ac2Jm2K72VCs4eSMsM7ykVRrV47w32xOGMvcgqn8vQdEMF9PRXyBGYEV9YbqRKWNKpNKmQJVi4AHM/fkCxs9w==",
+ "license": "MIT"
+ },
+ "node_modules/@probe.gl/log": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.1.0.tgz",
+ "integrity": "sha512-r4gRReNY6f+OZEMgfWEXrAE2qJEt8rX0HsDJQXUBMoc+5H47bdB7f/5HBHAmapK8UydwPKL9wCDoS22rJ0yq7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@probe.gl/env": "4.1.0"
+ }
+ },
+ "node_modules/@probe.gl/stats": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.0.tgz",
+ "integrity": "sha512-EI413MkWKBDVNIfLdqbeNSJTs7ToBz/KVGkwi3D+dQrSIkRI2IYbWGAU3xX+D6+CI4ls8ehxMhNpUVMaZggDvQ==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
+ "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
+ "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
+ "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
+ "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
+ "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
+ "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
+ "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
+ "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
+ "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
+ "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
+ "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
+ "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
+ "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
+ "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
+ "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
+ "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
+ "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
+ "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
+ "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
+ "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tweakpane/core": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@tweakpane/core/-/core-2.0.5.tgz",
+ "integrity": "sha512-punBgD5rKCF5vcNo6BsSOXiDR/NSs9VM7SG65QSLJIxfRaGgj54ree9zQW6bO3pNFf3AogiGgaNODUVQRk9YqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.53",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.53.tgz",
+ "integrity": "sha512-x+BLw/opaz9LiVyrMsP75nO1Rg0QfrACUYIbVSfGwY/w0DiWIPYYrpte6us//KZXinxFAOJl0+C17L1Vi2vmDw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/esbuild": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.2",
+ "@esbuild/android-arm": "0.24.2",
+ "@esbuild/android-arm64": "0.24.2",
+ "@esbuild/android-x64": "0.24.2",
+ "@esbuild/darwin-arm64": "0.24.2",
+ "@esbuild/darwin-x64": "0.24.2",
+ "@esbuild/freebsd-arm64": "0.24.2",
+ "@esbuild/freebsd-x64": "0.24.2",
+ "@esbuild/linux-arm": "0.24.2",
+ "@esbuild/linux-arm64": "0.24.2",
+ "@esbuild/linux-ia32": "0.24.2",
+ "@esbuild/linux-loong64": "0.24.2",
+ "@esbuild/linux-mips64el": "0.24.2",
+ "@esbuild/linux-ppc64": "0.24.2",
+ "@esbuild/linux-riscv64": "0.24.2",
+ "@esbuild/linux-s390x": "0.24.2",
+ "@esbuild/linux-x64": "0.24.2",
+ "@esbuild/netbsd-arm64": "0.24.2",
+ "@esbuild/netbsd-x64": "0.24.2",
+ "@esbuild/openbsd-arm64": "0.24.2",
+ "@esbuild/openbsd-x64": "0.24.2",
+ "@esbuild/sunos-x64": "0.24.2",
+ "@esbuild/win32-arm64": "0.24.2",
+ "@esbuild/win32-ia32": "0.24.2",
+ "@esbuild/win32-x64": "0.24.2"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gl-matrix": {
+ "version": "4.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-4.0.0-beta.2.tgz",
+ "integrity": "sha512-OF6IkQpMkF8p2CZF9EtzYZPlPaW3M41KMsgZGlTKmMv/nWaP6GMJi9V5tI+oPn8FG0io85Q5ZtKpCXP4u6YmDA==",
+ "license": "MIT"
+ },
+ "node_modules/image-size": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz",
+ "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==",
+ "license": "MIT",
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/ktx-parse": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.7.1.tgz",
+ "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
+ "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.31.0",
+ "@rollup/rollup-android-arm64": "4.31.0",
+ "@rollup/rollup-darwin-arm64": "4.31.0",
+ "@rollup/rollup-darwin-x64": "4.31.0",
+ "@rollup/rollup-freebsd-arm64": "4.31.0",
+ "@rollup/rollup-freebsd-x64": "4.31.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.31.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.31.0",
+ "@rollup/rollup-linux-arm64-musl": "4.31.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.31.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.31.0",
+ "@rollup/rollup-linux-x64-gnu": "4.31.0",
+ "@rollup/rollup-linux-x64-musl": "4.31.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.31.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.31.0",
+ "@rollup/rollup-win32-x64-msvc": "4.31.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/texture-compressor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/texture-compressor/-/texture-compressor-1.0.2.tgz",
+ "integrity": "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.10",
+ "image-size": "^0.7.4"
+ },
+ "bin": {
+ "texture-compressor": "bin/texture-compressor.js"
+ }
+ },
+ "node_modules/tweakpane": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.5.tgz",
+ "integrity": "sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/cocopon"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.0.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.9.tgz",
+ "integrity": "sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.24.2",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-glsl": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-glsl/-/vite-plugin-glsl-1.4.0.tgz",
+ "integrity": "sha512-mjT4AaU4qRmlpawgd0M2Qz72tvK4WF0ii2p0WbVRpr7ga6+cRScJUT3oIMv5coT8u/lqUe9u9T5+0zJLZ1uhug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.1.4"
+ },
+ "engines": {
+ "node": ">= 20.17.0",
+ "npm": ">= 10.8.3"
+ },
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ }
+ }
+ }
+}ADD · package.json +27 -0--- a/package.json
+++ b/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "vite-project",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@loaders.gl/core": "4.3.3",
+ "@loaders.gl/gltf": "4.3.3",
+ "gl-matrix": "^4.0.0-beta.2",
+ "tweakpane": "^4.0.5"
+ },
+ "devDependencies": {
+ "@tweakpane/core": "^2.0.5",
+ "@webgpu/types": "^0.1.53",
+ "typescript": "~5.6.2",
+ "vite": "^6.0.5",
+ "vite-plugin-glsl": "^1.4.0"
+ },
+ "unusedDependencies": {
+ "hdr.js": "^0.2.0"
+ }
+}ADD · public/Duck.gltf +219 -0--- a/public/Duck.gltf
+++ b/public/Duck.gltf
@@ -0,0 +1,219 @@
+{
+ "asset": {
+ "generator": "COLLADA2GLTF",
+ "version": "2.0"
+ },
+ "scene": 0,
+ "scenes": [
+ {
+ "nodes": [
+ 0
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "children": [
+ 2,
+ 1
+ ],
+ "matrix": [
+ 0.009999999776482582,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.009999999776482582,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.009999999776482582,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ]
+ },
+ {
+ "matrix": [
+ -0.7289686799049377,
+ 0.0,
+ -0.6845470666885376,
+ 0.0,
+ -0.4252049028873444,
+ 0.7836934328079224,
+ 0.4527972936630249,
+ 0.0,
+ 0.5364750623703003,
+ 0.6211478114128113,
+ -0.571287989616394,
+ 0.0,
+ 400.1130065917969,
+ 463.2640075683594,
+ -431.0780334472656,
+ 1.0
+ ],
+ "camera": 0
+ },
+ {
+ "mesh": 0
+ }
+ ],
+ "cameras": [
+ {
+ "perspective": {
+ "aspectRatio": 1.5,
+ "yfov": 0.6605925559997559,
+ "zfar": 10000.0,
+ "znear": 1.0
+ },
+ "type": "perspective"
+ }
+ ],
+ "meshes": [
+ {
+ "primitives": [
+ {
+ "attributes": {
+ "NORMAL": 1,
+ "POSITION": 2,
+ "TEXCOORD_0": 3
+ },
+ "indices": 0,
+ "mode": 4,
+ "material": 0
+ }
+ ],
+ "name": "LOD3spShape"
+ }
+ ],
+ "accessors": [
+ {
+ "bufferView": 0,
+ "byteOffset": 0,
+ "componentType": 5123,
+ "count": 12636,
+ "max": [
+ 2398
+ ],
+ "min": [
+ 0
+ ],
+ "type": "SCALAR"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": 2399,
+ "max": [
+ 0.9995989799499512,
+ 0.999580979347229,
+ 0.9984359741210938
+ ],
+ "min": [
+ -0.9990839958190918,
+ -1.0,
+ -0.9998319745063782
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 1,
+ "byteOffset": 28788,
+ "componentType": 5126,
+ "count": 2399,
+ "max": [
+ 96.17990112304688,
+ 163.97000122070313,
+ 53.92519760131836
+ ],
+ "min": [
+ -69.29850006103516,
+ 9.929369926452637,
+ -61.32819747924805
+ ],
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 2,
+ "byteOffset": 0,
+ "componentType": 5126,
+ "count": 2399,
+ "max": [
+ 0.9833459854125976,
+ 0.9800369739532472
+ ],
+ "min": [
+ 0.026409000158309938,
+ 0.01996302604675293
+ ],
+ "type": "VEC2"
+ }
+ ],
+ "materials": [
+ {
+ "pbrMetallicRoughness": {
+ "baseColorTexture": {
+ "index": 0
+ },
+ "metallicFactor": 0.0
+ },
+ "emissiveFactor": [
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "name": "blinn3-fx"
+ }
+ ],
+ "textures": [
+ {
+ "sampler": 0,
+ "source": 0
+ }
+ ],
+ "images": [
+ {
+ "uri": "DuckCM.png"
+ }
+ ],
+ "samplers": [
+ {
+ "magFilter": 9729,
+ "minFilter": 9986,
+ "wrapS": 10497,
+ "wrapT": 10497
+ }
+ ],
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteOffset": 76768,
+ "byteLength": 25272,
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 0,
+ "byteLength": 57576,
+ "byteStride": 12,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 57576,
+ "byteLength": 19192,
+ "byteStride": 8,
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 102040,
+ "uri": "Duck0.bin"
+ }
+ ]
+}ADD · public/Duck0.bin +0 -0--- a/public/Duck0.bin
+++ b/public/Duck0.binADD · public/DuckCM.png +0 -0--- a/public/DuckCM.png
+++ b/public/DuckCM.pngADD · public/EnvironmentTest.gltf +328 -0--- a/public/EnvironmentTest.gltf
+++ b/public/EnvironmentTest.gltf
@@ -0,0 +1,328 @@
+{
+ "asset": {
+ "copyright": "2018 (c) Adobe Systems Inc.",
+ "generator": "Adobe Dimension - b417c10282aa66313155856d4a54e84f3f388647",
+ "version": "2.0"
+ },
+ "accessors": [
+ {
+ "bufferView": 0,
+ "componentType": 5126,
+ "count": 4598,
+ "type": "VEC3",
+ "max": [
+ 10.647041320800782,
+ 1.6470409631729127,
+ 0.6470409631729126
+ ],
+ "min": [
+ -10.647041320800782,
+ 0.3529590368270874,
+ -0.6470409631729126
+ ]
+ },
+ {
+ "bufferView": 1,
+ "componentType": 5126,
+ "count": 4598,
+ "type": "VEC3"
+ },
+ {
+ "bufferView": 2,
+ "componentType": 5126,
+ "count": 4598,
+ "type": "VEC2"
+ },
+ {
+ "bufferView": 3,
+ "componentType": 5125,
+ "count": 25344,
+ "type": "SCALAR",
+ "max": [
+ 4597
+ ],
+ "min": [
+ 0
+ ]
+ },
+ {
+ "bufferView": 4,
+ "componentType": 5126,
+ "count": 4598,
+ "type": "VEC3",
+ "max": [
+ 10.647041320800782,
+ -0.3529590368270874,
+ 0.6470409631729126
+ ],
+ "min": [
+ -10.647041320800782,
+ -1.6470409631729127,
+ -0.6470409631729126
+ ]
+ },
+ {
+ "bufferView": 5,
+ "componentType": 5126,
+ "count": 4598,
+ "type": "VEC2"
+ }
+ ],
+ "bufferViews": [
+ {
+ "buffer": 0,
+ "byteOffset": 0,
+ "byteLength": 55176,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 55176,
+ "byteLength": 55176,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 110352,
+ "byteLength": 36784,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 147136,
+ "byteLength": 101376,
+ "target": 34963
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 248512,
+ "byteLength": 55176,
+ "target": 34962
+ },
+ {
+ "buffer": 0,
+ "byteOffset": 303688,
+ "byteLength": 36784,
+ "target": 34962
+ }
+ ],
+ "buffers": [
+ {
+ "byteLength": 340472,
+ "uri": "EnvironmentTest_binary.bin"
+ }
+ ],
+ "cameras": [
+ {
+ "perspective": {
+ "znear": 0.0010000000474974514,
+ "yfov": 0.6024156808853149,
+ "zfar": 200.0,
+ "aspectRatio": 1.3333333730697632
+ },
+ "type": "perspective",
+ "name": "render_camera"
+ }
+ ],
+ "images": [
+ {
+ "name": "tmp_image_pie_dc1e_1a22_fbf9roughness_map_roughness_tmp_image_pie_dc1e_1a22_fbf9metal_map_metallic_0",
+ "uri": "roughness_metallic_0.jpg",
+ "mimeType": "image/jpeg"
+ },
+ {
+ "name": "tmp_image_pie_b20b_ebb4_317droughness_map2_roughness_tmp_image_pie_b20b_ebb4_317dmetal_map2_metallic_1",
+ "uri": "roughness_metallic_1.jpg",
+ "mimeType": "image/jpeg"
+ }
+ ],
+ "materials": [
+ {
+ "pbrMetallicRoughness": {
+ "metallicRoughnessTexture": {
+ "index": 0
+ }
+ },
+ "name": "MetallicSpheresMat",
+ "doubleSided": true
+ },
+ {
+ "pbrMetallicRoughness": {
+ "metallicRoughnessTexture": {
+ "index": 1
+ }
+ },
+ "name": "DielectricSpheresMat",
+ "doubleSided": true
+ },
+ {
+ "pbrMetallicRoughness": {
+ "baseColorFactor": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0
+ ],
+ "metallicRoughnessTexture": {
+ "index": 1
+ }
+ },
+ "name": "DielectricSpheresMat",
+ "doubleSided": true
+ }
+ ],
+ "meshes": [
+ {
+ "name": "Metallic0_N3D",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": 0,
+ "NORMAL": 1,
+ "TEXCOORD_0": 2
+ },
+ "indices": 3,
+ "material": 0
+ }
+ ]
+ },
+ {
+ "name": "Dielectric0_N3D2",
+ "primitives": [
+ {
+ "attributes": {
+ "TEXCOORD_0": 5,
+ "NORMAL": 1,
+ "POSITION": 4
+ },
+ "indices": 3,
+ "material": 1
+ }
+ ]
+ },
+ {
+ "name": "Dielectric0_N3D",
+ "primitives": [
+ {
+ "attributes": {
+ "POSITION": 4,
+ "NORMAL": 1,
+ "TEXCOORD_0": 5
+ },
+ "indices": 3,
+ "material": 2
+ }
+ ]
+ }
+ ],
+ "nodes": [
+ {
+ "matrix": [
+ 0.9999533295631409,
+ 3.16067598760128e-8,
+ 0.009662099182605744,
+ 0.0,
+ 0.0014864075928926468,
+ 0.9880954027175903,
+ -0.15383504331111909,
+ 0.0,
+ -0.009547080844640732,
+ 0.15384222567081452,
+ 0.988049328327179,
+ 0.0,
+ -0.7599077224731445,
+ 7.708760738372803,
+ 27.743375778198243,
+ 1.0
+ ],
+ "camera": 0,
+ "name": "render_camera_n3d"
+ },
+ {
+ "name": "ground_plane_n3d"
+ },
+ {
+ "children": [
+ 3,
+ 4,
+ 5
+ ],
+ "matrix": [
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+ -0.5564079284667969,
+ 4.774584770202637,
+ -1.0962677001953126,
+ 1.0
+ ],
+ "name": "ENV_Spheres"
+ },
+ {
+ "mesh": 0,
+ "name": "Metallic0"
+ },
+ {
+ "mesh": 1,
+ "name": "Dielectric0"
+ },
+ {
+ "matrix": [
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+ 0.0,
+ -1.985867977142334,
+ 0.0,
+ 1.0
+ ],
+ "mesh": 2,
+ "name": "Dielectric0-Black"
+ }
+ ],
+ "samplers": [
+ {},
+ {}
+ ],
+ "scenes": [
+ {
+ "nodes": [
+ 0,
+ 1,
+ 2
+ ],
+ "name": "scene"
+ }
+ ],
+ "textures": [
+ {
+ "name": "tmp_image_pie_dc1e_1a22_fbf9roughness_map_roughness_tmp_image_pie_dc1e_1a22_fbf9metal_map_metallic_0_texture",
+ "sampler": 0,
+ "source": 0
+ },
+ {
+ "name": "tmp_image_pie_b20b_ebb4_317droughness_map2_roughness_tmp_image_pie_b20b_ebb4_317dmetal_map2_metallic_1_texture",
+ "sampler": 1,
+ "source": 1
+ }
+ ],
+ "scene": 0
+}<
\ No newline at end of fileADD · public/EnvironmentTest_binary.bin +0 -0--- a/public/EnvironmentTest_binary.bin
+++ b/public/EnvironmentTest_binary.binADD · public/LDR_RGBA_0.png +0 -0--- a/public/LDR_RGBA_0.png
+++ b/public/LDR_RGBA_0.pngADD · public/cornell_empty_rg.bin +0 -0--- a/public/cornell_empty_rg.bin
+++ b/public/cornell_empty_rg.binADD · public/cornell_empty_rg.gltf +445 -0--- a/public/cornell_empty_rg.gltf
+++ b/public/cornell_empty_rg.gltf
@@ -0,0 +1,445 @@
+{
+ "asset":{
+ "generator":"Khronos glTF Blender I/O v4.3.47",
+ "version":"2.0"
+ },
+ "extensionsUsed":[
+ "KHR_materials_specular",
+ "KHR_materials_ior"
+ ],
+ "scene":0,
+ "scenes":[
+ {
+ "name":"Scene",
+ "nodes":[
+ 0
+ ]
+ }
+ ],
+ "nodes":[
+ {
+ "mesh":0,
+ "name":"CornellBox-Empty-RG",
+ "rotation":[
+ 0.7071068286895752,
+ 0,
+ 0,
+ 0.7071068286895752
+ ]
+ }
+ ],
+ "materials":[
+ {
+ "doubleSided":true,
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ },
+ "KHR_materials_ior":{
+ "ior":1
+ }
+ },
+ "name":"floor.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.7250000238418579,
+ 0.7099999785423279,
+ 0.6800000071525574,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ },
+ {
+ "doubleSided":true,
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ },
+ "KHR_materials_ior":{
+ "ior":1
+ }
+ },
+ "name":"ceiling.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.7250000238418579,
+ 0.7099999785423279,
+ 0.6800000071525574,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ },
+ {
+ "doubleSided":true,
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ },
+ "KHR_materials_ior":{
+ "ior":1
+ }
+ },
+ "name":"backWall.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.7250000238418579,
+ 0.7099999785423279,
+ 0.6800000071525574,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ },
+ {
+ "doubleSided":true,
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ }
+ },
+ "name":"rightWall.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.14000000059604645,
+ 0.44999998807907104,
+ 0.09099999815225601,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ },
+ {
+ "doubleSided":true,
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ }
+ },
+ "name":"leftWall.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.6299999952316284,
+ 0.06499999761581421,
+ 0.05000000074505806,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ },
+ {
+ "doubleSided":true,
+ "emissiveFactor":[
+ 1,
+ 1,
+ 1
+ ],
+ "extensions":{
+ "KHR_materials_specular":{
+ "specularFactor":0
+ },
+ "KHR_materials_ior":{
+ "ior":1
+ }
+ },
+ "name":"light.002",
+ "pbrMetallicRoughness":{
+ "baseColorFactor":[
+ 0.7799999713897705,
+ 0.7799999713897705,
+ 0.7799999713897705,
+ 1
+ ],
+ "metallicFactor":0,
+ "roughnessFactor":0.8999999761581421
+ }
+ }
+ ],
+ "meshes":[
+ {
+ "name":"CornellBox-Empty-RG.002",
+ "primitives":[
+ {
+ "attributes":{
+ "POSITION":0,
+ "NORMAL":1
+ },
+ "indices":2,
+ "material":0
+ },
+ {
+ "attributes":{
+ "POSITION":3,
+ "NORMAL":4
+ },
+ "indices":2,
+ "material":1
+ },
+ {
+ "attributes":{
+ "POSITION":5,
+ "NORMAL":6
+ },
+ "indices":2,
+ "material":2
+ },
+ {
+ "attributes":{
+ "POSITION":7,
+ "NORMAL":8
+ },
+ "indices":2,
+ "material":3
+ },
+ {
+ "attributes":{
+ "POSITION":9,
+ "NORMAL":10
+ },
+ "indices":2,
+ "material":4
+ },
+ {
+ "attributes":{
+ "POSITION":11,
+ "NORMAL":12
+ },
+ "indices":2,
+ "material":5
+ }
+ ]
+ }
+ ],
+ "accessors":[
+ {
+ "bufferView":0,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ 1,
+ 0.9900000095367432,
+ 0
+ ],
+ "min":[
+ -1.0099999904632568,
+ -1.0399999618530273,
+ 0
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":1,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":2,
+ "componentType":5123,
+ "count":6,
+ "type":"SCALAR"
+ },
+ {
+ "bufferView":3,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ 1,
+ 0.9900000095367432,
+ -1.9900000095367432
+ ],
+ "min":[
+ -1.0199999809265137,
+ -1.0399999618530273,
+ -1.9900000095367432
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":4,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":5,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ 1,
+ -1.0399999618530273,
+ 0
+ ],
+ "min":[
+ -1.0199999809265137,
+ -1.0399999618530273,
+ -1.9900000095367432
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":6,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":7,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ 1,
+ 0.9900000095367432,
+ 0
+ ],
+ "min":[
+ 1,
+ -1.0399999618530273,
+ -1.9900000095367432
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":8,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":9,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ -0.9900000095367432,
+ 0.9900000095367432,
+ 0
+ ],
+ "min":[
+ -1.0199999809265137,
+ -1.0399999618530273,
+ -1.9900000095367432
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":10,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ },
+ {
+ "bufferView":11,
+ "componentType":5126,
+ "count":4,
+ "max":[
+ 0.23000000417232513,
+ 0.1599999964237213,
+ -1.9800000190734863
+ ],
+ "min":[
+ -0.23999999463558197,
+ -0.2199999988079071,
+ -1.9800000190734863
+ ],
+ "type":"VEC3"
+ },
+ {
+ "bufferView":12,
+ "componentType":5126,
+ "count":4,
+ "type":"VEC3"
+ }
+ ],
+ "bufferViews":[
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":0,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":48,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":12,
+ "byteOffset":96,
+ "target":34963
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":108,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":156,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":204,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":252,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":300,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":348,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":396,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":444,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":492,
+ "target":34962
+ },
+ {
+ "buffer":0,
+ "byteLength":48,
+ "byteOffset":540,
+ "target":34962
+ }
+ ],
+ "buffers":[
+ {
+ "byteLength":588,
+ "uri":"cornell_empty_rg.bin"
+ }
+ ]
+}ADD · public/roughness_metallic_0.jpg +0 -0--- a/public/roughness_metallic_0.jpg
+++ b/public/roughness_metallic_0.jpgADD · public/roughness_metallic_1.jpg +0 -0--- a/public/roughness_metallic_1.jpg
+++ b/public/roughness_metallic_1.jpgADD · src/bvh.ts +331 -0--- a/src/bvh.ts
+++ b/src/bvh.ts
@@ -0,0 +1,331 @@
+import { Vec3 } from "gl-matrix";
+
+const MAX_BOUND = 999999;
+
+interface Triangle {
+ cornerA: Vec3;
+ cornerB: Vec3;
+ cornerC: Vec3;
+ centroid: Vec3;
+}
+
+interface Bin {
+ instanceCount: number;
+ bounds: AABB;
+}
+
+// just for documentation
+// interface BVHNode {
+// min: Vec3; // 12
+// max: Vec3; // 12
+// left: number; // 4
+// instanceCount: number; // 4
+// }
+
+class AABB {
+ bmin: Vec3;
+ bmax: Vec3;
+ constructor() {
+ this.bmin = Vec3.fromValues(MAX_BOUND, MAX_BOUND, MAX_BOUND);
+ this.bmax = Vec3.fromValues(-MAX_BOUND, -MAX_BOUND, -MAX_BOUND);
+ }
+
+ grow(p: Vec3) {
+ Vec3.min(this.bmin, this.bmin, p);
+ Vec3.max(this.bmax, this.bmax, p);
+ }
+
+ growAABB(aabb: AABB) {
+ // Only grow if the other AABB is valid.
+ if (aabb.bmin[0] !== MAX_BOUND) {
+ this.grow(aabb.bmin);
+ this.grow(aabb.bmax);
+ }
+ }
+
+ area() {
+ const e = Vec3.create();
+ Vec3.subtract(e, this.bmax, this.bmin);
+ // standard surface area measure (omitting the factor 2 is acceptable since SAH is relative)
+ return e[0] * e[1] + e[1] * e[2] + e[2] * e[0];
+ }
+}
+
+class BVH {
+ nodesMin: Float32Array; // min x,y,z for each node
+ nodesMax: Float32Array; // max x,y,z for each node
+ nodesLeft: Uint32Array;
+ nodesInstanceCount: Uint32Array;
+ nodesUsed: number;
+ triangles: Triangle[];
+ triIdx: Uint32Array;
+
+ constructor(triangles: Triangle[]) {
+ this.triangles = triangles;
+ this.triIdx = new Uint32Array(triangles.length);
+ this.nodesUsed = 0;
+ const maxNodes = 2 * triangles.length - 1;
+ this.nodesMin = new Float32Array(maxNodes * 3);
+ this.nodesMax = new Float32Array(maxNodes * 3);
+ this.nodesLeft = new Uint32Array(maxNodes);
+ this.nodesInstanceCount = new Uint32Array(maxNodes);
+ }
+
+ construct() {
+ for (let i = 0; i < this.triangles.length; i++) {
+ this.triIdx[i] = i;
+ }
+ this.nodesInstanceCount[0] = this.triangles.length;
+ this.nodesLeft[0] = 0; // root node
+ this.nodesUsed = 1;
+ this.bounding(0);
+ this.subdivide(0);
+ }
+
+ bounding(nodeIdx: number) {
+ const off = nodeIdx * 3;
+ // initialize the node's AABB.
+ this.nodesMin[off + 0] = MAX_BOUND;
+ this.nodesMin[off + 1] = MAX_BOUND;
+ this.nodesMin[off + 2] = MAX_BOUND;
+ this.nodesMax[off + 0] = -MAX_BOUND;
+ this.nodesMax[off + 1] = -MAX_BOUND;
+ this.nodesMax[off + 2] = -MAX_BOUND;
+
+ const count = this.nodesInstanceCount[nodeIdx];
+ const start = this.nodesLeft[nodeIdx];
+
+ // temp vectors
+ const minVec = Vec3.create();
+ const maxVec = Vec3.create();
+
+ for (let i = 0; i < count; i++) {
+ const tri = this.triangles[this.triIdx[start + i]];
+
+ // walk through each tri and update the bounds
+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerA);
+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerA);
+ this.nodesMin[off + 0] = minVec[0];
+ this.nodesMin[off + 1] = minVec[1];
+ this.nodesMin[off + 2] = minVec[2];
+ this.nodesMax[off + 0] = maxVec[0];
+ this.nodesMax[off + 1] = maxVec[1];
+ this.nodesMax[off + 2] = maxVec[2];
+
+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerB);
+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerB);
+ this.nodesMin[off + 0] = minVec[0];
+ this.nodesMin[off + 1] = minVec[1];
+ this.nodesMin[off + 2] = minVec[2];
+ this.nodesMax[off + 0] = maxVec[0];
+ this.nodesMax[off + 1] = maxVec[1];
+ this.nodesMax[off + 2] = maxVec[2];
+
+ Vec3.min(minVec, [this.nodesMin[off], this.nodesMin[off + 1], this.nodesMin[off + 2]], tri.cornerC);
+ Vec3.max(maxVec, [this.nodesMax[off], this.nodesMax[off + 1], this.nodesMax[off + 2]], tri.cornerC);
+ this.nodesMin[off + 0] = minVec[0];
+ this.nodesMin[off + 1] = minVec[1];
+ this.nodesMin[off + 2] = minVec[2];
+ this.nodesMax[off + 0] = maxVec[0];
+ this.nodesMax[off + 1] = maxVec[1];
+ this.nodesMax[off + 2] = maxVec[2];
+ }
+ }
+
+ subdivide(nodeIdx: number) {
+ // not enough primitives
+ if (this.nodesInstanceCount[nodeIdx] <= 2) return;
+
+ let [split, axis, cost] = this.findBestPlane(nodeIdx);
+
+ // eval the parent node’s extent.
+ const off = nodeIdx * 3;
+ const extent = Vec3.create();
+ Vec3.subtract(
+ extent,
+ [this.nodesMax[off + 0], this.nodesMax[off + 1], this.nodesMax[off + 2]],
+ [this.nodesMin[off + 0], this.nodesMin[off + 1], this.nodesMin[off + 2]]
+ );
+ const parentArea = extent[0] * extent[1] + extent[1] * extent[2] + extent[2] * extent[0];
+ const parentCost = this.nodesInstanceCount[nodeIdx] * parentArea;
+
+ // fallback to median split if SAH cost is not better
+ if (cost >= parentCost) {
+ let longestAxis = 0;
+ if (extent[1] > extent[0]) longestAxis = 1;
+ if (extent[2] > extent[longestAxis]) longestAxis = 2;
+ const start = this.nodesLeft[nodeIdx];
+ const count = this.nodesInstanceCount[nodeIdx];
+ const centroids: number[] = [];
+ for (let i = 0; i < count; i++) {
+ centroids.push(this.triangles[this.triIdx[start + i]].centroid[longestAxis]);
+ }
+ centroids.sort((a, b) => a - b);
+ split = centroids[Math.floor(count / 2)];
+ axis = longestAxis;
+ }
+
+ // partition primitives based on the chosen split
+ let i = this.nodesLeft[nodeIdx];
+ let j = i + this.nodesInstanceCount[nodeIdx] - 1;
+ while (i <= j) {
+ const tri = this.triangles[this.triIdx[i]];
+ if (tri.centroid[axis] < split) {
+ i++;
+ } else {
+ const tmp = this.triIdx[i];
+ this.triIdx[i] = this.triIdx[j];
+ this.triIdx[j] = tmp;
+ j--;
+ }
+ }
+ const leftCount = i - this.nodesLeft[nodeIdx];
+ if (leftCount === 0 || leftCount === this.nodesInstanceCount[nodeIdx]) return;
+
+ // construct child nodes.
+ const leftIdx = this.nodesUsed++;
+ const rightIdx = this.nodesUsed++;
+
+ this.nodesLeft[leftIdx] = this.nodesLeft[nodeIdx];
+ this.nodesInstanceCount[leftIdx] = leftCount;
+ this.nodesLeft[rightIdx] = i;
+ this.nodesInstanceCount[rightIdx] = this.nodesInstanceCount[nodeIdx] - leftCount;
+
+ // internal node
+ this.nodesLeft[nodeIdx] = leftIdx;
+ this.nodesInstanceCount[nodeIdx] = 0;
+
+ // keep going
+ this.bounding(leftIdx);
+ this.bounding(rightIdx);
+ this.subdivide(leftIdx);
+ this.subdivide(rightIdx);
+ }
+
+ findBestPlane(nodeIdx: number): [number, number, number] {
+ let bestAxis = -1;
+ let bestSplit = 0;
+ let bestCost = Infinity;
+
+ const count = this.nodesInstanceCount[nodeIdx];
+ const start = this.nodesLeft[nodeIdx];
+
+ // eval centroid bounds
+ const centroidMin = [Infinity, Infinity, Infinity];
+ const centroidMax = [-Infinity, -Infinity, -Infinity];
+ for (let i = 0; i < count; i++) {
+ const tri = this.triangles[this.triIdx[start + i]];
+ for (let axis = 0; axis < 3; axis++) {
+ centroidMin[axis] = Math.min(centroidMin[axis], tri.centroid[axis]);
+ centroidMax[axis] = Math.max(centroidMax[axis], tri.centroid[axis]);
+ }
+ }
+
+ // fallback if this centroid has a degenerate centroid distributions
+ const EPSILON = 1e-5;
+ let degenerate = false;
+ for (let axis = 0; axis < 3; axis++) {
+ if (Math.abs(centroidMax[axis] - centroidMin[axis]) < EPSILON) {
+ degenerate = true;
+ break;
+ }
+ }
+ if (degenerate) {
+ // use median split along the longest axis of actual bounds
+ let longestAxis = 0;
+ const actualMin = [Infinity, Infinity, Infinity];
+ const actualMax = [-Infinity, -Infinity, -Infinity];
+ for (let i = 0; i < count; i++) {
+ const tri = this.triangles[this.triIdx[start + i]];
+ for (let axis = 0; axis < 3; axis++) {
+ actualMin[axis] = Math.min(actualMin[axis], tri.cornerA[axis], tri.cornerB[axis], tri.cornerC[axis]);
+ actualMax[axis] = Math.max(actualMax[axis], tri.cornerA[axis], tri.cornerB[axis], tri.cornerC[axis]);
+ }
+ }
+ const extent = [actualMax[0] - actualMin[0], actualMax[1] - actualMin[1], actualMax[2] - actualMin[2]];
+ if (extent[1] > extent[0]) longestAxis = 1;
+ if (extent[2] > extent[longestAxis]) longestAxis = 2;
+ const centroids: number[] = [];
+ for (let i = 0; i < count; i++) {
+ centroids.push(this.triangles[this.triIdx[start + i]].centroid[longestAxis]);
+ }
+ centroids.sort((a, b) => a - b);
+ bestSplit = centroids[Math.floor(count / 2)];
+ bestAxis = longestAxis;
+ return [bestSplit, bestAxis, bestCost];
+ }
+
+ // use adaptive binning (bin count based on number of primitives, clamped between 8 and 32)
+ const BINS = Math.max(8, Math.min(32, count));
+ const bins: Bin[] = Array.from({ length: BINS }, () => ({
+ instanceCount: 0,
+ bounds: new AABB(),
+ }));
+
+ // for each axis, evaluate candidate splits.
+ for (let axis = 0; axis < 3; axis++) {
+ const axisMin = centroidMin[axis];
+ const axisMax = centroidMax[axis];
+ if (axisMax === axisMin) continue; // skip degenerate axis
+
+ const axisScale = BINS / (axisMax - axisMin);
+ // reset bins for this axis.
+ for (let i = 0; i < BINS; i++) {
+ bins[i].instanceCount = 0;
+ bins[i].bounds = new AABB();
+ }
+
+ // distribute primitives into bins.
+ for (let i = 0; i < count; i++) {
+ const tri = this.triangles[this.triIdx[start + i]];
+ let binIDX = Math.floor((tri.centroid[axis] - axisMin) * axisScale);
+ if (binIDX >= BINS) binIDX = BINS - 1;
+ bins[binIDX].instanceCount++;
+ bins[binIDX].bounds.grow(tri.cornerA);
+ bins[binIDX].bounds.grow(tri.cornerB);
+ bins[binIDX].bounds.grow(tri.cornerC);
+ }
+
+
+ // compute cumulative sums from left and right
+ const leftCount = new Array(BINS - 1).fill(0);
+ const leftArea = new Array(BINS - 1).fill(0);
+ const rightCount = new Array(BINS - 1).fill(0);
+ const rightArea = new Array(BINS - 1).fill(0);
+
+ let leftBox = new AABB();
+ for (let i = 0, sum = 0; i < BINS - 1; i++) {
+ sum += bins[i].instanceCount;
+ leftCount[i] = sum;
+ leftBox.growAABB(bins[i].bounds);
+ leftArea[i] = leftBox.area();
+ }
+ let rightBox = new AABB();
+ for (let i = BINS - 1, sum = 0; i > 0; i--) {
+ sum += bins[i].instanceCount;
+ rightCount[i - 1] = sum;
+ rightBox.growAABB(bins[i].bounds);
+ rightArea[i - 1] = rightBox.area();
+ }
+
+ // highly axis aligned polygons like lucy and sponza fail
+ // duct taped solution to handle degen cases
+ const binWidth = (axisMax - axisMin) / BINS;
+ // eval candidate splits.
+ for (let i = 0; i < BINS - 1; i++) {
+ // small epsilon jitter to help break ties.
+ const jitter = 1e-6;
+ const candidatePos = axisMin + binWidth * (i + 1) + jitter;
+ const cost = leftCount[i] * leftArea[i] + rightCount[i] * rightArea[i];
+ if (cost < bestCost) {
+ bestCost = cost;
+ bestAxis = axis;
+ bestSplit = candidatePos;
+ }
+ }
+ }
+ return [bestSplit, bestAxis, bestCost];
+ }
+}
+
+export default BVH;ADD · src/camera.ts +139 -0--- a/src/camera.ts
+++ b/src/camera.ts
@@ -0,0 +1,139 @@
+import { Vec3, Mat4 } from "gl-matrix";
+
+export type CameraState = {
+ position: Float32Array; // [x, y, z]
+ rotation: Float32Array; // [theta, phi]
+ view: Float32Array;
+ inverseView: Float32Array;
+ projection: Float32Array;
+ currentViewProj: Float32Array;
+ previousViewProj: Float32Array;
+ dirty: boolean;
+};
+
+const moveVec = Vec3.create();
+const lastPosition = Vec3.create();
+const lastRotation = new Float32Array(2);
+const rotateDelta = new Float32Array(2);
+const keyState = new Set<string>();
+const sprintMultiplier = 2.5;
+
+let isPointerLocked = false;
+let oldTime = 0;
+
+export function createCamera(canvas: HTMLCanvasElement): CameraState {
+ const position = Vec3.fromValues(0, -75, 20);
+ const view = Mat4.create();
+ const projection = Mat4.create();
+ const inverseView = Mat4.create();
+
+ const lookAtTarget = Vec3.fromValues(0, 0, 20);
+ const up = Vec3.fromValues(0, 0, 1);
+ Mat4.lookAt(view, position, lookAtTarget, up);
+ Mat4.invert(inverseView, view);
+ Mat4.perspectiveZO(projection, Math.PI / 4, canvas.width / canvas.height, 0.1, 100);
+
+ const forward = Vec3.create();
+ Vec3.subtract(forward, lookAtTarget, position);
+ Vec3.normalize(forward, forward);
+ const phi = Math.asin(forward[2]);
+ const theta = Math.atan2(forward[0], forward[1]);
+
+ Vec3.copy(lastPosition, position);
+ lastRotation[0] = theta;
+ lastRotation[1] = phi;
+
+ return {
+ position,
+ rotation: new Float32Array([theta, phi]),
+ view,
+ inverseView,
+ projection,
+ currentViewProj: Mat4.create(),
+ previousViewProj: Mat4.create(),
+ dirty: true,
+ };
+}
+
+export function updateCamera(cam: CameraState): void {
+ const now = performance.now() * 0.001;
+ const dt = now - oldTime;
+ oldTime = now;
+
+ const theta = cam.rotation[0] += rotateDelta[0] * dt * 60;
+ const phi = cam.rotation[1] -= rotateDelta[1] * dt * 60;
+ cam.rotation[1] = Math.min(88, Math.max(-88, cam.rotation[1]));
+ rotateDelta[0] = rotateDelta[1] = 0;
+
+ updateMovementInput();
+
+ const scaledMove = Vec3.create();
+ Vec3.scale(scaledMove, moveVec, dt);
+
+ const moved = Vec3.length(scaledMove) > 1e-5;
+ const rotated = theta !== lastRotation[0] || phi !== lastRotation[1];
+ const changedPos = !Vec3.exactEquals(cam.position, lastPosition);
+ cam.dirty = moved || rotated || changedPos;
+
+ if (!cam.dirty) return;
+
+ const forwards = Vec3.fromValues(
+ Math.sin(toRad(theta)) * Math.cos(toRad(phi)),
+ Math.cos(toRad(theta)) * Math.cos(toRad(phi)),
+ Math.sin(toRad(phi))
+ );
+ const right = Vec3.create();
+ Vec3.cross(right, forwards, [0, 0, 1]);
+ Vec3.normalize(right, right);
+
+ const up = Vec3.create();
+ Vec3.cross(up, right, forwards);
+ Vec3.normalize(up, up);
+
+ Vec3.scaleAndAdd(cam.position, cam.position, forwards, scaledMove[0]);
+ Vec3.scaleAndAdd(cam.position, cam.position, right, scaledMove[1]);
+ Vec3.scaleAndAdd(cam.position, cam.position, up, scaledMove[2]);
+
+ const target = Vec3.create();
+ Vec3.add(target, cam.position, forwards);
+ Mat4.lookAt(cam.view, cam.position, target, up);
+ Mat4.invert(cam.inverseView, cam.view);
+
+ Vec3.copy(lastPosition, cam.position);
+ lastRotation[0] = theta;
+ lastRotation[1] = phi;
+}
+
+export function updateMovementInput(): void {
+ moveVec[0] = moveVec[1] = moveVec[2] = 0;
+ const speed = keyState.has("shift") ? 10 * sprintMultiplier : 10;
+
+ if (keyState.has("w")) moveVec[0] += speed;
+ if (keyState.has("s")) moveVec[0] -= speed;
+ if (keyState.has("d")) moveVec[1] += speed;
+ if (keyState.has("a")) moveVec[1] -= speed;
+ if (keyState.has("q")) moveVec[2] += speed;
+ if (keyState.has("e")) moveVec[2] -= speed;
+}
+
+function toRad(deg: number): number {
+ return (deg * Math.PI) / 180;
+}
+
+export function setupCameraInput(canvas: HTMLCanvasElement): void {
+ canvas.addEventListener("click", () => canvas.requestPointerLock());
+
+ document.addEventListener("pointerlockchange", () => {
+ isPointerLocked = document.pointerLockElement != null;
+ });
+
+ window.addEventListener("keydown", e => keyState.add(e.key.toLowerCase()));
+ window.addEventListener("keyup", e => keyState.delete(e.key.toLowerCase()));
+
+ window.addEventListener("mousemove", (e) => {
+ if (!isPointerLocked) return;
+ const sensitivity = 0.5;
+ rotateDelta[0] = e.movementX * sensitivity;
+ rotateDelta[1] = e.movementY * sensitivity;
+ });
+}ADD · src/gltf.ts +394 -0--- a/src/gltf.ts
+++ b/src/gltf.ts
@@ -0,0 +1,394 @@
+import { load } from "@loaders.gl/core";
+import { GLTFImagePostprocessed, GLTFLoader, GLTFMeshPrimitivePostprocessed, GLTFPostprocessed, postProcessGLTF } from "@loaders.gl/gltf";
+import { Mat3, Mat4, Mat4Like, Quat, QuatLike, Vec3, Vec3Like } from "gl-matrix";
+
+interface Triangle {
+ centroid: number[];
+ cornerA: number[];
+ cornerB: number[];
+ cornerC: number[];
+ normalA: number[];
+ normalB: number[];
+ normalC: number[];
+ mat: number;
+ uvA: number[];
+ uvB: number[];
+ uvC: number[];
+ tangentA: number[];
+ tangentB: number[];
+ tangentC: number[];
+}
+
+interface ProcessedMaterial {
+ baseColorFactor: number[],
+ baseColorTexture: number, // idx
+ metallicFactor: number,
+ roughnessFactor: number,
+ metallicRoughnessTexture: number, //idx
+ normalTexture: number, //idx
+ emissiveFactor: number[],
+ emissiveTexture: number, //idx
+ alphaMode: number, // parseAlphaMode
+ alphaCutoff: number,
+ doubleSided: number,
+};
+
+interface ProcessedTexture {
+ id: String,
+ sampler: GPUSampler,
+ texture: GPUTexture,
+ view: GPUTextureView,
+ source: GLTFImagePostprocessed,
+ samplerDescriptor: GPUSamplerDescriptor,
+}
+
+export class GLTF2 {
+ triangles: Array<Triangle>;
+ materials: Array<ProcessedMaterial>; // could make material more explicit here, like with textures
+ textures: Array<ProcessedTexture>;
+ device: GPUDevice;
+ gltfData!: GLTFPostprocessed;
+ url: string;
+ scale: number[];
+ position: number[];
+ rotation?: number[];
+
+ // pre allocating these here, faster that way? Intuitevely, I could be wrong.
+ tempVec3_0: Vec3 = Vec3.create();
+ tempVec3_1: Vec3 = Vec3.create();
+ tempVec3_2: Vec3 = Vec3.create();
+ tempVec3_3: Vec3 = Vec3.create();
+ tempVec3_4: Vec3 = Vec3.create();
+ tempMat4_0: Mat4 = Mat4.create();
+ tempMat4_1: Mat4 = Mat4.create();
+ tempMat3_0: Mat3 = Mat3.create();
+
+ constructor(device: GPUDevice, url: string, scale: number[], position: number[], rotation?: number[]) {
+ this.triangles = [];
+ this.materials = [];
+ this.textures = [];
+ this.device = device;
+ this.url = url;
+ this.scale = scale;
+ this.position = position;
+ this.rotation = rotation;
+ }
+ async initialize() {
+ const t = await load(this.url, GLTFLoader);
+ this.gltfData = postProcessGLTF(t);
+ this.traverseNodes();
+ return [this.triangles, this.materials, this.textures];
+ }
+ // some data swizzling swash buckling utils
+ transformVec3(inputVec: ArrayLike<number>, matrix: Mat4): number[] {
+ const v = this.tempVec3_0;
+ Vec3.set(v, inputVec[0], inputVec[1], inputVec[2]);
+ Vec3.transformMat4(v, v, matrix);
+ return [v[0], v[1], v[2]]; // Return new array copy
+ }
+ transformNormal(inputNormal: ArrayLike<number>, transformMatrix: Mat4): number[] {
+ const normalMatrix = this.tempMat3_0; // Reused Mat3
+ const tempMatrix = this.tempMat4_1; // Use tempMat4_1 to avoid conflict with finalTransform
+ const transformedNormal = this.tempVec3_0; // Reused Vec3 for result
+ const inputNormalVec = this.tempVec3_1; // Reused Vec3 for input
+
+ // calculate transpose(invert(transformMatrix))
+ // tempMat4_1 as scratch space to avoid clobbering tempMat4_0 (finalTransform)
+ Mat4.invert(tempMatrix, transformMatrix);
+ Mat4.transpose(tempMatrix, tempMatrix);
+
+ // upper-left 3x3 submatrix
+ Mat3.fromMat4(normalMatrix, tempMatrix);
+
+ // normal into reusable Vec3
+ Vec3.set(inputNormalVec, inputNormal[0], inputNormal[1], inputNormal[2]);
+
+ // transfrom that normal
+ Vec3.transformMat3(transformedNormal, inputNormalVec, normalMatrix);
+ Vec3.normalize(transformedNormal, transformedNormal);
+
+ // new array copy
+ return [transformedNormal[0], transformedNormal[1], transformedNormal[2]];
+ }
+ parseAlphaMode(alphaMode: string) {
+ if (alphaMode === "MASK") { return 1 }
+ return 2
+ }
+
+
+ // could break this up
+ extractTriangles(primitive: GLTFMeshPrimitivePostprocessed, transform: Mat4, targetArray: Array<Triangle>) {
+ const positions = primitive.attributes["POSITION"].value;
+ const indicesData = primitive.indices ? primitive.indices.value : null;
+ const numVertices = positions.length / 3;
+ const indices = indicesData ?? (() => {
+ const generatedIndices = new Uint32Array(numVertices);
+ for (let i = 0; i < numVertices; i++) generatedIndices[i] = i;
+ return generatedIndices;
+ })();
+ const normals = primitive.attributes["NORMAL"]
+ ? primitive.attributes["NORMAL"].value
+ : null;
+ const uvCoords = primitive.attributes["TEXCOORD_0"]
+ ? primitive.attributes["TEXCOORD_0"].value
+ : null;
+ const tangents = primitive.attributes["TANGENT"]
+ ? primitive.attributes["TANGENT"].value
+ : null;
+
+ const mat = parseInt(primitive.material?.id.match(/\d+$/)?.[0] ?? "-1");
+
+ // ensure these don't clash with temps used in transformNormal/transformVec3
+ // if called within the face normal logic (they aren't).
+ const vA = this.tempVec3_1; // maybe use distinct temps if needed, but seems ok
+ const vB = this.tempVec3_2;
+ const vC = this.tempVec3_3;
+ const edge1 = this.tempVec3_4;
+ const edge2 = this.tempVec3_0; // tempVec3_0 reused safely after position transforms
+ const faceNormal = this.tempVec3_1; // tempVec3_1 reused safely after normal transforms or for input
+
+ const defaultUV = [0, 0];
+ const defaultTangent = [1, 0, 0, 1];
+
+ for (let i = 0; i < indices.length; i += 3) {
+ const ai = indices[i];
+ const bi = indices[i + 1];
+ const ci = indices[i + 2];
+
+ const posA = [positions[ai * 3], positions[ai * 3 + 1], positions[ai * 3 + 2]];
+ const posB = [positions[bi * 3], positions[bi * 3 + 1], positions[bi * 3 + 2]];
+ const posC = [positions[ci * 3], positions[ci * 3 + 1], positions[ci * 3 + 2]];
+
+ // transform positions uses tempVec3_0 internally
+ const cornerA = this.transformVec3(posA, transform);
+ const cornerB = this.transformVec3(posB, transform);
+ const cornerC = this.transformVec3(posC, transform);
+
+ let normalA: number[], normalB: number[], normalC: number[];
+ if (normals) {
+ // transform normals uses tempVec3_0, tempVec3_1 internally
+ normalA = this.transformNormal([normals[ai * 3], normals[ai * 3 + 1], normals[ai * 3 + 2]], transform);
+ normalB = this.transformNormal([normals[bi * 3], normals[bi * 3 + 1], normals[bi * 3 + 2]], transform);
+ normalC = this.transformNormal([normals[ci * 3], normals[ci * 3 + 1], normals[ci * 3 + 2]], transform);
+ } else {
+ // compute fallback flat face normal
+ Vec3.set(vA, cornerA[0], cornerA[1], cornerA[2]);
+ Vec3.set(vB, cornerB[0], cornerB[1], cornerB[2]);
+ Vec3.set(vC, cornerC[0], cornerC[1], cornerC[2]);
+
+ Vec3.subtract(edge1, vB, vA);
+ Vec3.subtract(edge2, vC, vA);
+ Vec3.cross(faceNormal, edge1, edge2);
+ Vec3.normalize(faceNormal, faceNormal);
+
+ const normalArray = [faceNormal[0], faceNormal[1], faceNormal[2]];
+ normalA = normalArray;
+ normalB = normalArray;
+ normalC = normalArray;
+ }
+
+ const uvA = uvCoords ? [uvCoords[ai * 2], uvCoords[ai * 2 + 1]] : defaultUV;
+ const uvB = uvCoords ? [uvCoords[bi * 2], uvCoords[bi * 2 + 1]] : defaultUV;
+ const uvC = uvCoords ? [uvCoords[ci * 2], uvCoords[ci * 2 + 1]] : defaultUV;
+
+ const tangentA = tangents ? [tangents[ai * 4], tangents[ai * 4 + 1], tangents[ai * 4 + 2], tangents[ai * 4 + 3]] : defaultTangent;
+ const tangentB = tangents ? [tangents[bi * 4], tangents[bi * 4 + 1], tangents[bi * 4 + 2], tangents[bi * 4 + 3]] : defaultTangent;
+ const tangentC = tangents ? [tangents[ci * 4], tangents[ci * 4 + 1], tangents[ci * 4 + 2], tangents[ci * 4 + 3]] : defaultTangent;
+
+ const centroid = [
+ (cornerA[0] + cornerB[0] + cornerC[0]) / 3,
+ (cornerA[1] + cornerB[1] + cornerC[1]) / 3,
+ (cornerA[2] + cornerB[2] + cornerC[2]) / 3,
+ ];
+
+ targetArray.push({
+ centroid,
+ cornerA, cornerB, cornerC,
+ normalA, normalB, normalC, mat,
+ uvA, uvB, uvC,
+ tangentA, tangentB, tangentC,
+ });
+ }
+ }
+
+ traverseNodes() {
+ // texture processing
+ if (this.gltfData.textures) {
+ this.gltfData.textures.forEach((texture) => {
+ if (!texture.source?.image) {
+ // empty textures are handled on atlas creation
+ return;
+ }
+ const gpuTexture = this.device.createTexture({
+ size: {
+ width: texture.source.image.width ?? 0,
+ height: texture.source.image.height ?? 0,
+ depthOrArrayLayers: 1,
+ },
+ format: "rgba8unorm",
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+
+ const view = gpuTexture.createView({ format: "rgba8unorm" });
+
+ // TODO: Process gltfData.samplers[texture.sampler] if it exists
+ let samplerDescriptor: GPUSamplerDescriptor = {
+ magFilter: "linear", minFilter: "linear",
+ addressModeU: "repeat", addressModeV: "repeat",
+ };
+ const sampler = this.device.createSampler(samplerDescriptor);
+ this.textures.push({
+ id: texture.id,
+ texture: gpuTexture,
+ view: view,
+ sampler: sampler,
+ source: texture.source,
+ samplerDescriptor: samplerDescriptor
+ });
+ });
+ }
+ if (this.gltfData.materials) {
+ this.materials = this.gltfData.materials.map(mat => {
+ return {
+ baseColorFactor: mat.pbrMetallicRoughness?.baseColorFactor ?? [1.0, 1.0, 1.0, 1.0],
+ baseColorTexture: mat.pbrMetallicRoughness?.baseColorTexture?.index ?? -1,
+ metallicFactor: mat.pbrMetallicRoughness?.metallicFactor ?? 1.0,
+ roughnessFactor: mat.pbrMetallicRoughness?.roughnessFactor ?? 1.0,
+ metallicRoughnessTexture: mat.pbrMetallicRoughness?.metallicRoughnessTexture?.index ?? -1,
+ normalTexture: mat.normalTexture?.index ?? -1,
+ emissiveFactor: mat.emissiveFactor ?? [0.0, 0.0, 0.0],
+ emissiveTexture: mat.emissiveTexture?.index ?? -1,
+ alphaMode: mat.alphaMode ? this.parseAlphaMode(mat.alphaMode) : 0,
+ alphaCutoff: mat.alphaCutoff ?? 0.5,
+ doubleSided: mat.doubleSided ? 1 : 0,
+ };
+ });
+ }
+
+ // initial node transforms
+ const finalTransform = this.tempMat4_0; // reused for final calc per node
+ const nodeLocalTransform = this.tempMat4_1; // reused for local calc per node
+ const tMat = Mat4.create();
+ const rMat = Mat4.create();
+ const sMat = Mat4.create();
+ const tMatCustom = Mat4.create();
+ const rMatCustom = Mat4.create();
+ const sMatCustom = Mat4.create();
+ const yToZUp = Mat4.fromValues(
+ 1, 0, 0, 0,
+ 0, 0, 1, 0,
+ 0, -1, 0, 0,
+ 0, 0, 0, 1
+ );
+ // scene transforms
+ const sceneTransform = Mat4.create();
+ const sc_translation = this.position || [0, 0, 0];
+ const sc_rotation = this.rotation || [0, 0, 0, 1];
+ const sc_scale = this.scale || [1, 1, 1];
+ Mat4.fromTranslation(tMatCustom, sc_translation as Vec3Like);
+ Quat.normalize(rMatCustom, sc_rotation as QuatLike);
+ Mat4.fromQuat(rMatCustom, rMatCustom);
+ Mat4.fromScaling(sMatCustom, sc_scale as Vec3Like);
+ Mat4.multiply(sceneTransform, rMatCustom, sMatCustom);
+ Mat4.multiply(sceneTransform, tMatCustom, sceneTransform);
+
+ const meshMap = new Map(this.gltfData.meshes.map(m => [m.id, m]));
+
+ for (const node of this.gltfData.nodes) {
+ if (!node.mesh?.id) continue;
+ const mesh = meshMap.get(node.mesh.id);
+ if (!mesh) continue;
+ Mat4.identity(nodeLocalTransform);
+ if (node.matrix) {
+ Mat4.copy(nodeLocalTransform, node.matrix as Mat4Like);
+ } else {
+ const nodeTranslation = node.translation || [0, 0, 0];
+ const nodeRotation = node.rotation || [0, 0, 0, 1];
+ const nodeScale = node.scale || [1, 1, 1];
+ Mat4.fromTranslation(tMat, nodeTranslation as Vec3Like);
+ Mat4.fromQuat(rMat, nodeRotation as QuatLike);
+ Mat4.fromScaling(sMat, nodeScale as Vec3Like);
+ Mat4.multiply(nodeLocalTransform, rMat, sMat);
+ Mat4.multiply(nodeLocalTransform, tMat, nodeLocalTransform);
+ }
+
+ // finalTransform = sceneTransform * yToZUp * nodeLocalTransform
+ Mat4.multiply(finalTransform, yToZUp, nodeLocalTransform);
+ Mat4.multiply(finalTransform, sceneTransform, finalTransform);
+
+ mesh.primitives.forEach((primitive: GLTFMeshPrimitivePostprocessed) => {
+ this.extractTriangles(primitive, finalTransform, this.triangles);
+ });
+ }
+ }
+}
+
+export function combineGLTFs(gltfs: GLTF2[]) {
+ const triangles = [];
+ const materials = [];
+ const textures = [];
+
+ let textureOffset = 0;
+ let materialOffset = 0;
+ let largestTextureDimensions = { width: 0, height: 0 };
+
+ // offset idx
+ const offsetIdx = (idx: any) => {
+ return (typeof idx === 'number' && idx >= 0) ? idx + textureOffset : idx; // Keep original if invalid index
+ };
+
+ for (let i = 0; i < gltfs.length; i++) {
+ const gltf = gltfs[i];
+ const texCount = gltf.textures ? gltf.textures.length : 0;
+ const matCount = gltf.materials ? gltf.materials.length : 0;
+ // just append the textures for now
+ if (gltf.textures && texCount > 0) {
+ for (let t = 0; t < texCount; t++) {
+ const texture = gltf.textures[t];
+ let texHeight = texture.source.image.height as number;
+ let texWidth = texture.source.image.width as number;
+ textures.push(texture);
+ if (texWidth > largestTextureDimensions.width) {
+ largestTextureDimensions.width = texWidth;
+ }
+ if (texHeight > largestTextureDimensions.height) {
+ largestTextureDimensions.height = texHeight;
+ }
+ }
+ }
+ if (gltf.materials && matCount > 0) {
+ for (let m = 0; m < matCount; m++) {
+ const src = gltf.materials[m];
+ materials.push({
+ alphaCutoff: src.alphaCutoff,
+ alphaMode: src.alphaMode,
+ baseColorFactor: src.baseColorFactor,
+ baseColorTexture: offsetIdx(src.baseColorTexture),
+ doubleSided: src.doubleSided,
+ emissiveFactor: src.emissiveFactor,
+ emissiveTexture: offsetIdx(src.emissiveTexture),
+ metallicFactor: src.metallicFactor,
+ metallicRoughnessTexture: offsetIdx(src.metallicRoughnessTexture),
+ normalTexture: offsetIdx(src.normalTexture),
+ roughnessFactor: src.roughnessFactor
+ });
+ }
+ }
+ // update idx if needed
+ if (gltf.triangles) {
+ for (let t = 0; t < gltf.triangles.length; t++) {
+ const tri = gltf.triangles[t];
+ const triCopy = Object.create(tri);
+ if (tri.mat >= 0) {
+ triCopy.mat = tri.mat + materialOffset;
+ } else {
+ triCopy.mat = -1;
+ }
+ triangles.push(triCopy);
+ }
+ }
+ textureOffset += texCount;
+ materialOffset += matCount;
+ }
+ return { triangles, materials, textures, largestTextureDimensions };
+}ADD · src/main.ts +643 -0--- a/src/main.ts
+++ b/src/main.ts
@@ -0,0 +1,643 @@
+import { Vec3 } from "gl-matrix";
+import BVH from "./bvh.ts";
+import { createCamera, setupCameraInput, updateCamera, updateMovementInput } from "./camera.ts";
+import { GLTF2, combineGLTFs } from "./gltf.ts";
+import { InitPane } from "./pane.ts";
+import kernel from "./shaders/main.wgsl";
+import viewport from "./shaders/viewport.wgsl";
+
+declare global {
+ interface Window {
+ framecount: number;
+ }
+}
+
+// init device
+const canvas = document.querySelector("canvas") as HTMLCanvasElement;
+const adapter = (await navigator.gpu.requestAdapter()) as GPUAdapter;
+if (!navigator.gpu) {
+ throw new Error("WebGPU not supported on this browser.");
+}
+if (!adapter) {
+ throw new Error("No appropriate GPUAdapter found.");
+}
+const device = await adapter.requestDevice({
+ requiredFeatures: ['timestamp-query']
+});
+
+const width = canvas.clientWidth;
+const height = canvas.clientHeight;
+canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
+canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
+const context = canvas.getContext("webgpu") as GPUCanvasContext;
+const format = navigator.gpu.getPreferredCanvasFormat();
+context.configure({
+ device,
+ format: format,
+});
+
+// compose scene -> triangles -> BVH -> textures
+const x = new GLTF2(device, "Duck.gltf", [0.1, 0.1, 0.1], [-13, -1, -0.34], [0, 0, -1.25, 1]);
+const y = new GLTF2(device, "cornell_empty_rg.gltf", [20, 20, 20], [0, 0, 0.01], [0,0,0,0])
+const z = new GLTF2(device, "EnvironmentTest.gltf", [1.8, 1.8, 1.8], [0, 15, 25], [0, 0, 0, 0]);
+
+await x.initialize()
+await y.initialize()
+await z.initialize()
+const t = combineGLTFs([x,y,z]);
+let ab = new BVH(t.triangles);
+ab.construct();
+const hasTextures = t.textures && t.textures.length > 0;
+const textureCount = hasTextures ? t.textures.length : 0;
+const textureSizes = new Float32Array(textureCount * 4); // [width, height, invWidth, invHeight] per texture
+console.log(t.triangles)
+// viewport texture, rgba32float; we store full fat HDR and tonemap it in this
+const viewportTexture = device.createTexture({
+ size: {
+ width: canvas.width,
+ height: canvas.height,
+ },
+ format: "rgba32float",
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
+});
+const viewportTextureColorBuffer = viewportTexture.createView();
+
+
+// offsets for buffer data
+const MaterialSize = 64;
+const materialData = new Float32Array(t.materials.length * (MaterialSize / 4));
+const MaterialInfo = {
+ albedo: { type: Float32Array, byteOffset: 0, length: 4 },
+ metallic: { type: Float32Array, byteOffset: 16, length: 1 },
+ alphaMode: { type: Float32Array, byteOffset: 20, length: 1 },
+ alphaCutoff: { type: Float32Array, byteOffset: 24, length: 1 },
+ doubleSided: { type: Float32Array, byteOffset: 28, length: 1 },
+ emission: { type: Float32Array, byteOffset: 32, length: 3 },
+ roughness: { type: Float32Array, byteOffset: 44, length: 1 },
+ baseColorTexture: { type: Float32Array, byteOffset: 48, length: 1 },
+ normalTexture: { type: Float32Array, byteOffset: 52, length: 1 },
+ metallicRoughnessTexture: { type: Float32Array, byteOffset: 56, length: 1 },
+ emissiveTexture: { type: Float32Array, byteOffset: 60, length: 1 },
+};
+
+// NB: Very fat. Trimming these to vert should be (4*3) * 3 + 12 = 48.
+// at the point if it's just 48, might be cheaper to get rid of the triangle indicies. Skip the BVH -> tri_index lookup.
+// and then resolve the shading data with tri indicies? This would still need material index thought? To do alpha tests in trace()?
+// TODO: Trim these to only verts, the material index, and the shading index. Move the rest to shadingData[]
+// 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.
+const TriangleSize = 176;
+const TriangleData = new Float32Array(t.triangles.length * (TriangleSize / 4));
+const TriangleInfo = {
+ corner_a: { type: Float32Array, byteOffset: 0, length: 3 },
+ corner_b: { type: Float32Array, byteOffset: 16, length: 3 },
+ corner_c: { type: Float32Array, byteOffset: 32, length: 3 },
+ normal_a: { type: Float32Array, byteOffset: 48, length: 3 },
+ normal_b: { type: Float32Array, byteOffset: 64, length: 3 },
+ normal_c: { type: Float32Array, byteOffset: 80, length: 3 },
+ material: { type: Float32Array, byteOffset: 92, length: 1 },
+ uVA: { type: Float32Array, byteOffset: 96, length: 2 },
+ uVB: { type: Float32Array, byteOffset: 104, length: 2 },
+ uVC: { type: Float32Array, byteOffset: 112, length: 2 },
+ tangentA: { type: Float32Array, byteOffset: 128, length: 4 },
+ tangentB: { type: Float32Array, byteOffset: 144, length: 4 },
+ tangentC: { type: Float32Array, byteOffset: 160, length: 4 },
+};
+
+// init scene buffers
+const triangleBuffer = device.createBuffer({
+ label: "Triangle Storage",
+ size: t.triangles.length * TriangleSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const materialBuffer = device.createBuffer({
+ label: "Material storage",
+ size: 8 * materialData.length,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const emissiveTrianglesBuffer = device.createBuffer({
+ label: "Emissive triangles",
+ size: t.triangles.length * TriangleSize,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const nodeBuffer = device.createBuffer({
+ size: 32 * ab.nodesUsed,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const triangleIndexBuffer = device.createBuffer({
+ size: 4 * t.triangles.length,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const accumulationBuffer = device.createBuffer({
+ size: canvas.width * canvas.height * 16,
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
+});
+const uniformBuffer0 = device.createBuffer({
+ label: "Camera Transform Buffer",
+ size: 512,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+});
+const textureSizeBuffer = device.createBuffer({
+ size: Math.max(textureSizes.byteLength, 2048),
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+});
+
+// populate buffers
+const emissiveMaterialIndices: number[] = []
+const emissiveTriangleIndices: number[] = []
+const bvhPrimitiveTriangleIndices: Float32Array = new Float32Array(ab.triIdx.length);
+
+type MaterialPropertyName = keyof typeof MaterialInfo;
+t.materials.forEach((mat, i) => {
+ const materialOffset = i * (MaterialSize / 4); // Base offset for the current material
+ const setData = (propertyName: MaterialPropertyName, value: number[]) => {
+ const info = MaterialInfo[propertyName];
+ materialData.set(value, materialOffset + info.byteOffset / 4);
+ };
+ const setFloat = (propertyName: MaterialPropertyName, value: number) => {
+ const info = MaterialInfo[propertyName];
+ materialData[materialOffset + info.byteOffset / 4] = value;
+ };
+ setData("albedo", mat.baseColorFactor); // Now sets 4 floats instead of 3
+ setFloat("metallic", mat.metallicFactor);
+ setFloat("alphaMode", mat.alphaMode);
+ setFloat("alphaCutoff", mat.alphaCutoff);
+ setFloat("doubleSided", mat.doubleSided);
+ setData("emission", mat.emissiveFactor);
+ setFloat("roughness", mat.roughnessFactor);
+ setFloat("baseColorTexture", mat.baseColorTexture);
+ setFloat("normalTexture", mat.normalTexture);
+ setFloat("metallicRoughnessTexture", mat.metallicRoughnessTexture);
+ setFloat("emissiveTexture", mat.emissiveTexture);
+ if (mat.emissiveFactor[0] !== 0 || mat.emissiveFactor[1] !== 0 || mat.emissiveFactor[2] !== 0) {
+ emissiveMaterialIndices.push(i);
+ }
+});
+device.queue.writeBuffer(materialBuffer, 0, materialData);
+
+type TrianglePropertyName = keyof typeof TriangleInfo;
+t.triangles.forEach((tri, i) => {
+ const triOffset = i * (TriangleSize / 4);
+ const setData = (propertyName: TrianglePropertyName, value: number[]) => {
+ const info = TriangleInfo[propertyName];
+ TriangleData.set(value, triOffset + info.byteOffset / 4);
+ };
+ const setFloat = (propertyName: TrianglePropertyName, value: number) => {
+ const info = TriangleInfo[propertyName];
+ TriangleData[triOffset + info.byteOffset / 4] = value;
+ };
+ setData("corner_a", tri.cornerA);
+ setData("corner_b", tri.cornerB);
+ setData("corner_c", tri.cornerC);
+ setData("normal_a", tri.normalA);
+ setData("normal_b", tri.normalB);
+ setData("normal_c", tri.normalC);
+ setFloat("material", tri.mat);
+ setData("uVA", tri.uvA);
+ setData("uVB", tri.uvB);
+ setData("uVC", tri.uvC);
+ setData("tangentA", tri.tangentA);
+ setData("tangentB", tri.tangentB);
+ setData("tangentC", tri.tangentC);
+ if (emissiveMaterialIndices.includes(tri.mat)) {
+ emissiveTriangleIndices.push(i); // Push the triangle's index
+ }
+});
+device.queue.writeBuffer(triangleBuffer, 0, TriangleData);
+device.queue.writeBuffer(emissiveTrianglesBuffer, 0, new Float32Array(emissiveTriangleIndices));
+
+const nodeData: Float32Array = new Float32Array(8 * ab.nodesUsed);
+for (let i = 0; i < ab.nodesUsed; i++) {
+ const minOffset = i * 3;
+ const maxOffset = i * 3;
+ nodeData[8 * i] = ab.nodesMin[minOffset + 0];
+ nodeData[8 * i + 1] = ab.nodesMin[minOffset + 1];
+ nodeData[8 * i + 2] = ab.nodesMin[minOffset + 2];
+ nodeData[8 * i + 3] = ab.nodesLeft[i];
+ nodeData[8 * i + 4] = ab.nodesMax[maxOffset + 0];
+ nodeData[8 * i + 5] = ab.nodesMax[maxOffset + 1];
+ nodeData[8 * i + 6] = ab.nodesMax[maxOffset + 2];
+ nodeData[8 * i + 7] = ab.nodesInstanceCount[i];
+}
+device.queue.writeBuffer(nodeBuffer, 0, nodeData, 0, 8 * ab.nodesUsed);
+
+for (let i = 0; i < ab.triIdx.length; i++) {
+ bvhPrimitiveTriangleIndices[i] = ab.triIdx[i];
+}
+device.queue.writeBuffer(triangleIndexBuffer, 0, bvhPrimitiveTriangleIndices, 0, ab.triIdx.length);
+
+// bluenoise texture 2. 3.24: Bluenoise
+// bluenoise texture form https://momentsingraphics.de/BlueNoise.html
+async function loadImageBitmap(url: string) {
+ const res = await fetch(url);
+ const blob = await res.blob();
+ return await createImageBitmap(blob, { colorSpaceConversion: 'none' });
+}
+const bnnoiseSource = await loadImageBitmap("LDR_RGBA_0.png")
+const blueNoiseTexture = device.createTexture({
+ label: 'bluenoise-texture',
+ format: 'rgba8unorm',
+ size: [bnnoiseSource.width, bnnoiseSource.height],
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
+});
+device.queue.copyExternalImageToTexture(
+ { source: bnnoiseSource },
+ { texture: blueNoiseTexture },
+ { width: bnnoiseSource.width, height: bnnoiseSource.height },
+);
+
+
+// construct the texture atlas
+const emptySampler = device.createSampler({
+ addressModeU: "clamp-to-edge",
+ addressModeV: "clamp-to-edge",
+ addressModeW: "clamp-to-edge",
+ magFilter: "nearest",
+ minFilter: "nearest",
+ mipmapFilter: "nearest",
+});
+const emptyTexture = device.createTexture({
+ size: [1, 1, 1],
+ format: "rgba8unorm",
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
+});
+const emptyView = emptyTexture.createView({
+ dimension: "2d-array",
+});
+let maxWidth = 1, maxHeight = 1;
+let textureSampler = emptySampler;
+let textureArray = emptyTexture;
+let textureViewArray = emptyView;
+if (hasTextures) {
+ maxWidth = t.largestTextureDimensions.width
+ maxHeight = t.largestTextureDimensions.height
+ textureSampler = t.textures[0].sampler // lazy, store the samplers correctly
+ textureArray = device.createTexture({
+ size: [maxWidth, maxHeight, t.textures.length],
+ format: "rgba8unorm",
+ usage:
+ GPUTextureUsage.TEXTURE_BINDING |
+ GPUTextureUsage.COPY_DST |
+ GPUTextureUsage.RENDER_ATTACHMENT,
+ dimension: "2d",
+ })
+ textureViewArray = textureArray.createView({ dimension: "2d-array" });
+}
+
+// rather wasteful (and sometimes incorrect, but whatever. Fine for now)
+// 1. get each texture's dimension
+// 2. pad it to the largest one
+// 3. store the original h w in textureSizes[]
+// 4. stack the padded texture
+if (t.textures.length) {
+ for (let i = 0; i < t.textures.length; i++) {
+ const source = t.textures[i].source;
+ // @ts-ignore / Poorly defined type for the original GLTFImagePostprocessed
+ const bitmap = source.image as ImageBitmap;
+ textureSizes[i * 4] = bitmap.width;
+ textureSizes[i * 4 + 1] = bitmap.height;
+ textureSizes[i * 4 + 2] = 0.0;
+ textureSizes[i * 4 + 3] = 0.0;
+
+ device.queue.copyExternalImageToTexture(
+ { source: bitmap },
+ { texture: textureArray, origin: [0, 0, i] },
+ [bitmap.width, bitmap.height, 1]
+ );
+ }
+ device.queue.writeBuffer(textureSizeBuffer, 0, textureSizes);
+}
+
+// bind groups and layouts
+const geometryBindgroupLayout = device.createBindGroupLayout({
+ label: 'geometry-bind-group-layout',
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: "write-only",
+ format: "rgba32float",
+ viewDimension: "2d",
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "read-only-storage",
+ hasDynamicOffset: false,
+ },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "uniform",
+ },
+ },
+
+ {
+ binding: 5,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "read-only-storage",
+ hasDynamicOffset: false,
+ },
+ },
+ {
+ binding: 6,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "read-only-storage",
+ hasDynamicOffset: false,
+ },
+ },
+ {
+ binding: 7,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "storage",
+ },
+ },
+ ],
+});
+
+const geometryBindgroup = device.createBindGroup({
+ label: 'geometry-bind-group',
+ layout: geometryBindgroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: viewportTextureColorBuffer,
+ },
+ {
+ binding: 1,
+ resource: {
+ buffer: triangleBuffer,
+ },
+ },
+ {
+ binding: 2,
+ resource: {
+ buffer: uniformBuffer0,
+ },
+ },
+ {
+ binding: 5,
+ resource: { buffer: nodeBuffer },
+ },
+ {
+ binding: 6,
+ resource: { buffer: triangleIndexBuffer },
+ },
+ {
+ binding: 7,
+ resource: { buffer: accumulationBuffer },
+ },
+ ],
+});
+
+const shadingBindGroupLayout = device.createBindGroupLayout({
+ label: 'shading-bind-group-layout',
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "read-only-storage",
+ hasDynamicOffset: false,
+ },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.COMPUTE,
+ texture: {
+ viewDimension: "2d-array",
+ },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.COMPUTE,
+ sampler: {},
+ },
+ {
+ binding: 4,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "uniform",
+ hasDynamicOffset: false,
+ },
+ },
+ {
+ binding: 6,
+ visibility: GPUShaderStage.COMPUTE,
+ storageTexture: {
+ access: "read-only",
+ format: "rgba8unorm",
+ viewDimension: "2d",
+ },
+ },
+ {
+ binding: 7,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: {
+ type: "read-only-storage",
+ hasDynamicOffset: false,
+ },
+ },
+ ],
+});
+
+const shadingBindGroup = device.createBindGroup({
+ label: 'shading-bind-group',
+ layout: shadingBindGroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: {
+ buffer: materialBuffer,
+ },
+ },
+ { binding: 1, resource: textureViewArray },
+ { binding: 2, resource: textureSampler },
+ {
+ binding: 4,
+ resource: {
+ buffer: textureSizeBuffer,
+ },
+ },
+ {
+ binding: 6,
+ resource: blueNoiseTexture.createView(),
+ },
+ {
+ binding: 7,
+ resource: {
+ buffer: emissiveTrianglesBuffer,
+ },
+ },
+ ],
+});
+
+const viewportBindgroupLayout = device.createBindGroupLayout({
+ entries: [
+ {
+ binding: 0,
+ visibility: GPUShaderStage.FRAGMENT,
+ texture: {
+ sampleType: 'unfilterable-float',
+ viewDimension: '2d',
+ multisampled: false,
+ },
+
+ },
+ ],
+});
+
+const viewportBindgroup = device.createBindGroup({
+ layout: viewportBindgroupLayout,
+ entries: [
+ {
+ binding: 0,
+ resource: viewportTextureColorBuffer,
+ },
+ ],
+});
+
+// pipelines
+const kernelPipelineLayout = device.createPipelineLayout({
+ bindGroupLayouts: [geometryBindgroupLayout, shadingBindGroupLayout],
+});
+
+const kernelPipeline = device.createComputePipeline({
+ layout: kernelPipelineLayout,
+ compute: {
+ module: device.createShaderModule({
+ code: kernel,
+ }),
+ entryPoint: "main",
+ },
+});
+
+const viewportPipelineLayout = device.createPipelineLayout({
+ bindGroupLayouts: [viewportBindgroupLayout],
+});
+
+const viewportPipeline = device.createRenderPipeline({
+ layout: viewportPipelineLayout,
+ vertex: {
+ module: device.createShaderModule({
+ code: viewport,
+ }),
+ entryPoint: "vert_main",
+ },
+ fragment: {
+ module: device.createShaderModule({
+ code: viewport,
+ }),
+ entryPoint: "frag_main",
+ targets: [
+ {
+ format: format,
+ },
+ ],
+ },
+ primitive: {
+ topology: "triangle-list",
+ },
+});
+
+
+var frametime = 0;
+window.framecount = 0;
+const UNIFORMS = {
+ sample_count: 1.0,
+ bounce_count: 3.0,
+ aperture: 0.1,
+ focal_length: 4.0,
+ frameTimeMs: 0,
+ fps: frametime / 1000,
+ sun_angle: { x: 0.3, y: -0.7, z: 0.3 },
+ sun_color: { r: 1.0, g: 0.96, b: 0.85 },
+ scale: 22000.0, // sun_color rgb -> lux scale
+ albedo_factor: z.materials[0].baseColorFactor,
+ metallicFactor: z.materials[0].metallicFactor,
+ roughnessFactor: z.materials[0].roughnessFactor,
+ thin_lens: false,
+};
+// initialize values based on UNIFORMS, updates are createPane()
+device.queue.writeBuffer(uniformBuffer0, 208, Vec3.fromValues(UNIFORMS.sun_angle.x, UNIFORMS.sun_angle.y, UNIFORMS.sun_angle.z));
+device.queue.writeBuffer(uniformBuffer0, 220, new Float32Array([0.53 * (Math.PI / 180.0)])); // ~0.5332 degrees / 32.15 arcminutes
+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));
+device.queue.writeBuffer(uniformBuffer0, 236, new Float32Array([UNIFORMS.sample_count, UNIFORMS.bounce_count, UNIFORMS.aperture, UNIFORMS.focal_length]));
+device.queue.writeBuffer(uniformBuffer0, 252, new Float32Array([emissiveTriangleIndices.length - 1, UNIFORMS.thin_lens ? 1 : 0]));
+
+let camera = createCamera(canvas);
+InitPane(device, UNIFORMS, uniformBuffer0)
+setupCameraInput(canvas)
+
+device.queue.writeBuffer(uniformBuffer0, 0, camera.position);
+device.queue.writeBuffer(uniformBuffer0, 16, camera.view);
+device.queue.writeBuffer(uniformBuffer0, 80, camera.inverseView);
+device.queue.writeBuffer(uniformBuffer0, 144, camera.projection);
+
+let cpuStart = 0;
+let cpuEnd = 0;
+let frametimeMs;
+const framedata = new Float32Array(1);
+
+const workgroupSize = 16;
+const dispatchX = Math.ceil(width / workgroupSize);
+const dispatchY = Math.ceil(height / workgroupSize);
+
+async function renderFrame() {
+ cpuStart = performance.now();
+ window.framecount++;
+ framedata[0] = window.framecount;
+ updateMovementInput();
+ updateCamera(camera);
+ if (camera.dirty) {
+ window.framecount = 0; // reset accumulation
+ device.queue.writeBuffer(uniformBuffer0, 0, camera.position);
+ device.queue.writeBuffer(uniformBuffer0, 16, camera.view);
+ device.queue.writeBuffer(uniformBuffer0, 80, camera.inverseView);
+ device.queue.writeBuffer(uniformBuffer0, 144, camera.projection);
+ }
+ device.queue.writeBuffer(uniformBuffer0, 12, framedata);
+ const commandEncoder = device.createCommandEncoder();
+ // compute pass
+ var computePass = commandEncoder.beginComputePass();
+ computePass.setPipeline(kernelPipeline);
+ computePass.setBindGroup(0, geometryBindgroup);
+ computePass.setBindGroup(1, shadingBindGroup);
+ computePass.dispatchWorkgroups(dispatchX, dispatchY);
+ computePass.end();
+ // blitt pass
+ const renderPass = commandEncoder.beginRenderPass({
+ label: "main",
+ colorAttachments: [
+ {
+ view: context.getCurrentTexture().createView(),
+ clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }, // rgba
+ loadOp: "clear",
+ storeOp: "store",
+ },
+ ],
+ });
+ renderPass.setPipeline(viewportPipeline);
+ renderPass.setBindGroup(0, viewportBindgroup);
+ renderPass.draw(6, 1, 0, 0);
+ renderPass.end();
+ device.queue.submit([commandEncoder.finish()]);
+ device.queue.onSubmittedWorkDone().then(
+ () => {
+ cpuEnd = performance.now();
+ frametimeMs = cpuEnd - cpuStart;
+ frametime = parseInt(frametimeMs.toFixed(2));
+ UNIFORMS.frameTimeMs = frametime;
+ }
+ );
+ requestAnimationFrame(renderFrame);
+}
+
+requestAnimationFrame(renderFrame);<
\ No newline at end of fileADD · src/pane.ts +142 -0--- a/src/pane.ts
+++ b/src/pane.ts
@@ -0,0 +1,142 @@
+import { Vec3 } from 'gl-matrix';
+import { Pane } from 'tweakpane';
+
+export const InitPane = (device: GPUDevice, UNIFORMS: any, uniformBuffer0: GPUBuffer) => {
+ const container = document.getElementById("pane-container") as HTMLElement;
+ const header = document.getElementById("pane-container-header") as HTMLElement;
+
+ const pane = new Pane({
+ container: container,
+ });
+ let offsetX = 0;
+ let offsetY = 0;
+ let isDragging = false;
+ header.addEventListener('mousedown', (e) => {
+ isDragging = true;
+ offsetX = e.clientX - container.offsetLeft;
+ offsetY = e.clientY - container.offsetTop;
+ container.style.cursor = 'move';
+ });
+ document.addEventListener('mousemove', (e) => {
+ if (isDragging) {
+ let x = e.clientX - offsetX;
+ let y = e.clientY - offsetY;
+
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ const containerWidth = container.offsetWidth;
+ const containerHeight = container.offsetHeight;
+
+ const minX = 0;
+ const maxX = viewportWidth - containerWidth;
+ const minY = 0;
+ const maxY = viewportHeight - containerHeight;
+
+ x = Math.max(minX, Math.min(x, maxX));
+ y = Math.max(minY, Math.min(y, maxY));
+
+ container.style.left = `${x}px`;
+ container.style.top = `${y}px`;
+ }
+ });
+ document.addEventListener('mouseup', () => {
+ isDragging = false;
+ container.style.cursor = 'default';
+ });
+
+ //packed vars: samp_count, bounce, aperture, focal_length]
+ let packed0 = new Float32Array([UNIFORMS.sample_count, UNIFORMS.bounce_count, UNIFORMS.aperture, UNIFORMS.focal_length])
+ pane.addBinding(UNIFORMS, 'frameTimeMs', {
+ readonly: true,
+ label: "Frame Time ",
+ view: 'number',
+ min: 0,
+ max: 120.00,
+ });
+ pane.addBinding(UNIFORMS, 'frameTimeMs', {
+ readonly: true,
+ label: "Frame Time Graph",
+ view: 'graph',
+ min: 0,
+ max: 120.00,
+ });
+ const cameraFolder = pane.addFolder({ title: 'Camera' })
+ cameraFolder.addBinding(UNIFORMS, 'thin_lens', {
+ label: "Enable thin lens approx",
+ }).on('change', (e) => {
+ device.queue.writeBuffer(uniformBuffer0, 256, new Float32Array([e.value]));
+ window.framecount = 0;
+ });
+ cameraFolder.addBinding(UNIFORMS, 'sample_count', {
+ view: 'slider',
+ type: 'number',
+ label: 'Sample Count',
+ min: 1,
+ max: 5,
+ value: 1,
+ step: 1,
+ }).on('change', (e) => {
+ packed0[0] = e.value;
+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
+ window.framecount = 0;
+ });
+ cameraFolder.addBinding(UNIFORMS, 'bounce_count', {
+ view: 'slider',
+ type: 'number',
+ label: 'Max Bounces',
+ min: 1,
+ max: 10,
+ value: 1,
+ step: 1,
+ }).on('change', (e) => {
+ packed0[1] = e.value;
+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
+ window.framecount = 0;
+ });
+ cameraFolder.addBinding(UNIFORMS, 'aperture', {
+ view: 'slider',
+ type: 'number',
+ label: 'Aperture',
+ min: 0.1,
+ max: 1,
+ value: 0.1,
+ }).on('change', (e) => {
+ packed0[2] = e.value;
+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
+ window.framecount = 0;
+ });
+ cameraFolder.addBinding(UNIFORMS, 'focal_length', {
+ view: 'slider',
+ type: 'number',
+ label: 'Focal Length',
+ min: 1,
+ max: 200,
+ value: 1,
+ }).on('change', (e) => {
+ packed0[3] = e.value;
+ device.queue.writeBuffer(uniformBuffer0, 236, packed0);
+ window.framecount = 0;
+ });
+ const dirFolder = pane.addFolder({ title: 'Environment' });
+ dirFolder.addBinding(UNIFORMS, 'sun_angle', {
+ label: 'Sun Angle',
+ x: { min: -1, max: 1, step: 0.1 },
+ y: { min: -1, max: 1, step: 0.1 },
+ z: { min: -1, max: 1, step: 0.1 },
+ }).on('change', () => {
+ device.queue.writeBuffer(uniformBuffer0, 208, Vec3.fromValues(UNIFORMS.sun_angle.x, UNIFORMS.sun_angle.y, UNIFORMS.sun_angle.z));
+ window.framecount = 0;
+ });
+ dirFolder.addBinding(UNIFORMS, 'sun_color', {
+ color: { type: 'float' },
+ picker: 'inline',
+ label: 'Sun Color',
+ x: { min: -1, max: 1, step: 0.1 },
+ y: { min: -1, max: 1, step: 0.1 },
+ z: { min: -1, max: 1, step: 0.1 },
+ }).on('change', (e) => {
+ device.queue.writeBuffer(uniformBuffer0, 224, Vec3.fromValues(e.value.r * UNIFORMS.scale, e.value.g * UNIFORMS.scale, e.value.b * UNIFORMS.scale));
+ window.framecount = 0;
+ });
+}<
\ No newline at end of fileADD · src/shaders/any_hit.wgsl +138 -0--- a/src/shaders/any_hit.wgsl
+++ b/src/shaders/any_hit.wgsl
@@ -0,0 +1,138 @@
+fn is_occluded(pos: vec3f, normal: vec3f, light_dir: vec3f, light_distance: f32) -> bool {
+ var shadow_ray: Ray;
+ shadow_ray.origin = offset_ray(pos, normal);
+ shadow_ray.direction = light_dir;
+ var shadow_hit = trace_any(shadow_ray, light_distance);
+ return shadow_hit;
+}
+
+fn trace_any(ray: Ray, t_max: f32) -> bool {
+ var node_idx_stack: array<u32, 64>;
+ var stack_ptr: i32 = 0;
+ var current_node_idx: u32 = 0;
+
+ while (true) {
+ let node = node_tree.nodes[current_node_idx];
+
+ let primitive_count = u32(node.primitive_count);
+ let child_or_prim_idx = u32(node.left_child);
+ if (primitive_count == 0u) {
+ // internal node
+ let left_child_idx = child_or_prim_idx;
+ let right_child_idx = child_or_prim_idx + 1u;
+
+ // use t_max for pruning
+ let hit1 = any_hit_aabb(ray, node_tree.nodes[left_child_idx].min_corner, node_tree.nodes[left_child_idx].max_corner, t_max);
+ let hit2 = any_hit_aabb(ray, node_tree.nodes[right_child_idx].min_corner, node_tree.nodes[right_child_idx].max_corner, t_max);
+
+ var near_child_idx = left_child_idx;
+ var far_child_idx = right_child_idx;
+ var dist1_hit = hit1;
+ var dist2_hit = hit2;
+
+ if (!hit1 && hit2) {
+ near_child_idx = right_child_idx;
+ far_child_idx = left_child_idx;
+ dist1_hit = hit2;
+ dist2_hit = hit1;
+ }
+
+ if (dist1_hit) {
+ current_node_idx = near_child_idx;
+ if (dist2_hit) {
+ if (stack_ptr >= 64) {
+ break;
+ }
+ // overflow
+ node_idx_stack[stack_ptr] = far_child_idx;
+ stack_ptr += 1;
+ }
+ continue;
+ // descend into near child
+ }
+ // neither child is relevant, fall through to pop
+
+ }
+ else {
+ // leaf node
+ for (var i = 0u; i < primitive_count; i += 1u) {
+ let prim_index = tri_lut.primitive_indices[child_or_prim_idx + i];
+ let triangle = objects.triangles[i32(prim_index)];
+
+ // any_hit_triangle returns true if hit within range
+ if (any_hit_triangle(ray, triangle, 0.001, t_max)) {
+ return true;
+ // found an occlusion, exit immediately
+ }
+ }
+ // finished leaf without finding occlusion, fall through to pop
+ }
+
+ // pop from stack or break if empty
+ if (stack_ptr == 0) {
+ break;
+ // traversal finished without finding occlusion
+ }
+ else {
+ stack_ptr -= 1;
+ current_node_idx = node_idx_stack[stack_ptr];
+ }
+ }
+ // kill sunlight 0.0
+ let floor_z = 0.0;
+ let denom = ray.direction.z;
+ if (abs(denom) > 1e-6) {
+ let t = (floor_z - ray.origin.z) / denom;
+ if (t > 0.001 && t < t_max) {
+ return true;
+ // hit floor within range
+ }
+ }
+ return false;
+ // no occlusion found
+}
+
+fn any_hit_aabb(ray: Ray, aabb_min: vec3f, aabb_max: vec3f, t_max: f32) -> bool {
+ var inverse_dir: vec3<f32> = vec3(1.0) / ray.direction;
+ var tmin = (aabb_min - ray.origin) * inverse_dir;
+ var tmax = (aabb_max - ray.origin) * inverse_dir;
+ var t1 = min(tmin, tmax);
+ var t2 = max(tmin, tmax);
+ var t_near = max(max(t1.x, t1.y), t1.z);
+ var t_far = min(min(t2.x, t2.y), t2.z);
+ return t_near <= t_far && t_far >= 0.001 && t_near <= t_max;
+}
+
+// lazy, just clean this up to only use hit_triangle and move all shading data post hit out
+fn any_hit_triangle(ray: Ray, tri: Triangle, dist_min: f32, dist_max: f32) -> bool {
+ let edge1 = tri.corner_b - tri.corner_a;
+ let edge2 = tri.corner_c - tri.corner_a;
+
+ let pvec = cross(ray.direction, edge2);
+ let determinant = dot(edge1, pvec);
+
+ // reject nearly parallel rays.
+ if abs(determinant) < EPSILON {
+ return false;
+ }
+
+ let inv_det = 1.0 / determinant;
+ let tvec = ray.origin - tri.corner_a;
+
+ // compute barycentric coordinate u.
+ let u = dot(tvec, pvec) * inv_det;
+ if (u < 0.0 || u > 1.0) {
+ return false;
+ }
+
+ // compute barycentric coordinate v.
+ let qvec = cross(tvec, edge1);
+ let v = dot(ray.direction, qvec) * inv_det;
+ if (v < 0.0 || (u + v) > 1.0) {
+ return false;
+ }
+
+ // calculate ray parameter (distance).
+ let dist = dot(edge2, qvec) * inv_det;
+ return dist > dist_min && dist < dist_max;
+}<
\ No newline at end of fileADD · src/shaders/brdf.wgsl +154 -0--- a/src/shaders/brdf.wgsl
+++ b/src/shaders/brdf.wgsl
@@ -0,0 +1,154 @@
+// computes the geometry (shadowing/masking) term using smith's method.
+fn smith_geometry(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, roughness: f32) -> f32 {
+ let alpha = roughness * roughness;
+ let n_dot_v = max(dot(normal, view_dir), 0.0);
+ let n_dot_l = max(dot(normal, light_dir), 0.0);
+ let k = (alpha + 1.0) * (alpha + 1.0) / 8.0;
+ let geom_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
+ let geom_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
+ return geom_v * geom_l;
+}
+
+// computes the ggx normal distribution function (ndf) d.
+fn ggx_distribution(normal: vec3<f32>, half_vec: vec3<f32>, roughness: f32) -> f32 {
+ let rgh = max(0.03, roughness);
+ let alpha = rgh * rgh;
+ let alpha2 = alpha * alpha;
+ let n_dot_h = max(dot(normal, half_vec), 0.001);
+ let denom = (n_dot_h * n_dot_h) * (alpha2 - 1.0) + 1.0;
+ return alpha2 / (PI * denom * denom);
+}
+
+// samples a half–vector (h) from the ggx distribution in tangent space.
+fn ggx_sample_vndf(view_dir: vec3<f32>, normal: vec3<f32>, roughness: f32, noise: vec2<f32>) -> vec3<f32> {
+ // build local frame (t, b, n)
+ 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);
+ let tangent = normalize(cross(up, normal));
+ let bitangent = cross(normal, tangent);
+
+ // transform view direction to local space
+ let v = normalize(vec3<f32>(dot(view_dir, tangent), dot(view_dir, bitangent), dot(view_dir, normal)));
+
+ // stretch view vector
+ let a = roughness;
+ let vh = normalize(vec3<f32>(a * v.x, a * v.y, v.z));
+
+ // orthonormal basis
+ let lensq = vh.x * vh.x + vh.y * vh.y;
+ let t1 = select(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(- vh.y, vh.x, 0.0) / sqrt(lensq), lensq > 1e-6);
+ let t2 = cross(vh, t1);
+
+ // sample point with polar coordinates
+ let r = sqrt(noise.x);
+ let phi = 2.0 * PI * noise.y;
+ let r1 = r * cos(phi);
+ let r2 = r * sin(phi);
+ let s = 0.5 * (1.0 + vh.z);
+ let t2_ = mix(sqrt(1.0 - r1 * r1), r2, s);
+
+ // sampled halfway vector in local space
+ let nh = r1 * t1 + t2_ * t2 + sqrt(max(0.0, 1.0 - r1 * r1 - t2_ * t2_)) * vh;
+
+ // unstretch
+ let h = normalize(vec3<f32>(a * nh.x, a * nh.y, max(0.0, nh.z)));
+
+ // transform back to world space
+ return normalize(h.x * tangent + h.y * bitangent + h.z * normal);
+}
+
+fn ggx_specular_sample(view_dir: vec3<f32>, normal: vec3<f32>, seed: vec2<f32>, roughness: f32) -> vec3<f32> {
+ let h = ggx_sample_vndf(view_dir, normal, roughness, seed);
+ let r = reflect(- view_dir, h);
+ return select(normalize(r), vec3<f32>(0.0), dot(r, normal) <= EPSILON);
+}
+
+// cosine-weighted hemisphere sample in local space, then converted to world space.
+fn cosine_hemisphere_sample(normal: vec3<f32>, noise: vec2<f32>) -> vec3<f32> {
+ // var current_seed = seed;
+ let r1 = noise.x;
+ let r2 = noise.y;
+
+ // let r1 = uniform_float(state);
+ // let r2 = uniform_float(state);
+
+ let phi = 2.0 * PI * r2;
+ let cos_theta = sqrt(1.0 - r1);
+ let sin_theta = sqrt(r1);
+
+ // sample in local coordinates (with z as the normal)
+ let local_sample = vec3<f32>(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
+
+ // build tangent space and transform sample to world space.
+ // let up = select(vec3<f32>(0.0, 0.0, 1.0), vec3<f32>(1.0, 0.0, 0.0), abs(normal.z) > 0.999);
+
+ 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));
+ let bitangent = cross(normal, tangent);
+
+ // let tangent = normalize(cross(up, normal));
+ // let bitangent = cross(normal, tangent);
+ let world_dir = normalize(local_sample.x * tangent + local_sample.y * bitangent + local_sample.z * normal);
+ return world_dir;
+}
+
+fn eval_f0(metallic: f32, albedo: vec3<f32>) -> vec3<f32> {
+ let dielectric_f0 = vec3<f32>(0.04);
+ return mix(dielectric_f0, albedo, metallic);
+}
+
+fn fresnel_schlick_roughness(cos_theta: f32, f0: vec3<f32>, roughness: f32) -> vec3<f32> {
+ let one_minus_cos = 1.0 - cos_theta;
+ let factor = pow(one_minus_cos, 5.0);
+ let fresnel = f0 + (max(vec3f(1.0 - roughness), f0) - f0) * factor;
+ return fresnel;
+}
+
+fn disney_diffuse(albedo: vec3<f32>, roughness: f32, n_dot_l: f32, n_dot_v: f32, l_dot_h: f32) -> vec3<f32> {
+ let fd90 = 0.5 + 2.0 * l_dot_h * l_dot_h * roughness;
+ let light_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_l, 5.0);
+ let view_scatter = 1.0 + (fd90 - 1.0) * pow(1.0 - n_dot_v, 5.0);
+ return albedo * light_scatter * view_scatter * (1.0 / PI);
+}
+
+fn eval_brdf(normal: vec3<f32>, view_dir: vec3<f32>, light_dir: vec3<f32>, material: Material) -> vec3<f32> {
+ let n = normal;
+ let v = view_dir;
+ let l = light_dir;
+ let h = normalize(v + l);
+
+ let ndot_l = max(dot(n, l), EPSILON);
+ let ndot_v = max(dot(n, v), EPSILON);
+ let ndot_h = max(dot(n, h), EPSILON);
+ let vdot_h = max(dot(v, h), EPSILON);
+ let ldot_h = max(dot(l, h), EPSILON);
+
+ let f0 = mix(vec3f(0.04), material.albedo.rgb, material.metallic);
+ let f = fresnel_schlick_roughness(vdot_h, f0, material.roughness);
+
+ // frostbite specular
+ let d = ggx_distribution(n, h, material.roughness);
+ let g = smith_geometry(n, v, l, material.roughness);
+ let spec = (d * g * f) / (4.0 * ndot_v * ndot_l + 0.001);
+
+ let diffuse = (1.0 - material.metallic) * disney_diffuse(material.albedo.rgb, material.roughness, ndot_l, ndot_v, ldot_h);
+ return diffuse + spec;
+}
+
+fn ggx_pdf(view_dir: vec3<f32>, normal: vec3<f32>, h: vec3<f32>, roughness: f32) -> f32 {
+ let d = ggx_distribution(normal, h, roughness);
+ let n_dot_h = max(dot(normal, h), EPSILON);
+ let v_dot_h = max(dot(view_dir, h), EPSILON);
+ // change–of–variables: pdf(r) = d(h) * n_dot_h / (4 * v_dot_h)
+ return (d * n_dot_h) / (4.0 * v_dot_h);
+}
+
+fn cosine_pdf(normal: vec3<f32>, dir: vec3<f32>) -> f32 {
+ // cosine weighted density: d = cos(theta) / PI.
+ let cos_theta = max(dot(normal, dir), EPSILON);
+ if cos_theta <= 0.0 {
+ return 0.0;
+ // no contribution when direction is opposite or perpendicular to the normal.
+ }
+ return cos_theta / PI;
+}
+
+ADD · src/shaders/main.wgsl +593 -0--- a/src/shaders/main.wgsl
+++ b/src/shaders/main.wgsl
@@ -0,0 +1,593 @@
+#include random.wgsl
+#include brdf.wgsl
+#include sky.wgsl
+#include any_hit.wgsl
+#include utils.wgsl
+
+@group(0) @binding(0) var output_buffer : texture_storage_2d<rgba32float, write>;
+@group(0) @binding(1) var<storage, read> objects: Objects;
+@group(0) @binding(2) var<uniform> uniforms: UniformLayout;
+@group(0) @binding(5) var<storage, read> node_tree: BVH;
+@group(0) @binding(6) var<storage, read> tri_lut: ObjectIndices;
+@group(0) @binding(7) var<storage, read_write> input_buffer:array<vec3f>;
+
+@group(1) @binding(0) var<storage, read> materials:array<Material>;
+@group(1) @binding(1) var textures: texture_2d_array<f32>;
+@group(1) @binding(2) var t_sampler: sampler;
+@group(1) @binding(4) var<uniform> textureSizes: array<vec4<f32>, 128>;
+@group(1) @binding(6) var blueNoiseTexture : texture_storage_2d<rgba8unorm, read>;
+@group(1) @binding(7) var<storage, read> emissiveTriangleIndices : array<f32>;
+
+// @group(0) @binding(3) var skybox: texture_2d<f32>;
+// @group(0) @binding(4) var skybox_sampler: sampler;
+// @group(1) @binding(5) var skyboxCDF: texture_storage_2d<rg32float, read>;
+// @group(1) @binding(3) var<storage, read> areaLights:array<AreaLight>;
+
+struct Triangle {
+ corner_a: vec3<f32>,
+ corner_b: vec3<f32>,
+ corner_c: vec3<f32>,
+ normal_a: vec3<f32>,
+ normal_b: vec3<f32>,
+ normal_c: vec3<f32>,
+ material_idx: f32,
+ uv_a: vec2<f32>,
+ uv_b: vec2<f32>,
+ uv_c: vec2<f32>,
+ tangent_a: vec4f,
+ tangent_b: vec4f,
+ tangent_c: vec4f,
+}
+
+// struct AreaLight {
+// center: vec3<f32>,
+// u: vec3<f32>,
+// v: vec3<f32>,
+// normal: vec3<f32>,
+// emission: vec3<f32>,
+// };
+
+struct Ray {
+ direction: vec3<f32>,
+ origin: vec3<f32>,
+}
+
+struct HitInfo {
+ dist: f32,
+ hit: bool,
+ position: vec3<f32>,
+ normal: vec3<f32>,
+ material_idx: i32,
+ geo_normal: vec3f,
+ tri: Triangle,
+ uv: vec2f,
+ tangent: vec3<f32>,
+ bitangent: vec3<f32>,
+}
+
+struct UniformLayout {
+ position: vec3<f32>,
+ frame_idx: f32,
+ view: mat4x4<f32>,
+ inverse_view: mat4x4<f32>,
+ projection: mat4x4<f32>,
+ sun_direction: vec3<f32>,
+ sun_angular_size: f32,
+ sun_radiance: vec3<f32>,
+ sample_count: f32,
+ max_depth: f32,
+ aperture: f32,
+ focus_distance: f32,
+ emissive_triangle_count: f32,
+ thin_lens: f32,
+}
+
+struct Node {
+ min_corner: vec3<f32>,
+ left_child: f32,
+ max_corner: vec3<f32>,
+ primitive_count: f32,
+}
+
+struct BVH {
+ nodes: array<Node>,
+}
+
+struct ObjectIndices {
+ primitive_indices: array<f32>,
+}
+
+struct Objects {
+ triangles: array<Triangle>,
+}
+
+struct Material {
+ albedo: vec4<f32>,
+ metallic: f32,
+ alpha_mode: f32,
+ alpha_cutoff: f32,
+ double_sided: f32,
+ emission: vec3<f32>,
+ roughness: f32,
+ base_color_texture: f32,
+ normal_texture: f32,
+ metallic_roughness_texture: f32,
+ emissive_texture: f32,
+}
+
+
+
+const EPSILON :f32 = 0.00001f;
+const PI :f32 = 3.1415927f;
+// ray tracing gems part 1 chapter 6
+const FLOAT_SCALE = 1.0 / 65536.0;
+const INT_SCALE = 256.0;
+const ORIGIN = 1.0 / 32.0;
+
+// Slightly offsets a ray to prevent self intersection artifacts
+// Ray tracing gems part 1 chapter 6
+fn offset_ray(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
+ let of_i = vec3<i32>(
+ i32(INT_SCALE * n.x),
+ i32(INT_SCALE * n.y),
+ i32(INT_SCALE * n.z)
+ );
+
+ let p_i = vec3<f32>(
+ int_to_float(float_to_int(p.x) + select(of_i.x, -of_i.x, p.x < 0.0)),
+ int_to_float(float_to_int(p.y) + select(of_i.y, -of_i.y, p.y < 0.0)),
+ int_to_float(float_to_int(p.z) + select(of_i.z, -of_i.z, p.z < 0.0))
+ );
+
+ return vec3<f32>(
+ select(p.x + FLOAT_SCALE * n.x, p_i.x, abs(p.x) >= ORIGIN),
+ select(p.y + FLOAT_SCALE * n.y, p_i.y, abs(p.y) >= ORIGIN),
+ select(p.z + FLOAT_SCALE * n.z, p_i.z, abs(p.z) >= ORIGIN)
+ );
+}
+
+fn sample_material_texture(uv: vec2<f32>, texture_index: u32) -> vec4<f32> {
+ let tex_size = textureSizes[texture_index].xy;
+ let max_tex_size = vec2<f32>(textureDimensions(textures).xy);
+ // let scaled_uv = uv * tex_size / max_tex_size;
+ // let clamped_uv = clamp(scaled_uv, vec2<f32>(0.0), vec2<f32>(1.0));
+ // compute the valid uv bounds inside the texture array
+ let tex_uv_min = vec2<f32>(0.0); // always starts at (0,0)
+ let tex_uv_max = tex_size / max_tex_size; // upper-right boundary in the atlas
+ // remap u_vs to this valid range
+ let mapped_uv = mix(tex_uv_min, tex_uv_max, uv);
+ return textureSampleLevel(textures, t_sampler, mapped_uv, texture_index, 1.0).rgba;
+}
+
+
+fn parse_textures(curr_material: Material, result: HitInfo) -> Material {
+ var material = curr_material;
+ if material.base_color_texture > -1.0 {
+ material.albedo *= sample_material_texture(result.uv, u32(curr_material.base_color_texture)).rgba;
+ }
+ if material.metallic_roughness_texture > -1.0 {
+ let metallic_roughness_texture = sample_material_texture(result.uv, u32(curr_material.metallic_roughness_texture));
+ material.roughness *= metallic_roughness_texture.g;
+ material.metallic *= metallic_roughness_texture.b;
+ }
+ if material.emissive_texture > -1.0 {
+ material.emission = sample_material_texture(result.uv, u32(curr_material.emissive_texture)).rgb;
+ }
+ return material;
+}
+
+
+fn point_in_unit_disk(u: vec2f) -> vec2f {
+ let r = sqrt(u.x);
+ let theta = 2f * PI * u.y;
+ return vec2f(r * cos(theta), r * sin(theta));
+}
+
+fn generate_pinhole_camera_ray(ndc: vec2<f32>, noise: vec2f) -> Ray {
+ var ray : Ray;
+ let aspect = uniforms.projection[1][1] / uniforms.projection[0][0]; // same as 1/tan_half_fov_y divided by 1/tan_half_fov_x
+ let tan_half_fov_y = 1.0 / uniforms.projection[1][1];
+
+ let x = ndc.x * aspect * tan_half_fov_y;
+ let y = ndc.y * tan_half_fov_y;
+
+ // camera basis vectors from the view matrix
+ let right = uniforms.inverse_view[0].xyz;
+ let up = uniforms.inverse_view[1].xyz;
+ let forward = -uniforms.inverse_view[2].xyz;
+ let origin = uniforms.position;
+
+ let pinhole_dir = normalize(x * right + y * up + forward);
+
+ let focus_dist = uniforms.focus_distance;
+ let aperture = uniforms.aperture;
+ let focus_point = origin + pinhole_dir * focus_dist;
+
+ // sample lens (in local right-up plane)
+ let lens_sample = point_in_unit_disk(noise) * aperture;
+ let lens_offset = lens_sample.x * right + lens_sample.y * up;
+
+ if (uniforms.thin_lens == 0.0){
+ ray.origin = origin;
+ ray.direction = pinhole_dir;
+ } else {
+ ray.origin = origin + lens_offset;
+ ray.direction = normalize(focus_point - ray.origin);
+ }
+ return ray;
+}
+
+
+@compute @workgroup_size(16, 16)
+fn main(
+ @builtin(global_invocation_id) GlobalInvocationID: vec3<u32>,
+ @builtin(local_invocation_id) LocalInvocationID: vec3<u32>,
+ @builtin(workgroup_id) GroupIndex: vec3<u32>) {
+ // https://www.w3.org/TR/webgpu/#coordinate-systems
+ let output_dimension: vec2<i32> = vec2<i32>(textureDimensions(output_buffer));
+ let pixel_position: vec2<i32> = vec2<i32>(i32(GlobalInvocationID.x), i32(GlobalInvocationID.y));
+ let pixel_idx: i32 = pixel_position.y * output_dimension.x + pixel_position.x;
+
+ let pixel_center: vec2<f32> = vec2<f32>(pixel_position) + vec2f(0.5);
+ let uv: vec2<f32> = pixel_center / vec2f(output_dimension);
+ let ndc: vec2<f32> = uv * 2.0 - vec2f(1.0);
+
+ let noise = animated_blue_noise(pixel_position, u32(uniforms.frame_idx), u32(64));
+ var rnd_state = u32(0);
+ init_random(&rnd_state, u32(uniforms.frame_idx));
+ init_random(&rnd_state, u32(pixel_position.x));
+ init_random(&rnd_state, u32(pixel_position.y));
+
+ let jitter_scale: f32 = 1;
+ // Apply blue noise instead of uniformFloat
+ let jitter_x: f32 = (noise.x - 0.5) / f32(output_dimension.x) * jitter_scale;
+ let jitter_y: f32 = (noise.y - 0.5) / f32(output_dimension.y) * jitter_scale;
+
+ let n2 = (ndc.x + jitter_x);
+ let n3 = ndc.y + jitter_y;
+ let ray = generate_pinhole_camera_ray(vec2f(n2, n3), noise);
+
+ var accumulated_color: vec3<f32> = vec3<f32>(0.0);
+ let frame_weight: f32 = 1.0 / (uniforms.frame_idx + 1);
+ let samples_per_pixel: i32 = i32(uniforms.sample_count);
+ for (var i: i32 = 0; i < samples_per_pixel; i ++) {
+ var pixel_color: vec3<f32> = shade_hit(ray, rnd_state, noise);
+ var r = pixel_color.x;
+ var g = pixel_color.y;
+ var b = pixel_color.z;
+ // lazy NaN catching
+ if (r != r){ pixel_color.r = 0.0;};
+ if (g != g){ pixel_color.g = 0.0;};
+ if (b != b){ pixel_color.b = 0.0;};
+ accumulated_color += pixel_color;
+ }
+
+ accumulated_color = accumulated_color / f32(samples_per_pixel);
+ var prev_color: vec3<f32> = input_buffer[pixel_idx];
+ var final_output : vec3f = (prev_color * uniforms.frame_idx + accumulated_color) / (uniforms.frame_idx + 1.0);
+ input_buffer[pixel_idx] = final_output;
+ textureStore(output_buffer, pixel_position, vec4f(final_output, 1.0));
+}
+
+fn trace(ray: Ray) -> HitInfo {
+ var render_state: HitInfo;
+ render_state.hit = false;
+ var nearest_hit: f32 = 999.0;
+
+ // set up for bvh traversal
+ var node: Node = node_tree.nodes[0];
+ var stack: array<Node, 32>;
+ var stack_location: i32 = 0;
+
+ while true {
+ var primitive_count: u32 = u32(node.primitive_count);
+ var contents: u32 = u32(node.left_child);
+
+ if primitive_count == 0 {
+ var child1: Node = node_tree.nodes[contents];
+ var child2: Node = node_tree.nodes[contents + 1];
+
+ var distance1: f32 = hit_aabb(ray, child1);
+ var distance2: f32 = hit_aabb(ray, child2);
+
+ if distance1 > distance2 {
+ var temp_dist: f32 = distance1;
+ distance1 = distance2;
+ distance2 = temp_dist;
+
+ var temp_child: Node = child1;
+ child1 = child2;
+ child2 = temp_child;
+ }
+
+ if distance1 > nearest_hit {
+ if stack_location == 0 {
+ break;
+ } else {
+ stack_location -= 1;
+ node = stack[stack_location];
+ }
+ } else {
+ node = child1;
+ if distance1 < nearest_hit {
+ stack[stack_location] = child2;
+ stack_location += 1;
+ }
+ }
+ } else {
+ for (var i: u32 = 0; i < primitive_count; i++) {
+ var new_render_state: HitInfo = hit_triangle(
+ ray,
+ objects.triangles[u32(tri_lut.primitive_indices[i + contents])],
+ 0.001,
+ nearest_hit,
+ render_state,
+ );
+ if new_render_state.hit {
+ nearest_hit = new_render_state.dist;
+ render_state = new_render_state;
+ }
+ }
+ if stack_location == 0 {
+ break;
+ } else {
+ stack_location -= 1;
+ node = stack[stack_location];
+ }
+ }
+ }
+ return render_state;
+}
+
+fn shade_hit(ray: Ray, seed: u32, noise: vec2f) -> vec3<f32> {
+ var current_seed = seed;
+ var radiance = vec3f(0.0);
+ var throughput = vec3f(1.0);
+ var result: HitInfo;
+
+ var temp_ray = ray;
+ let bounces: u32 = u32(uniforms.max_depth);
+
+ var pdf: f32;
+ var env_pdf: f32;
+ var mis_weight : f32 = 1.0;
+
+ var sun_solid_angle = 2.0 * PI * (1.0 - cos(uniforms.sun_angular_size));
+ let sun_pdf = 1.0 / sun_solid_angle;
+ let sky_pdf = 1.0 / PI;
+
+ for (var bounce: u32 = 0; bounce < bounces; bounce++) {
+ result = trace(temp_ray);
+ if (!result.hit) {
+ // We hit the environment; skip the sun for now. Atleast till this rudimentry temporal accmulation exists.
+ // let to_sun = dot(temp_ray.direction, uniforms.sun_direction) > cos(uniforms.sun_angular_size);
+ // let sun_radiance = sun_glow(temp_ray.direction, uniforms.sun_direction);
+ // if (to_sun) {
+ // radianceOut += sun_radiance;
+ // }
+ // if (to_sun) {
+ // env_pdf_eval = 0.5 * sun_pdf;
+ // }
+ let viewZenith = abs(temp_ray.direction.z);
+ let extinction = exp(-2.0 * pow(1.0 - viewZenith, 3.0));
+ let skyRadiance = sky_glow(temp_ray.direction, uniforms.sun_direction) * extinction;
+ let radianceOut = skyRadiance;
+ if (bounce == 0) {
+ radiance += throughput * radianceOut;
+ break;
+ }
+ // bsdf generated ray carries the PDF forward to this bounce
+ var env_pdf_eval = 0.5 * sky_pdf;
+ let env_mis_weight = pdf / (pdf + env_pdf_eval);
+ radiance += clamp_hdr(throughput * radianceOut * env_mis_weight, 10.0);
+ break;
+ }
+
+ let rand = vec2f(uniform_float(¤t_seed), uniform_float(¤t_seed));
+ var material: Material = parse_textures(materials[result.material_idx], result);
+ if (material.emission.x > 0.0 || material.emission.y > 0.0 || material.emission.z > 0.0) {
+ radiance += throughput * material.emission;
+ // break;
+ }
+
+ // sun nee, mis weight based on prior bounce brdf
+ let env_dir = sample_sun_cone_dir(rand);
+ let env_color = sun_glow(env_dir, uniforms.sun_direction);
+ let env_pdf = sun_pdf;
+ let n_dot_env = dot(result.normal, env_dir);
+ if (n_dot_env > 0.0 && !is_occluded(result.position, result.geo_normal, env_dir, 99999.9)) {
+ let env_brdf = eval_brdf(result.normal, -temp_ray.direction, env_dir, material);
+ let diffuse_density = cosine_pdf(result.normal, env_dir);
+ let specular_density = ggx_pdf(-temp_ray.direction, result.normal, normalize(-temp_ray.direction + env_dir), material.roughness);
+ let bsdf_pdf = 0.5 * specular_density + 0.5 * diffuse_density;
+ let weight = env_pdf / (env_pdf + bsdf_pdf);
+ radiance += clamp_hdr(throughput * env_brdf * env_color * n_dot_env * weight / env_pdf, 10.0);
+ }
+
+ // TODO: Better selection, and also move this out.
+ // emissive nee, uniformly sample emissives
+ let light_index = min(u32(floor(rand.x * f32(uniforms.emissive_triangle_count))), u32(uniforms.emissive_triangle_count - 1.0));
+ let tri_index = emissiveTriangleIndices[light_index];
+ let tri = objects.triangles[i32(tri_index)];
+ // uniformly sample point on triangle
+ let u = 1.0 - rand.x;
+ let v = rand.x * (1.0 - rand.y);
+ let w = rand.x * rand.y;
+ let light_pos = u * tri.corner_a + v * tri.corner_b + w * tri.corner_c;
+ let light_normal = normalize(cross(tri.corner_b - tri.corner_a, tri.corner_c - tri.corner_a));
+ let to_light = light_pos - result.position;
+ let dist2 = dot(to_light, to_light);
+ let dist = sqrt(dist2);
+ let light_dir = to_light / dist;
+ let cos_surf = dot(result.normal, light_dir);
+ let cos_light = dot(light_normal, -light_dir);
+
+ if (cos_surf > 0.0 && cos_light > 0.0 && !is_occluded(result.position, result.geo_normal, light_dir, dist)) {
+ var mat = materials[i32(tri.material_idx)];
+ let direct_light_emissive_brdf = eval_brdf(result.normal, -temp_ray.direction, light_dir, material);
+ // compute area of the triangle
+ let edge1 = tri.corner_b - tri.corner_a;
+ let edge2 = tri.corner_c - tri.corner_a;
+ let area = 0.5 * length(cross(edge1, edge2));
+ let light_power = area * mat.emission;
+ // area to solid angle PDF conversion
+ let pdf_solid_angle = dist2 / ( area);
+
+ let diffuse_pdf = cosine_pdf(result.normal, light_dir);
+ let specular_pdf = ggx_pdf(-temp_ray.direction, result.normal, normalize(-temp_ray.direction + light_dir), material.roughness);
+ let bsdf_pdf = 0.5 * diffuse_pdf + 0.5 * specular_pdf;
+ let mis_weight = pdf_solid_angle / (pdf_solid_angle + bsdf_pdf);
+ let contrib = (throughput * direct_light_emissive_brdf * light_power * mis_weight) / pdf_solid_angle;
+ radiance += clamp_hdr(contrib, 10.0);
+ }
+
+ // rr
+ if (bounce > u32(2)) {
+ let rrProbability = min(0.9, luminance(throughput));
+ if (rrProbability < rand.y) {
+ break;
+ } else {
+ throughput /= rrProbability;
+ }
+ }
+
+ var view_dir = -temp_ray.direction;
+ var new_dir: vec3<f32>;
+ var specular_density: f32;
+ var diffuse_density: f32;
+
+ if (uniform_float(¤t_seed) < 0.5) {
+ new_dir = ggx_specular_sample(view_dir, result.normal, rand, material.roughness);
+ } else {
+ new_dir = cosine_hemisphere_sample(result.normal, vec2f(rand.y, rand.x));
+ }
+ let n_dot_l = dot(result.normal, new_dir);
+ if (n_dot_l <= 0.0) { break; }
+ specular_density = ggx_pdf(view_dir, result.normal, normalize(view_dir + new_dir), material.roughness);
+ diffuse_density = cosine_pdf(result.normal, normalize(new_dir));
+ pdf = 0.5 * specular_density + 0.5 * diffuse_density;
+
+ let indirect_brdf = eval_brdf(result.normal, view_dir, new_dir, material);
+ throughput *= (indirect_brdf * n_dot_l) / pdf;
+
+ temp_ray.origin = offset_ray(result.position, result.geo_normal);
+ temp_ray.direction = new_dir;
+ }
+
+ return radiance;
+}
+
+fn hit_triangle(ray: Ray, tri: Triangle, dist_min: f32, dist_max: f32, prevRay: HitInfo) -> HitInfo {
+ var hit: HitInfo;
+ hit.hit = false;
+
+ let edge1 = tri.corner_b - tri.corner_a;
+ let edge2 = tri.corner_c - tri.corner_a;
+
+ let pvec = cross(ray.direction, edge2);
+ let determinant = dot(edge1, pvec);
+
+ // reject nearly parallel rays.
+ if abs(determinant) < EPSILON {
+ return hit;
+ }
+
+ let inv_det = 1.0 / determinant;
+ let tvec = ray.origin - tri.corner_a;
+
+ // compute barycentric coordinate u.
+ let u = dot(tvec, pvec) * inv_det;
+ if (u < 0.0 || u > 1.0) {
+ return hit;
+ }
+
+ // compute barycentric coordinate v.
+ let qvec = cross(tvec, edge1);
+ let v = dot(ray.direction, qvec) * inv_det;
+ if (v < 0.0 || (u + v) > 1.0) {
+ return hit;
+ }
+
+ // calculate ray parameter (distance).
+ let dist = dot(edge2, qvec) * inv_det;
+ if (dist < dist_min || dist > dist_max) {
+ return hit;
+ }
+
+ // no early outs; valid hit
+ hit.hit = true;
+ hit.dist = dist;
+ hit.position = ray.origin + ray.direction * dist;
+ hit.tri = tri;
+ hit.material_idx = i32(tri.material_idx);
+
+ var geo_normal = normalize(cross(edge1, edge2));
+ var shading_normal = normalize((1.0 - u - v) * tri.normal_a + u * tri.normal_b + v * tri.normal_c);
+ let tangent = normalize((1.0 - u - v) * tri.tangent_a + u * tri.tangent_b + v * tri.tangent_c);
+
+ // shadow terminator fix: warp the hit position based on vertex normals
+ // normal aware EPSILON on hit position basically
+ let w = 1.0 - u - v;
+ let tmpu = hit.position - tri.corner_a;
+ let tmpv = hit.position - tri.corner_b;
+ let tmpw = hit.position - tri.corner_c;
+
+ let dotu = min(0.0, dot(tmpu, tri.normal_a));
+ let dotv = min(0.0, dot(tmpv, tri.normal_b));
+ let dotw = min(0.0, dot(tmpw, tri.normal_c));
+
+ let pu = tmpu - dotu * tri.normal_a;
+ let pv = tmpv - dotv * tri.normal_b;
+ let pw = tmpw - dotw * tri.normal_c;
+
+ let warped_offset = w * pu + u * pv + v * pw;
+ // Move the hit point slightly along the warped vector field
+ hit.position = hit.position + warped_offset;
+
+ // TBN
+ let T = normalize(tangent.xyz);
+ let N = normalize(shading_normal);
+ let B = normalize(cross(N, T)) * tangent.w;
+
+ hit.tangent = cross(B, N);
+ hit.normal = shading_normal;
+ hit.uv = (1.0 - u - v) * tri.uv_a + u * tri.uv_b + v * tri.uv_c;
+
+ // If a normal map is present, perturb the shading normal.
+ let material = materials[i32(tri.material_idx)];
+ if (material.normal_texture > -1.0) {
+ var normal_map = sample_material_texture(hit.uv, u32(material.normal_texture));
+ var normalized_map = normalize(normal_map * 2.0 - 1.0);
+ normalized_map.y = -normalized_map.y;
+ let world_normal = normalize(
+ normalized_map.x * T +
+ normalized_map.y * B +
+ normalized_map.z * N
+ );
+ hit.normal = world_normal;
+ }
+ var ray_dot_tri: f32 = dot(ray.direction, geo_normal);
+ if (ray_dot_tri > 0.0) {
+ hit.geo_normal = -hit.geo_normal;
+ hit.normal = -hit.normal;
+ }
+ return hit;
+}
+
+fn hit_aabb(ray: Ray, node: Node) -> f32 {
+ var reciprocal : vec3<f32> = vec3f(1.0) / ray.direction;
+ var t_near: vec3<f32> = (node.min_corner - ray.origin) * reciprocal;
+ var t_far: vec3<f32> = (node.max_corner - ray.origin) * reciprocal;
+ var t_min: vec3<f32> = min(t_near, t_far);
+ var t_max: vec3<f32> = max(t_near, t_far);
+
+ var min_intersection: f32 = max(max(t_min.x, t_min.y), t_min.z); // t0
+ var max_intersection: f32 = min(min(t_max.x, t_max.y), t_max.z); // t1
+
+ var mask: f32 = step(max_intersection, min_intersection) + step(max_intersection, 0.0);
+ if min_intersection > max_intersection || max_intersection < 0 {
+ return 9999.0;
+ } else {
+ return min_intersection;
+ }
+}<
\ No newline at end of fileADD · src/shaders/random.wgsl +34 -0--- a/src/shaders/random.wgsl
+++ b/src/shaders/random.wgsl
@@ -0,0 +1,34 @@
+fn init_random(state: ptr<function, u32>, value: u32) {
+ * state ^= value;
+ * state = pcg(*state);
+}
+
+fn pcg(n: u32) -> u32 {
+ var h = n * 747796405u + 2891336453u;
+ h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u;
+ return (h >> 22u) ^ h;
+}
+
+fn uniform_uint(state: ptr<function, u32>, max: u32) -> u32 {
+ * state = pcg(*state);
+ return * state % max;
+}
+
+fn uniform_float(state: ptr<function, u32>) -> f32 {
+ * state = pcg(*state);
+ return f32(*state) / 4294967295.0;
+}
+
+fn animated_blue_noise(coord: vec2<i32>, frame_count: u32, frame_count_cycle: u32) -> vec2f {
+ // spatial
+ let tex_size = vec2<u32>(textureDimensions(blueNoiseTexture).xy);
+ 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));
+ let blue_noise = textureLoad(blueNoiseTexture, wrapped_coord).xy;
+ let idx = (f32(wrapped_coord.y) % blue_noise.y) * blue_noise.x + (f32(wrapped_coord.x) % blue_noise.x);
+ // temporal
+ let n = frame_count % frame_count_cycle;
+ let a1 = 0.7548776662466927f;
+ let a2 = 0.5698402909980532f;
+ let r2_seq = fract(vec2(a1 * f32(n), a2 * f32(n)));
+ return fract(blue_noise + r2_seq);
+}ADD · src/shaders/sky.wgsl +67 -0--- a/src/shaders/sky.wgsl
+++ b/src/shaders/sky.wgsl
@@ -0,0 +1,67 @@
+const sun_angular_size = 1.0 * (PI / 180.0);
+
+fn dir_in_cone(u: vec2f) -> vec3<f32> {
+ let sun_cos_theta_max = cos(0.255f * PI / 180f);
+ let cos_theta = 1f - u.x * (1f - sun_cos_theta_max);
+ let sin_theta = sqrt(1f - cos_theta * cos_theta);
+ let phi = 2f * PI * u.y;
+ let x = cos(phi) * sin_theta;
+ let y = sin(phi) * sin_theta;
+ let z = cos_theta;
+ return vec3(x, y, z);
+}
+
+fn sample_sun_cone_dir(u: vec2f) -> vec3<f32> {
+ let v = dir_in_cone(u);
+ let onb = pixar_onb(normalize(uniforms.sun_direction));
+ return normalize(onb * v);
+}
+
+// https://www.jcgt.org/published/0006/01/01/paper-lowres.pdf
+fn pixar_onb(n: vec3f) -> mat3x3<f32> {
+ let s = select(- 1f, 1f, n.z >= 0f);
+ let a = - 1f / (s + n.z);
+ let b = n.x * n.y * a;
+ let u = vec3(1f + s * n.x * n.x * a, s * b, - s * n.x);
+ let v = vec3(b, s + n.y * n.y * a, - n.y);
+ return mat3x3(u, v, n);
+}
+
+// simplified approximation of preetham
+fn sky_glow(dir: vec3f, sun_dir: vec3f) -> vec3f {
+ let view_dir = normalize(dir);
+ let sun_dir_n = normalize(sun_dir);
+ let cos_theta = dot(view_dir, sun_dir_n);
+ if sun_dir_n.z <= 0.0 {
+ return vec3f(0.0);
+ }
+ // sun altitude still helps modulate overall warmth
+ let sun_altitude = clamp(sun_dir_n.z, 0.0, 1.0);
+ // more saturated warm tone for horizon, and deeper blue for zenith
+ let horizon_color = vec3f(1.1, 0.4, 0.2);
+ // rich orange
+ let zenith_color = vec3f(0.05, 0.2, 0.6);
+ // deep blue
+ // exaggerated curve to preserve saturation
+ let sky_color = mix(horizon_color, zenith_color, pow(sun_altitude, 0.1));
+ // rayleigh-like gradient with vertical bias
+ let rayleigh = sky_color * (0.6 + 0.4 * cos_theta * cos_theta);
+ // warm sun glow with stronger color (no gray falloff)
+ let mie = vec3f(1.3, 0.6, 0.3) * pow(max(cos_theta, 0.0), 12.0) * 0.6;
+ return clamp(rayleigh + mie, vec3f(0.0), vec3f(100.0));
+}
+
+fn sun_glow(dir: vec3f, sun_dir: vec3f) -> vec3f {
+ let view_dir = normalize(dir);
+ let sun_n = normalize(sun_dir);
+ let cos_theta = dot(view_dir, sun_n);
+ // angular radius (half the angular size)
+ let angular_radius = 0.5 * uniforms.sun_angular_size;
+ let inner = cos(angular_radius * 0.9);
+ let outer = cos(angular_radius * 1.1);
+ let sun_disk = smoothstep(outer, inner, max(cos_theta, 0.0));
+ // compute sun altitude (z-up): 0 = horizon, 1 = overhead
+ let sun_altitude = clamp(sun_n.z, 0.0, 1.0);
+ return uniforms.sun_radiance * /*tint*/
+ sun_disk;
+}<
\ No newline at end of fileADD · src/shaders/types.d.ts +4 -0--- a/src/shaders/types.d.ts
+++ b/src/shaders/types.d.ts
@@ -0,0 +1,4 @@
+declare module "*.wgsl" {
+ const shader: "string";
+ export default shader;
+}ADD · src/shaders/utils.wgsl +37 -0--- a/src/shaders/utils.wgsl
+++ b/src/shaders/utils.wgsl
@@ -0,0 +1,37 @@
+fn is_nan(x: f32) -> bool {
+ return x != x;
+}
+
+fn is_inf(x: f32) -> bool {
+ return abs(x) > 1e20;
+}
+
+fn is_nan3(v: vec3<f32>) -> bool {
+ return is_nan(v.x) || is_nan(v.y) || is_nan(v.z);
+}
+
+fn is_inf3(v: vec3<f32>) -> bool {
+ return abs(v.x) > 1e20 || abs(v.y) > 1e20 || abs(v.z) > 1e20;
+}
+
+fn luminance(color: vec3<f32>) -> f32 {
+ return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
+}
+
+fn float_to_int(f: f32) -> i32 {
+ return bitcast<i32>(f);
+}
+
+fn int_to_float(i: i32) -> f32 {
+ return bitcast<f32>(i);
+}
+
+fn srgb_to_linear(rgb: vec3<f32>) -> vec3<f32> {
+ return select(pow((rgb + 0.055) * (1.0 / 1.055), vec3<f32>(2.4)), rgb * (1.0 / 12.92), rgb <= vec3<f32>(0.04045));
+}
+
+fn clamp_hdr(color: vec3<f32>, max_luminance: f32) -> vec3<f32> {
+ let lum = dot(color, vec3f(0.2126, 0.7152, 0.0722));
+ return color * min(1.0, max_luminance / max(lum, 1e-6));
+}
+ADD · src/shaders/viewport.wgsl +68 -0--- a/src/shaders/viewport.wgsl
+++ b/src/shaders/viewport.wgsl
@@ -0,0 +1,68 @@
+#include utils.wgsl
+
+@group(0) @binding(0) var output_buffer: texture_2d<f32>;
+
+struct Interpolator {
+ @builtin(position) position: vec4<f32>,
+ @location(0) tex_coord: vec2<f32>,
+}
+
+
+fn uncharted2_tonemap_base(x : vec3f) -> vec3f
+{
+ let a = 0.15;
+ let b = 0.50;
+ let c = 0.10;
+ let d = 0.20;
+ let e = 0.02;
+ let f = 0.30;
+ return ((x*(a*x+c*b)+d*e)/(x*(a*x+b)+d*f))-e/f;
+}
+
+fn uncharted2_tonemap(color: vec3f) -> vec3f {
+ let exposure = 2.0; // adjustable
+ let white_point = uncharted2_tonemap_base(vec3f(11.2));
+ let mapped = uncharted2_tonemap_base(color * exposure);
+ return mapped / white_point;
+}
+
+fn gamma_correct(color: vec3<f32>) -> vec3<f32> {
+ return pow(color, vec3<f32>(1.0 / 2.2));
+}
+
+// fn reinhard_tonemap(color: vec3<f32>) -> vec3<f32> {
+// return color / (color + vec3f(1.0));
+// }
+
+
+@vertex
+fn vert_main(@builtin(vertex_index) vertex_index: u32) -> Interpolator {
+ var positions = array<vec2<f32>, 6>(
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(1.0, -1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>(-1.0, 1.0)
+ );
+ var tex_coords = array<vec2<f32>, 6>(
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(1.0, 1.0),
+ vec2<f32>(0.0, 0.0),
+ vec2<f32>(0.0, 1.0)
+ );
+ var output: Interpolator;
+ output.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
+ output.tex_coord = tex_coords[vertex_index];
+ return output;
+}
+
+@fragment
+fn frag_main(@location(0) tex_coord: vec2<f32>) -> @location(0) vec4<f32> {
+ let dims = textureDimensions(output_buffer);
+ let uv = vec2<u32>(tex_coord * vec2<f32>(dims));
+ let linear_color = textureLoad(output_buffer, uv, 0).rgb;
+ return vec4f(gamma_correct(uncharted2_tonemap(linear_color)), 1.0);
+}ADD · src/vite-env.d.ts +2 -0--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+/// <reference types="vite/client" />
+/// <reference types="@webgpu/types" /><
\ No newline at end of fileADD · tsconfig.json +25 -0--- a/tsconfig.json
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+ "types": ["@webgpu/types", "vite-plugin-glsl/ext"]
+ },
+ "include": ["src"]
+}ADD · vite.config.ts +15 -0--- a/vite.config.ts
+++ b/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import glsl from 'vite-plugin-glsl';
+
+
+export default defineConfig({
+ plugins: [glsl()],
+ build: {
+ target: "es2022",
+ modulePreload: true,
+ outDir: "dist",
+ },
+ esbuild: {
+ target: "es2022",
+ },
+});