diff --git a/example/furnace_test.html b/example/furnace_test.html
new file mode 100644
index 00000000..05ed8ffc
--- /dev/null
+++ b/example/furnace_test.html
@@ -0,0 +1,17 @@
+
+
+ Furnace Test
+
+
+
+
+
+
+
+
+
diff --git a/example/furnace_test.js b/example/furnace_test.js
new file mode 100644
index 00000000..dcb81091
--- /dev/null
+++ b/example/furnace_test.js
@@ -0,0 +1,157 @@
+import { Scene, SphereGeometry, MeshStandardMaterial, Mesh, PerspectiveCamera, WebGPURenderer } from 'three/webgpu';
+import { WebGLRenderer } from 'three';
+import { GradientEquirectTexture } from 'three-gpu-pathtracer';
+import { WebGPUPathTracer } from 'three-gpu-pathtracer/webgpu';
+import { WebGLPathTracer } from 'three-gpu-pathtracer';
+import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
+
+const options = {
+ enable: true,
+ useMegakernel: true,
+ isWebGPU: true,
+};
+
+// init scene
+const scene = new Scene();
+
+// build an 11x11 grid of spheres:
+// roughness increases left to right
+// metalness increases top to bottom
+const sphereGeom = new SphereGeometry( 0.4, 100, 50 );
+for ( let x = 0; x <= 10; x ++ ) {
+
+ for ( let y = 0; y <= 10; y ++ ) {
+
+ const mesh = new Mesh(
+ sphereGeom,
+ new MeshStandardMaterial( {
+ color: 0xffffff,
+ roughness: x / 10,
+ metalness: y / 10,
+ } )
+ );
+
+ mesh.position.x = x - 5;
+ mesh.position.y = 5 - y;
+ scene.add( mesh );
+
+ }
+
+}
+
+const texture = new GradientEquirectTexture();
+texture.topColor.set( 0xcccccc );
+texture.bottomColor.set( 0xcccccc );
+texture.update();
+
+scene.environment = texture;
+scene.background = texture;
+
+const camera = new PerspectiveCamera( 40, 1, 1, 100 );
+camera.position.set( 0, 0, 18 );
+
+let renderer;
+let pathTracer;
+
+function createRendererAndPathTracer() {
+
+ if ( renderer ) {
+
+ renderer.dispose();
+ pathTracer.dispose();
+ document.body.removeChild( renderer.domElement );
+
+ }
+
+ if ( options.isWebGPU ) {
+
+ renderer = new WebGPURenderer( { antialias: true, trackTimestamp: false } );
+ renderer.init();
+ pathTracer = new WebGPUPathTracer( renderer );
+ pathTracer.useMegakernel( options.useMegakernel );
+
+ } else {
+
+ renderer = new WebGLRenderer( { antialias: true } );
+ pathTracer = new WebGLPathTracer( renderer );
+
+ }
+
+ document.body.appendChild( renderer.domElement );
+ renderer.setSize( innerWidth, innerHeight );
+ renderer.setPixelRatio( devicePixelRatio );
+ renderer.setAnimationLoop( animate );
+ pathTracer.setScene( scene, camera );
+ pathTracer.reset();
+
+}
+
+createRendererAndPathTracer();
+
+const gui = new GUI();
+gui.add( options, 'enable' );
+const megakernelController = gui.add( options, 'useMegakernel' ).onChange( () => {
+
+ if ( options.isWebGPU ) {
+
+ pathTracer.useMegakernel( options.useMegakernel );
+
+ }
+
+ pathTracer.setScene( scene, camera );
+ pathTracer.reset();
+
+} );
+gui.add( options, 'isWebGPU' ).onChange( () => {
+
+ createRendererAndPathTracer();
+
+ if ( ! options.isWebGPU ) {
+
+ megakernelController.hide();
+
+ } else {
+
+ megakernelController.show();
+
+ }
+
+} );
+
+onResize();
+
+window.addEventListener( 'resize', onResize );
+
+function animate() {
+
+ if ( options.enable ) {
+
+ if ( ! pathTracer.dynamicLowRes && pathTracer.fadeState !== 1 ) {
+
+ renderer.render( scene, camera );
+
+ }
+
+ pathTracer.renderSample();
+
+ } else {
+
+ renderer.render( scene, camera );
+
+ }
+
+}
+
+function onResize() {
+
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+
+ renderer.setSize( w, h );
+ renderer.setPixelRatio( window.devicePixelRatio );
+
+ const aspect = w / h;
+ camera.aspect = aspect;
+ camera.updateProjectionMatrix();
+
+}
diff --git a/package-lock.json b/package-lock.json
index fb65dbdb..b811e9ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1368,6 +1368,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1743,7 +1744,6 @@
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"optional": true,
- "peer": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
@@ -1854,6 +1854,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -2688,35 +2689,6 @@
"immediate": "~3.0.5"
}
},
- "node_modules/lightningcss": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.1.tgz",
- "integrity": "sha512-kUpHOLiH5GB0ERSv4pxqlL0RYKnOXtgGtVe7shDGfhS0AZ4D1ouKFYAcLcZhql8aMspDNzaUCumGHZ78tb2fTg==",
- "dev": true,
- "optional": true,
- "peer": true,
- "dependencies": {
- "detect-libc": "^1.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-darwin-arm64": "1.24.1",
- "lightningcss-darwin-x64": "1.24.1",
- "lightningcss-freebsd-x64": "1.24.1",
- "lightningcss-linux-arm-gnueabihf": "1.24.1",
- "lightningcss-linux-arm64-gnu": "1.24.1",
- "lightningcss-linux-arm64-musl": "1.24.1",
- "lightningcss-linux-x64-gnu": "1.24.1",
- "lightningcss-linux-x64-musl": "1.24.1",
- "lightningcss-win32-x64-msvc": "1.24.1"
- }
- },
"node_modules/lightningcss-darwin-arm64": {
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.1.tgz",
@@ -2729,7 +2701,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2750,7 +2721,6 @@
"os": [
"darwin"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2771,7 +2741,6 @@
"os": [
"freebsd"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2792,7 +2761,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2813,7 +2781,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2834,7 +2801,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2855,7 +2821,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2876,7 +2841,6 @@
"os": [
"linux"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -2897,7 +2861,6 @@
"os": [
"win32"
],
- "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -3654,7 +3617,8 @@
"resolved": "https://registry.npmjs.org/three/-/three-0.183.1.tgz",
"integrity": "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/three-mesh-bvh": {
"version": "0.9.9",
@@ -3713,6 +3677,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -3784,6 +3749,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3924,6 +3890,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -4865,7 +4832,8 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"acorn-jsx": {
"version": "5.3.2",
@@ -5135,12 +5103,10 @@
"dev": true
},
"detect-libc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "version": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"devtools-protocol": {
"version": "0.0.1011705",
@@ -5223,6 +5189,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
"integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
"dev": true,
+ "peer": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -5829,97 +5796,59 @@
"immediate": "~3.0.5"
}
},
- "lightningcss": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.1.tgz",
- "integrity": "sha512-kUpHOLiH5GB0ERSv4pxqlL0RYKnOXtgGtVe7shDGfhS0AZ4D1ouKFYAcLcZhql8aMspDNzaUCumGHZ78tb2fTg==",
- "dev": true,
- "optional": true,
- "peer": true,
- "requires": {
- "detect-libc": "^1.0.3",
- "lightningcss-darwin-arm64": "1.24.1",
- "lightningcss-darwin-x64": "1.24.1",
- "lightningcss-freebsd-x64": "1.24.1",
- "lightningcss-linux-arm-gnueabihf": "1.24.1",
- "lightningcss-linux-arm64-gnu": "1.24.1",
- "lightningcss-linux-arm64-musl": "1.24.1",
- "lightningcss-linux-x64-gnu": "1.24.1",
- "lightningcss-linux-x64-musl": "1.24.1",
- "lightningcss-win32-x64-msvc": "1.24.1"
- }
- },
"lightningcss-darwin-arm64": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.24.1.tgz",
"integrity": "sha512-1jQ12jBy+AE/73uGQWGSafK5GoWgmSiIQOGhSEXiFJSZxzV+OXIx+a9h2EYHxdJfX864M+2TAxWPWb0Vv+8y4w==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-darwin-x64": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.24.1.tgz",
"integrity": "sha512-R4R1d7VVdq2mG4igMU+Di8GPf0b64ZLnYVkubYnGG0Qxq1KaXQtAzcLI43EkpnoWvB/kUg8JKCWH4S13NfiLcQ==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-freebsd-x64": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.24.1.tgz",
"integrity": "sha512-z6NberUUw5ALES6Ixn2shmjRRrM1cmEn1ZQPiM5IrZ6xHHL5a1lPin9pRv+w6eWfcrEo+qGG6R9XfJrpuY3e4g==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-linux-arm-gnueabihf": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.24.1.tgz",
"integrity": "sha512-NLQLnBQW/0sSg74qLNI8F8QKQXkNg4/ukSTa+XhtkO7v3BnK19TS1MfCbDHt+TTdSgNEBv0tubRuapcKho2EWw==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-linux-arm64-gnu": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.24.1.tgz",
"integrity": "sha512-AQxWU8c9E9JAjAi4Qw9CvX2tDIPjgzCTrZCSXKELfs4mCwzxRkHh2RCxX8sFK19RyJoJAjA/Kw8+LMNRHS5qEg==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-linux-arm64-musl": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.24.1.tgz",
"integrity": "sha512-JCgH/SrNrhqsguUA0uJUM1PvN5+dVuzPIlXcoWDHSv2OU/BWlj2dUYr3XNzEw748SmNZPfl2NjQrAdzaPOn1lA==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-linux-x64-gnu": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.24.1.tgz",
"integrity": "sha512-TYdEsC63bHV0h47aNRGN3RiK7aIeco3/keN4NkoSQ5T8xk09KHuBdySltWAvKLgT8JvR+ayzq8ZHnL1wKWY0rw==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-linux-x64-musl": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.24.1.tgz",
"integrity": "sha512-HLfzVik3RToot6pQ2Rgc3JhfZkGi01hFetHt40HrUMoeKitLoqUUT5owM6yTZPTytTUW9ukLBJ1pc3XNMSvlLw==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"lightningcss-win32-x64-msvc": {
- "version": "1.24.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.1.tgz",
+ "version": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.24.1.tgz",
"integrity": "sha512-joEupPjYJ7PjZtDsS5lzALtlAudAbgIBMGJPNeFe5HfdmJXFd13ECmEM+5rXNxYVMRHua2w8132R6ab5Z6K9Ow==",
"dev": true,
- "optional": true,
- "peer": true
+ "optional": true
},
"locate-path": {
"version": "5.0.0",
@@ -6429,7 +6358,8 @@
"version": "0.183.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.1.tgz",
"integrity": "sha512-Psv6bbd3d/M/01MT2zZ+VmD0Vj2dbWTNhfe4CuSg7w5TuW96M3NOyCVuh9SZQ05CpGmD7NEcJhZw4GVjhCYxfQ==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"three-mesh-bvh": {
"version": "0.9.9",
@@ -6465,7 +6395,8 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true
+ "dev": true,
+ "peer": true
}
}
},
@@ -6510,7 +6441,8 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"unbzip2-stream": {
"version": "1.4.3",
@@ -6569,7 +6501,8 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"rollup": {
"version": "4.53.2",
diff --git a/src/webgpu/MegaKernelPathTracer.js b/src/webgpu/MegaKernelPathTracer.js
index b9ffe971..414589e4 100644
--- a/src/webgpu/MegaKernelPathTracer.js
+++ b/src/webgpu/MegaKernelPathTracer.js
@@ -14,7 +14,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
this.envInfo = new EquirectHdrInfoUniform();
// kernels
- this.kernel = new PathTracerMegaKernel().setWorkgroupSize( 8, 8, 1 );
+ this.kernel = new PathTracerMegaKernel( this.material ).setWorkgroupSize( 8, 8, 1 );
}
@@ -33,6 +33,14 @@ export class MegaKernelPathTracer extends PathTracerBackend {
}
+ setMaterial( material ) {
+
+ this.material = material;
+ this.kernel = new PathTracerMegaKernel( this.material ).setWorkgroupSize( 8, 8, 1 );
+ this.reset();
+
+ }
+
setEnvironment(
envMap,
envMapIntensity,
diff --git a/src/webgpu/PathTracerBackend.js b/src/webgpu/PathTracerBackend.js
index a7c8234e..f349cd36 100644
--- a/src/webgpu/PathTracerBackend.js
+++ b/src/webgpu/PathTracerBackend.js
@@ -1,6 +1,7 @@
import { ColorManagement, FloatType, LinearFilter, RGBAFormat } from 'three';
import { RedIntegerFormat, StorageTexture, UnsignedIntType } from 'three/webgpu';
import { ZeroOutKernel } from './compute/ZeroOutKernel.js';
+import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js';
export class PathTracerBackend {
@@ -39,6 +40,8 @@ export class PathTracerBackend {
this.sampleCountClearKernel = new ZeroOutKernel( { textureType: 'r32uint' } ).setWorkgroupSize( 8, 8, 1 );
this.outputTargetClearKernel = new ZeroOutKernel( { textureType: 'rgba32float' } ).setWorkgroupSize( 8, 8, 1 );
+ this.material = new GltfCompliantMaterial();
+
}
setCamera( camera ) {
@@ -56,6 +59,10 @@ export class PathTracerBackend {
}
+ setMaterial( material ) {
+
+ }
+
setEnvironment(
envMap,
envMapIntensity,
@@ -117,6 +124,13 @@ export class PathTracerBackend {
}
+ if ( ! this.material.initialized ) {
+
+ this.material.init( renderer );
+ this.material.initialized = true;
+
+ }
+
if ( ! this._renderTask ) {
this._renderTask = this.createRenderTask();
diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js
index baa7b0e6..2f4a3185 100644
--- a/src/webgpu/WaveFrontPathTracer.js
+++ b/src/webgpu/WaveFrontPathTracer.js
@@ -27,12 +27,12 @@ export class WaveFrontPathTracer extends PathTracerBackend {
this.envInfo = new EquirectHdrInfoUniform();
// queues
- this.rayQueue = new IndirectStorageBufferAttribute( MAX_RAY_COUNT, 16 );
+ this.rayQueue = new IndirectStorageBufferAttribute( MAX_RAY_COUNT, queuedRayStruct.getLength() );
this.rayQueue.name = 'Ray Queue';
this.rayQueueSize = new IndirectStorageBufferAttribute( 2, 1 );
this.rayQueueSize.name = 'Ray Queue Size';
- this.hitQueue = new IndirectStorageBufferAttribute( MAX_HIT_COUNT, 16 );
+ this.hitQueue = new IndirectStorageBufferAttribute( MAX_HIT_COUNT, queuedHitStruct.getLength() );
this.hitQueue.name = 'Hit Queue';
this.hitQueueSize = new IndirectStorageBufferAttribute( 2, 1 );
this.hitQueueSize.name = 'Hit Queue Size';
@@ -47,7 +47,7 @@ export class WaveFrontPathTracer extends PathTracerBackend {
this.enqueueRaysKernel = new RayGenerationKernel().setWorkgroupSize( 8, 8, 1 );
this.rayIntersectionKernel = new RayIntersectionKernel().setWorkgroupSize( 64, 1, 1 );
this.updateRayQueueParamsKernel = new UpdateRayQueueParamsKernel().setWorkgroupSize( 1, 1, 1 );
- this.hitProcessKernel = new ProcessHitsKernel().setWorkgroupSize( 64, 1, 1 );
+ this.hitProcessKernel = new ProcessHitsKernel( this.material ).setWorkgroupSize( 64, 1, 1 );
// clear kernels
this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 );
@@ -77,6 +77,14 @@ export class WaveFrontPathTracer extends PathTracerBackend {
}
+ setMaterial( material ) {
+
+ this.material = material;
+ this.hitProcessKernel = new ProcessHitsKernel( this.material ).setWorkgroupSize( 64, 1, 1 );
+ this.reset();
+
+ }
+
setEnvironment(
envMap,
envMapIntensity,
diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js
index 8802a294..453cb93e 100644
--- a/src/webgpu/WebGPUPathTracer.js
+++ b/src/webgpu/WebGPUPathTracer.js
@@ -136,6 +136,12 @@ export class WebGPUPathTracer {
}
+ setMaterial( material ) {
+
+ this._pathTracer.setMaterial( material );
+
+ }
+
// TODO: support async generation of ObjectBVH
setSceneAsync( ...args ) {
@@ -339,12 +345,23 @@ export class WebGPUPathTracer {
const buffer = await renderer.readRenderTargetPixelsAsync( targetStub, 0, 0, width, height );
const uintBuffer = new Uint32Array( buffer.buffer );
+ // copyTexture requires a multiple of 256 bytes for texelsPerRow
+ // Hence a multiple of 64 u32 per row
+ const texelsPerRow = Math.ceil( width / 64 ) * 64;
+
// Sum up all sample counts and divide by pixel count to get average samples per pixel
let totalSamples = 0;
let minSamples = Number.MAX_VALUE;
let maxSamples = - Number.MAX_VALUE;
for ( let i = 0, l = uintBuffer.length; i < l; i ++ ) {
+ // Skip padding
+ if ( i % texelsPerRow >= width ) {
+
+ continue;
+
+ }
+
// Each entry contains sample count in lower bits and active flag in high bit
// Mask out the active flag (0xF0000000) to get just the sample count
const samples = uintBuffer[ i ] & 0x0FFFFFFF;
diff --git a/src/webgpu/compute/PathTracerMegaKernel.js b/src/webgpu/compute/PathTracerMegaKernel.js
index 74a29043..9e52fbdb 100644
--- a/src/webgpu/compute/PathTracerMegaKernel.js
+++ b/src/webgpu/compute/PathTracerMegaKernel.js
@@ -3,14 +3,14 @@ import { ndcToCameraRay } from '../lib/wgsl/common.wgsl.js';
import { ComputeKernel } from './ComputeKernel.js';
import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl';
import { pcgRand2, pcgRand3, pcgInit } from '../nodes/random.wgsl.js';
-import { getSurfaceRecordFunc, lambertBsdfFunc } from '../nodes/material.wgsl.js';
+import { getSurfaceRecordFunc } from '../nodes/material.wgsl.js';
import { sampleEnvironmentFn, weightedAlphaBlendFn } from '../nodes/sampling.wgsl.js';
import { proxy } from '../lib/nodes/NodeProxy.js';
import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js';
export class PathTracerMegaKernel extends ComputeKernel {
- constructor() {
+ constructor( material ) {
const parameters = {
bvhData: { value: null },
@@ -140,9 +140,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
let surface = getSurfaceRecord( material, vertexData, hitResult.side, hitResult.normal, textures, textureSampler );
- let scatterRec = bsdfSample( - ray.direction, surface );
+ let scatterRec = ${ material.getBsdfNode() }( - ray.direction, surface );
+
+ if ( scatterRec.pdf <= 0.0 || any( scatterRec.color != scatterRec.color ) ) {
+
+ return;
+
+ }
- // white diffuse surface
throughputColor *= scatterRec.color / scatterRec.pdf;
ray.origin = vertexData.position.xyz;
@@ -166,6 +171,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
}
+ resultColor = clamp( resultColor, vec4( 0.0 ), vec4( 4.0 ) );
let sampleCount = textureLoad( ${ parameters.sampleCountTarget }, indexUV ).r + 1;
let prevColor = textureLoad( ${ parameters.prevOutputTarget }, indexUV );
let blendedColor = weightedAlphaBlend( prevColor, resultColor, 1.0 / f32( sampleCount ) );
@@ -180,7 +186,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
proxy( 'bvhData.value.structs.transform', parameters ),
proxy( 'bvhData.value.fns.raycastFirstHit', parameters ),
proxy( 'bvhData.value.fns.sampleTrianglePoint', parameters ),
- ndcToCameraRay, pcgRand2, pcgRand3, pcgInit, lambertBsdfFunc,
+ ndcToCameraRay, pcgRand2, pcgRand3, pcgInit,
sampleEnvironmentFn, getSurfaceRecordFunc, weightedAlphaBlendFn,
] }`;
diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js
index 61feaa06..cd7e7a8a 100644
--- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js
+++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js
@@ -1,15 +1,16 @@
import { IndirectStorageBufferAttribute, StorageTexture, DataTexture } from 'three/webgpu';
import { ComputeKernel } from '../ComputeKernel.js';
-import { uniform, storage, wgslFn, textureStore, globalId, texture, sampler } from 'three/tsl';
+import { uniform, storage, textureStore, globalId, texture, sampler } from 'three/tsl';
import { pcgRand3, pcgInit } from '../../nodes/random.wgsl.js';
-import { getSurfaceRecordFunc, lambertBsdfFunc } from '../../nodes/material.wgsl.js';
+import { getSurfaceRecordFunc } from '../../nodes/material.wgsl.js';
import { queuedRayStruct, queuedHitStruct } from './structs.js';
import { proxy } from '../../lib/nodes/NodeProxy.js';
import { weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js';
+import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js';
export class ProcessHitsKernel extends ComputeKernel {
- constructor() {
+ constructor( material ) {
const parameters = {
bvhData: { value: null },
@@ -29,13 +30,13 @@ export class ProcessHitsKernel extends ComputeKernel {
hitQueue: storage( new IndirectStorageBufferAttribute( 1, queuedHitStruct.getLength() ), queuedHitStruct ),
hitQueueSize: storage( new IndirectStorageBufferAttribute( 2, 1 ), 'u32' ),
- textures: texture( new DataTexture() ),
- textureSampler: sampler( new DataTexture() ),
+ textures: texture( new DataTexture( ) ),
+ textureSampler: sampler( new DataTexture( ) ),
globalId: globalId,
};
- const fn = wgslFn( /* wgsl */`
+ const fn = wgslTagFn/* wgsl */`
fn compute(
// indices and target
@@ -55,6 +56,7 @@ export class ProcessHitsKernel extends ComputeKernel {
hitQueue: ptr, read_write>,
hitQueueSize: ptr, read_write>,
+ // texture array for material textures
textures: texture_2d_array,
textureSampler: sampler,
@@ -90,7 +92,18 @@ export class ProcessHitsKernel extends ComputeKernel {
let surface = getSurfaceRecord( material, vertexData, input.side, input.normal, textures, textureSampler );
- let scatterRec = bsdfSample( input.view, surface );
+ let scatterRec = ${ material.getBsdfNode() }( input.view, surface );
+
+ // terminate ray if scatter is impossible or color is nan
+ // TODO: Investigate ways to not generate such scatters
+ if ( scatterRec.pdf <= 0 || any( scatterRec.color != scatterRec.color ) ) {
+
+ let sampleCount = ( textureLoad( sampleCountTarget, indexUV ).r & ( ~ ACTIVE_FLAG ) );
+ textureStore( sampleCountTarget, indexUV, vec4( sampleCount ) );
+
+ return;
+
+ }
if ( input.currentBounce >= bounces ) {
@@ -115,16 +128,17 @@ export class ProcessHitsKernel extends ComputeKernel {
}
}
- `, [
- proxy( 'bvhData.value.structs.material', parameters ),
- proxy( 'bvhData.value.structs.transform', parameters ),
- proxy( 'bvhData.value.storage.materials', parameters ),
- proxy( 'bvhData.value.storage.transforms', parameters ),
- proxy( 'bvhData.value.fns.sampleTrianglePoint', parameters ),
- queuedRayStruct, lambertBsdfFunc, getSurfaceRecordFunc,
- pcgRand3, pcgInit, queuedHitStruct,
- weightedAlphaBlendFn,
- ] );
+
+ ${ [
+ proxy( 'bvhData.value.structs.material', parameters ),
+ proxy( 'bvhData.value.structs.transform', parameters ),
+ proxy( 'bvhData.value.storage.materials', parameters ),
+ proxy( 'bvhData.value.storage.transforms', parameters ),
+ proxy( 'bvhData.value.fns.sampleTrianglePoint', parameters ),
+ queuedRayStruct, getSurfaceRecordFunc,
+ pcgRand3, pcgInit, queuedHitStruct,
+ weightedAlphaBlendFn,
+ ] }`;
super( fn( parameters ) );
diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js
index 37045fb4..e8a3dd37 100644
--- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js
+++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js
@@ -131,6 +131,7 @@ export class RayIntersectionKernel extends ComputeKernel {
resultColor = sampleEnvironment( background, backgroundSampler, backgroundInfo, input.direction, pcgRand2() );
}
+ resultColor = clamp( resultColor, vec4( 0.0 ), vec4( 4.0 ) );
let sampleCount = ( textureLoad( sampleCountTarget, indexUV ).r & ( ~ ACTIVE_FLAG ) ) + 1;
let prevColor = textureLoad( prevOutputTarget, indexUV );
diff --git a/src/webgpu/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js
new file mode 100644
index 00000000..19d2946b
--- /dev/null
+++ b/src/webgpu/materials/GltfCompliantMaterial.js
@@ -0,0 +1,161 @@
+import { wgslFn, texture, sampler, textureStore, globalId } from 'three/tsl';
+import { StorageTexture, RedFormat, LinearFilter, FloatType } from 'three/webgpu';
+import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode';
+import { PathtracingMaterial } from './PathtracingMaterial';
+import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc, albedoIntegralUniform, albedoIntegralMonteCarlo } from '../nodes/material.wgsl';
+import { diffuseDirectionFunc, getLobeWeightsFunc } from '../nodes/sampling.wgsl';
+import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc } from '../nodes/ggx.wgsl';
+import { scatterRecordStruct } from '../nodes/structs.wgsl';
+import { pcgRand } from '../nodes/random.wgsl';
+import { ComputeKernel } from '../compute/ComputeKernel';
+
+export class GltfCompliantMaterial extends PathtracingMaterial {
+
+ constructor( options = {} ) {
+
+ super();
+
+ this.turquinTexture = new StorageTexture( 32, 32 );
+ this.turquinTexture.format = RedFormat;
+ this.turquinTexture.type = FloatType;
+ this.turquinTexture.minFilter = LinearFilter;
+ this.turquinTexture.magFilter = LinearFilter;
+
+ const turquinNode = texture( this.turquinTexture ).setName( 'turquinTexture' );
+
+ const {
+ specularBrdf = specularBrdfFunc,
+ diffuseBrdf = diffuseBrdfFunc,
+ fresnelMix = fresnelMixFunc,
+ conductorFresnel = conductorFresnelFunc,
+ } = options;
+
+ this.specularBrdf = specularBrdf;
+ this.diffuseBrdf = diffuseBrdf;
+ this.fresnelMix = fresnelMix;
+ this.conductorFresnel = conductorFresnel( turquinNode );
+
+ }
+
+ init( renderer ) {
+
+ const turquinParams = {
+ texture: textureStore( this.turquinTexture ).toWriteOnly(),
+ globalId,
+ };
+ const turquinKernel = new ComputeKernel( albedoIntegralMonteCarlo( turquinParams ), { workgroupSize: [ 16, 16, 1 ] } );
+
+ renderer.compute( turquinKernel.kernel, [ 4, 4, 1 ] );
+
+ }
+
+ getBsdfNode() {
+
+ const bsdfEvalFunc = wgslTagFn/* wgsl */`
+
+ fn bsdfEval( wo: vec3f, wi: vec3f, wh: vec3f, surf: SurfaceRecord ) -> vec3f {
+
+ let alpha = surf.roughness * surf.roughness;
+
+ let NdotV = max( wo.z, 1e-5 );
+ let NdotL = saturate( wi.z );
+ let NdotH = saturate( wh.z );
+ let VdotH = saturate( dot( wo, wh ) );
+
+ let specular = ${ this.specularBrdf }( NdotL, NdotV, NdotH, alpha );
+
+ let diffuse = ${ this.diffuseBrdf }( NdotV, NdotL, VdotH, surf );
+
+ let dielectric = ${ this.fresnelMix }( VdotH, surf.ior, diffuse, specular );
+
+ let metallic = ${ this.conductorFresnel }( NdotV, VdotH, surf.color, specular, alpha );
+
+ return mix( dielectric, metallic, surf.metalness );
+
+ }
+
+ `;
+
+ return wgslFn( /* wgsl */ `
+
+ fn bsdfSample( worldWo: vec3f, surf: SurfaceRecord ) -> ScatterRecord {
+
+ let alpha = surf.roughness * surf.roughness;
+ let normalBasis = surf.normalBasis;
+ let invBasis = surf.normalInvBasis;
+ let wo = normalize( invBasis * worldWo );
+
+ let weights = getLobeWeights( wo, wo, vec3( 0, 0, 1 ), vec3( 0, 0, 1 ), surf );
+
+ var cdf: vec4f;
+ cdf.x = weights.diffuse;
+ cdf.y = weights.specular + cdf.x;
+ cdf.z = 0; // pdf.transmission + cdf.y;
+ cdf.w = 0; // pdf.clearcoat + cdf.z;
+
+ let r = pcgRand() * cdf.y;
+
+ var wi: vec3f;
+ var wh: vec3f;
+
+ if ( r <= cdf.x ) { // diffuse
+
+ wi = diffuseDirection( wo, surf );
+ wh = normalize( wi + wo );
+
+ } else if ( r <= cdf.y ) { // specular
+
+ wh = ggxDirection( wo, vec2( alpha ), pcgRand2() );
+ if ( wh.z < 0 ) {
+
+ wh = -wh;
+
+ }
+ wi = - reflect( wo, wh );
+
+ } else if ( r <= cdf.z ) { // transmission / refraction
+
+ // NOT IMPLEMENTED
+
+ } else if ( r <= cdf.w ) { // clearcoat
+
+ // NOT IMPLEMENTED
+
+ }
+
+ var result: ScatterRecord;
+ result.pdf = 0;
+
+ if ( weights.diffuse > 0.0 ) {
+
+ result.pdf += weights.diffuse * wi.z / PI;
+
+ }
+
+ if ( weights.specular > 0.0 ) {
+
+ result.pdf += weights.specular * ggxReflectionAdjustedPDF( wo, wh, alpha );
+
+ }
+
+ result.color = bsdfEval( wo, wi, wh, surf );
+ result.color *= max( 0.0, wi.z );
+ result.direction = normalize( normalBasis * wi );
+
+ return result;
+
+ }
+
+ `, [
+ bsdfEvalFunc,
+ ggxReflectionAdjustedPDFFunc,
+ ggxDirectionFunc,
+ diffuseDirectionFunc,
+ scatterRecordStruct,
+ getLobeWeightsFunc,
+ pcgRand,
+ ] );
+
+ }
+
+}
diff --git a/src/webgpu/materials/PathtracingMaterial.js b/src/webgpu/materials/PathtracingMaterial.js
new file mode 100644
index 00000000..d6703295
--- /dev/null
+++ b/src/webgpu/materials/PathtracingMaterial.js
@@ -0,0 +1,30 @@
+import { lambertBsdfFunc } from '../nodes/material.wgsl';
+
+/**
+ * Defines a material sampled by the pathtracer
+ */
+export class PathtracingMaterial {
+
+ /**
+ *
+ * Must return a bsdf sampling function node with signature
+ * ( worldView: vec3f, surface: Surface ) -> ScatterRecord
+ *
+ */
+ getBsdfNode() {
+
+ return lambertBsdfFunc;
+
+ }
+
+ /**
+ *
+ * Called once per material
+ * Adds ability to initialize state
+ *
+ */
+ init( /* renderer */ ) {
+
+ }
+
+}
diff --git a/src/webgpu/nodes/ggx.wgsl.js b/src/webgpu/nodes/ggx.wgsl.js
new file mode 100644
index 00000000..926a2177
--- /dev/null
+++ b/src/webgpu/nodes/ggx.wgsl.js
@@ -0,0 +1,176 @@
+import { wgslFn } from 'three/tsl';
+import { constants } from './structs.wgsl.js';
+
+// See sampling.wgsl for vector shorthand explanations
+// The GGX functions provide sampling and distribution information for normals as output so
+// in order to get probability of scatter direction the half vector must be computed and provided.
+// [0] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
+// [1] https://hal.archives-ouvertes.fr/hal-01509746/document
+// [2] http://jcgt.org/published/0007/04/01/
+// [4] http://jcgt.org/published/0003/02/03/
+// [5] https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
+export const ggxDirectionFunc = wgslFn( /* wgsl */ `
+
+ fn ggxDirection( incidentDir: vec3f, alpha: vec2f, uv: vec2f ) -> vec3f {
+
+ // The GGX functions provide sampling and distribution information for normals as output so
+ // in order to get probability of scatter direction the half vector must be computed and provided.
+ // [0] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
+ // [1] https://hal.archives-ouvertes.fr/hal-01509746/document
+ // [2] http://jcgt.org/published/0007/04/01/
+ // [4] http://jcgt.org/published/0003/02/03/
+
+ // trowbridge-reitz === GGX === GTR
+
+
+ // TODO: try GGXVNDF implementation from reference [2], here. Needs to update ggxDistribution
+ // function below, as well
+
+ // Implementation from reference [1]
+ // stretch view
+ let V = normalize( vec3f( alpha * incidentDir.xy, incidentDir.z ) );
+
+ // orthonormal basis
+ var T1: vec3f;
+ if ( V.z < 0.9999 ) {
+ T1 = normalize( cross( V, vec3( 0.0, 0.0, 1.0 ) ) );
+ } else {
+ T1 = vec3( 1.0, 0.0, 0.0 );
+ }
+
+ let T2 = cross( T1, V );
+
+ // sample point with polar coordinates (r, phi)
+ let a = 1.0 / ( 1.0 + V.z );
+ let r = sqrt( uv.x );
+ var phi: f32;
+ if ( uv.y < a ) {
+ phi = uv.y / a * PI;
+ } else {
+ phi = PI + ( uv.y - a ) / ( 1.0 - a ) * PI;
+ }
+ let P1 = r * cos( phi );
+ var P2 = r * sin( phi );
+ if ( uv.y >= a ) {
+ P2 *= V.z;
+ }
+
+ // compute normal
+ var N = P1 * T1 + P2 * T2 + V * sqrt( max( 0.0, 1.0 - P1 * P1 - P2 * P2 ) );
+
+ // unstretch
+ N = normalize( vec3( alpha * N.xy, max( 0.0, N.z ) ) );
+
+ return N;
+
+ }
+
+`, [ constants ] );
+
+// Below are PDF and related functions for use in a Monte Carlo path tracer
+// as specified in Appendix B of the following paper
+// See equation (34) from reference [0]
+export const ggxLamdaFunc = wgslFn( /* wgsl */ `
+
+ fn ggxLamda( theta: f32, alpha: f32 ) -> f32 {
+
+ let tanTheta = tan( theta );
+ let tanTheta2 = tanTheta * tanTheta;
+ let alpha2 = alpha * alpha;
+
+ let numerator = - 1.0 + sqrt( 1.0 + alpha2 * tanTheta2 );
+ return numerator / 2.0;
+
+ }
+
+` );
+
+// TODO: write an optimized version, without tan/acos (see [5])
+// See equation (34) from reference [0]
+export const ggxShadowMaskG1Func = wgslFn( /* wgsl */ `
+
+ fn ggxShadowMaskG1( theta: f32, alpha: f32 ) -> f32 {
+
+ return 1.0 / ( 1.0 + ggxLamda( theta, alpha ) );
+
+ }
+
+`, [ ggxLamdaFunc ] );
+
+// See listing 2 from reference [5]
+export const ggxSmithVisibilityFunc = wgslFn( /* wgsl */ `
+
+ fn ggxSmithVisibility( NdotV: f32, NdotL: f32, alpha: f32 ) -> f32 {
+
+ // Original formulation of G_SmithGGX Correlated
+ // lambda_v = ( -1 + sqrt ( alphaG2 * (1 - NdotL2 ) / NdotL2 + 1)) * 0.5 f;
+ // lambda_l = ( -1 + sqrt ( alphaG2 * (1 - NdotV2 ) / NdotV2 + 1)) * 0.5 f;
+ // G_SmithGGXCorrelated = 1 / (1 + lambda_v + lambda_l );
+ // V_SmithGGXCorrelated = G_SmithGGXCorrelated / (4.0 f * NdotL * NdotV );
+
+ // This is an optimized version
+ let alpha2 = alpha * alpha;
+ // Caution: the "NdotL *" and "NdotV *" are explicitely inversed , this is not a mistake.
+ let Lambda_GGXV = NdotL * sqrt (( - NdotV * alpha2 + NdotV ) * NdotV + alpha2 );
+ let Lambda_GGXL = NdotV * sqrt (( - NdotL * alpha2 + NdotL ) * NdotL + alpha2 );
+
+ return 0.5 / ( Lambda_GGXV + Lambda_GGXL );
+
+ }
+
+`, [ ggxLamdaFunc ] );
+
+
+// TODO: write an aptimized version without tan/acos (see [5])
+// See equation (33) from reference [0]
+export const ggxDistributionFunc = wgslFn( /* wgsl */ `
+ fn ggxDistribution( halfVectorAngleCos: f32, alpha: f32 ) -> f32 {
+
+ var a2 = alpha * alpha;
+ a2 = max( EPSILON, a2 );
+ let cosTheta = halfVectorAngleCos;
+ let cosTheta4 = pow( cosTheta, 4.0 );
+
+ if ( cosTheta == 0.0 ) {
+ return 0.0;
+ }
+
+ let theta = acos( clamp( cosTheta, -1.0, 1.0 ) );
+ let tanTheta = tan( theta );
+ let tanTheta2 = pow( tanTheta, 2.0 );
+
+ let denom = PI * cosTheta4 * pow( a2 + tanTheta2, 2.0 );
+ return ( a2 / denom );
+
+ }
+`, );
+
+// See equation (3) from reference [2]
+export const ggxPDFFunc = wgslFn( /* wgsl */ `
+ fn ggxPDF( wo: vec3f, wh: vec3f, roughness: f32 ) -> f32 {
+
+ let D = ggxDistribution( wh.z, roughness );
+ let incidentTheta = acos( wo.z );
+ let G1 = ggxShadowMaskG1( incidentTheta, roughness );
+
+ return D * G1 * max( 0.0, dot( wo, wh ) ) / wo.z;
+
+ }
+`, [ ggxDistributionFunc, ggxShadowMaskG1Func ] );
+
+// ggxPDF, divided by the Jacobian of reflection operation
+// See equation (17) from [2]
+// Note: dot( wo, halfVector ) cancel out bc its guaranteed to be > 0
+export const ggxReflectionAdjustedPDFFunc = wgslFn( /* wgsl */ `
+ fn ggxReflectionAdjustedPDF( wo: vec3f, wh: vec3f, alpha: f32 ) -> f32 {
+
+ let NdotV = max( wo.z, 1e-5 );
+ let NdotH = max( wh.z, 1e-5 );
+ let D = ggxDistribution( NdotH, alpha );
+ let incidentTheta = acos( NdotV );
+ let G1 = ggxShadowMaskG1( incidentTheta, alpha );
+
+ return D * G1 / ( 4 * wo.z );
+
+ }
+`, [ ggxDistributionFunc, ggxShadowMaskG1Func ] );
diff --git a/src/webgpu/nodes/material.wgsl.js b/src/webgpu/nodes/material.wgsl.js
index 7c089fc1..e06f4c5a 100644
--- a/src/webgpu/nodes/material.wgsl.js
+++ b/src/webgpu/nodes/material.wgsl.js
@@ -1,32 +1,15 @@
import { wgslFn } from 'three/tsl';
-import { inverseMat3x3Func, getBasisFromNormalFunc } from './utils.wgsl';
+import {
+ inverseMat3x3Func,
+ getBasisFromNormalFunc,
+ iorToF0Func,
+ schlickFresnelFunc,
+ schlickFresnelVecFunc,
+} from './utils.wgsl';
+import { ggxSmithVisibilityFunc, ggxDistributionFunc, ggxDirectionFunc, ggxReflectionAdjustedPDFFunc } from './ggx.wgsl';
import { constants, surfaceRecordStruct, scatterRecordStruct } from './structs.wgsl';
import { sampleSphereCosineFn } from './sampling.wgsl';
-import { pcgRand2 } from './random.wgsl';
-
-export const applyFilteredGlossyFunc = wgslFn( /* wgsl */ `
-
- fn applyFilteredGlossy( roughness: f32, accumulatedRoughness: f32 ) -> f32 {
-
- return clamp(
- max(
- roughness,
- accumulatedRoughness * filterGlossyFactor * 5.0 ),
- 0.0,
- 1.0
- );
-
- }
-
-`, [ constants ] );
-
-export const iorToF0Func = wgslFn( /* wgsl */ `
-
- fn iorToF0( ior: f32 ) -> f32 {
- return pow( ( 1 - ior ) / ( 1 + ior ), 2 );
- }
-
-` );
+import { pcgInit, pcgRand2 } from './random.wgsl';
export const getSurfaceRecordFunc = wgslFn( /* wgsl */ `
@@ -244,8 +227,8 @@ export const getSurfaceRecordFunc = wgslFn( /* wgsl */ `
surf.specularColor = specularColor;
surf.specularIntensity = specularIntensity;
- surf.roughness = roughness * roughness;
- surf.clearcoatRoughness = clearcoatRoughness * clearcoatRoughness;
+ surf.roughness = roughness;
+ surf.clearcoatRoughness = clearcoatRoughness;
surf.sheenRoughness = sheenRoughness;
// frontFace is used to determine transmissive properties and PDF. If no transmission is used
@@ -258,15 +241,6 @@ export const getSurfaceRecordFunc = wgslFn( /* wgsl */ `
}
surf.f0 = iorToF0( surf.eta );
- // Compute the filtered roughness value to use during specular reflection computations.
- // The accumulated roughness value is scaled by a user setting and a "magic value" of 5.0.
- // If we're exiting something transmissive then scale the factor down significantly so we can retain
- // sharp internal reflections
- // TODO: accumulate roughness in the main cycle
- let accumulatedRoughness = 0.0;
- surf.filteredRoughness = applyFilteredGlossy( surf.roughness, accumulatedRoughness );
- surf.filteredClearcoatRoughness = applyFilteredGlossy( surf.clearcoatRoughness, accumulatedRoughness );
-
// get the normal frames
surf.normalBasis = getBasisFromNormal( surf.normal );
surf.normalInvBasis = inverse( surf.normalBasis );
@@ -280,7 +254,6 @@ export const getSurfaceRecordFunc = wgslFn( /* wgsl */ `
`, [
inverseMat3x3Func,
iorToF0Func,
- applyFilteredGlossyFunc,
getBasisFromNormalFunc,
surfaceRecordStruct,
] );
@@ -302,3 +275,187 @@ export const lambertBsdfFunc = wgslFn( /* wgsl */`
}
`, [ scatterRecordStruct, sampleSphereCosineFn, pcgRand2, constants, surfaceRecordStruct ] );
+
+/*
+ *
+ * N : Macronormal of the surface
+ * V ( wo ) : View direction
+ * L ( wi ) : Light direction
+ * H ( wh ) : Halfvector between V and L, micronormal of the surface in ggx
+ * f0 : Amount of light reflected when looking at a surface head on - "fresnel 0"
+ * f90 : Amount of light reflected at grazing angles
+ *
+ */
+
+// Disney Diffuse BRDF without subsurface approximation
+export const diffuseBrdfFunc = wgslFn( /* wgslFn */ `
+
+ fn diffuseBrdf( NdotV: f32, NdotL: f32, VdotH: f32, surf: SurfaceRecord ) -> vec3f {
+
+ // https://blog.selfshadow.com/publications/s2015-shading-course/burley/s2015_pbs_disney_bsdf_notes.pdf
+ // See equation (4)
+
+ let fl = schlickFresnel( NdotL, 0.0 );
+ let fv = schlickFresnel( NdotV, 0.0 );
+
+ let alpha = surf.roughness * surf.roughness;
+ let bias = mix( 0.0, 0.5, alpha) - 1;
+ let energyFactor = mix( 1.0, 1.0 / 1.51, alpha );
+
+ let rr = 2.0 * alpha * VdotH * VdotH;
+ let retro = rr * ( fl + fv + fl * fv * ( rr + 2.0 * bias ) );
+ let fresnel = ( 1.0 + bias * fl ) * ( 1.0f + bias * fv );
+
+ // TODO: subsurface approx?
+
+ return energyFactor * ( surf.color / PI ) * ( retro + fresnel );
+
+ }
+
+`, [ constants, schlickFresnelFunc, surfaceRecordStruct ] );
+
+export const specularBrdfFunc = wgslFn( /* wgslFn */ `
+
+ fn specularBrdf( NdotL: f32, NdotV: f32, NdotH: f32, alpha: f32 ) -> vec3f {
+
+ let Vis = ggxSmithVisibility( NdotV, NdotL, alpha );
+ let D = ggxDistribution( NdotH, alpha );
+
+ return vec3f( D * Vis );
+
+ }
+
+`, [ ggxSmithVisibilityFunc, ggxDistributionFunc ] );
+
+export const fresnelMixFunc = wgslFn( /* wgslFn */ `
+
+ fn fresnelMix( VdotH: f32, ior: f32, base: vec3f, layer: vec3f ) -> vec3f {
+
+ let f0 = iorToF0( ior );
+ let F = schlickFresnel( abs( VdotH ), f0 );
+ return base + F * layer;
+
+ }
+
+`, [ schlickFresnelFunc, iorToF0Func ] );
+
+export const conductorFresnelFunc = ( turquinTexture ) => wgslFn( /* wgslFn */ `
+
+ fn conductorFresnel( NdotV: f32, VdotH: f32, f0: vec3f, bsdf: vec3f, alpha: f32 ) -> vec3f {
+
+ let ss = bsdf * schlickFresnelVec( abs( VdotH ), f0, vec3f( 1 ) );
+
+ let uv = vec2( NdotV, sqrt( alpha ) );
+ let energySs = max( textureSampleLevel( turquinTexture, turquinTexture_sampler, uv, 0 ).r, 1e-5);
+
+ return ss * ( 1.0 + f0 * ( 1.0 - energySs ) / energySs );
+
+ }
+
+`, [ schlickFresnelVecFunc, turquinTexture ] );
+
+// GGX Multibounce compensation using Turquin's method
+
+export const albedoIntegralUniform = wgslFn( /* wgsl */ `
+
+ fn albedo(
+ texture: texture_storage_2d,
+
+ globalId: vec3u,
+ ) -> void {
+
+ const INTEGRATION_DIMENSIONS = vec2( 128, 128 );
+
+ let dimensions = textureDimensions( texture ).xy;
+ let uv = ( vec2f( globalId.xy ) + vec2f( 0.5 ) ) / vec2f( dimensions );
+
+ let cosThetaO = uv.x;
+ let roughness = uv.y;
+ let alpha = roughness * roughness;
+
+ let wo = vec3( sqrt( 1 - cosThetaO * cosThetaO ), 0 , cosThetaO );
+
+ var result = 0.0;
+ for ( var i = 0; i < INTEGRATION_DIMENSIONS.x; i++ ) {
+
+ let phi = ( f32( i ) + 0.5 ) * 2.0 * PI / f32( INTEGRATION_DIMENSIONS.x );
+ let cosPhi = cos( phi );
+ let sinPhi = sin( phi );
+
+ for ( var j = 0; j < INTEGRATION_DIMENSIONS.y; j++ ) {
+
+ // cosTheta
+ let nu = ( f32( j ) + 0.5 ) / f32( INTEGRATION_DIMENSIONS.y );
+
+ let sinTheta = sqrt( 1 - nu * nu );
+
+ let wi = vec3( sinTheta * cosPhi, sinTheta * sinPhi, nu );
+
+ let wh = normalize( wi + wo );
+
+ let NdotV = max( wo.z, 1e-5 );
+ let NdotL = saturate( wi.z );
+ let NdotH = saturate( wh.z );
+
+ let specular = specularBrdf( NdotL, NdotV, NdotH, alpha );
+ let weight = 2.0 * PI / f32( INTEGRATION_DIMENSIONS.x * INTEGRATION_DIMENSIONS.y );
+ result += specular.x * NdotL * weight;
+ }
+
+ }
+
+ textureStore(texture, globalId.xy, vec4( saturate( result ) ));
+
+ }
+
+`, [ constants, specularBrdfFunc ] );
+
+export const albedoIntegralMonteCarlo = wgslFn( /* wgsl */ `
+
+ fn albedo(
+ texture: texture_storage_2d,
+
+ globalId: vec3u,
+ ) -> void {
+
+ const INTEGRATION_SAMPLES = 4096;
+ pcgInitialize( globalId.xy, 0 );
+
+ let dimensions = textureDimensions( texture ).xy;
+ let uv = ( vec2f( globalId.xy ) + vec2f( 0.5 ) ) / vec2f( dimensions );
+
+ let cosThetaO = uv.x;
+ let roughness = uv.y;
+
+ let alpha = roughness * roughness;
+
+ let wo = vec3( sqrt( 1 - cosThetaO * cosThetaO ), 0 , cosThetaO );
+
+ var result = 0.0;
+ for ( var i = 0; i < INTEGRATION_SAMPLES; i++ ) {
+
+ var wh = ggxDirection( wo, vec2( alpha ), pcgRand2() );
+ if ( wh.z < 0 ) {
+
+ wh = -wh;
+
+ }
+ let wi = - reflect( wo, wh );
+
+ let NdotV = max( wo.z, 1e-5 );
+ let NdotL = saturate( wi.z );
+ let NdotH = saturate( wh.z );
+
+ let specular = specularBrdf( NdotL, NdotV, NdotH, alpha );
+ let weight = 1 / ggxReflectionAdjustedPDF( wo, wh, alpha );
+ result += specular.x * NdotL * weight;
+
+ }
+
+ result /= f32( INTEGRATION_SAMPLES );
+
+ textureStore(texture, globalId.xy, vec4( result ));
+
+ }
+
+`, [ pcgInit, pcgRand2, constants, specularBrdfFunc, ggxDirectionFunc, ggxReflectionAdjustedPDFFunc ] );
diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js
index ecb2dee0..58909f06 100644
--- a/src/webgpu/nodes/random.wgsl.js
+++ b/src/webgpu/nodes/random.wgsl.js
@@ -25,9 +25,9 @@ export const pcgInit = wgslFn( /* wgsl */`
export const pcg4d = wgslFn( /* wgsl */ `
fn pcg4d(v: ptr) -> void {
*v = *v * 1664525u + 1013904223u;
- v.x += v.y*v.w; v.y += v.z*v.x; v.z += v.x*v.y; v.w += v.y*v.z;
+ *v = *v + v.yzxy * v.wxyz;
*v = *v ^ (*v >> vec4u(16u));
- v.x += v.y*v.w; v.y += v.z*v.x; v.z += v.x*v.y; v.w += v.y*v.z;
+ *v = *v + v.yzxy * v.wxyz;
}
` );
@@ -36,14 +36,12 @@ export const pcgCycleState = wgslFn( /* wgsl */ `
for (var i = 0u; i < n; i++) {
pcg4d(&g_state.s0);
}
- }
` );
-// TODO: test if abs there is necessary
-export const pcgRand3 = wgslFn( /*wgsl*/`
- fn pcgRand3() -> vec3f {
+export const pcgRand = wgslFn( /*wgsl*/`
+ fn pcgRand() -> f32 {
pcg4d(&g_state.s0);
- return abs( vec3f(g_state.s0.xyz) / f32(0xffffffffu) );
+ return abs( f32( g_state.s0.x ) / f32(0xffffffffu) );
}
`, [ pcg4d, pcgStateStruct ] );
@@ -53,3 +51,10 @@ export const pcgRand2 = wgslFn( /*wgsl*/`
return abs( vec2f(g_state.s0.xy) / f32(0xffffffffu) );
}
`, [ pcg4d, pcgStateStruct ] );
+
+export const pcgRand3 = wgslFn( /*wgsl*/`
+ fn pcgRand3() -> vec3f {
+ pcg4d(&g_state.s0);
+ return abs( vec3f(g_state.s0.xyz) / f32(0xffffffffu) );
+ }
+`, [ pcg4d, pcgStateStruct ] );
diff --git a/src/webgpu/nodes/sampling.wgsl.js b/src/webgpu/nodes/sampling.wgsl.js
index 42a42fe8..a448f019 100644
--- a/src/webgpu/nodes/sampling.wgsl.js
+++ b/src/webgpu/nodes/sampling.wgsl.js
@@ -1,5 +1,15 @@
import { wgslFn } from 'three/tsl';
-import { environmentInfoStruct, constants } from './structs.wgsl.js';
+import { environmentInfoStruct, constants, lobeWeightsStruct } from './structs.wgsl.js';
+import { pcgRand2 } from './random.wgsl.js';
+import { evaluateFresnelFunc } from './utils.wgsl.js';
+
+/*
+wi : incident vector or light vector (pointing toward the light)
+wo : outgoing vector or view vector (pointing towards the camera)
+wh : computed half vector from wo and wi
+Vectors above are assumed to be in tangent space. i.e. +z is along macronormal of the surface
+eta : Greek character used to denote the "ratio of ior"
+*/
// TODO: Move to a local (s, t, n) coordinate system
// From RayTracingGems v1.9 chapter 16.6.2 -- Its shit!
@@ -7,6 +17,7 @@ import { environmentInfoStruct, constants } from './structs.wgsl.js';
// result.xyz = cosine-wighted vector on the hemisphere oriented to a vector
// result.w = pdf
export const sampleSphereCosineFn = wgslFn( /* wgsl */ `
+
fn sampleSphereCosine(rng: vec2f, n: vec3f) -> vec4f {
let a = (1 - 2 * rng.x) * 0.99999;
@@ -17,9 +28,70 @@ export const sampleSphereCosineFn = wgslFn( /* wgsl */ `
return vec4f( direction, pdf );
}
+
`, [ constants ] );
+export const sampleSphereFunc = wgslFn( /* wgsl */ `
+
+ fn sampleSphere( uv: vec2f ) -> vec3f {
+
+ let u = ( uv.x - 0.5 ) * 2.0;
+ let t = uv.y * PI * 2.0;
+ let f = sqrt( 1.0 - u * u );
+
+ return vec3f( f * cos( t ), f * sin( t ), u );
+
+ }
+
+`, [ constants ] );
+
+export const diffuseDirectionFunc = wgslFn( /* wgsl */ `
+
+ fn diffuseDirection( wo: vec3f, surf: SurfaceRecord ) -> vec3f {
+
+ var lightDirection = sampleSphere( pcgRand2() );
+ lightDirection.z += 1.0;
+ lightDirection = normalize( lightDirection );
+
+ return lightDirection;
+
+ }
+
+`, [ sampleSphereFunc, pcgRand2 ] );
+
+export const getLobeWeightsFunc = wgslFn( /* wgsl */ `
+
+ fn getLobeWeights(wo: vec3f, wi: vec3f, wh: vec3f, clearcoatWo: vec3f, surf: SurfaceRecord) -> LobeWeights {
+
+ // TODO: experiment with this; I don't see any usage of normal?
+ let metalness = surf.metalness;
+ let transmission = surf.transmission;
+ let HdotL = dot( wh, wo );
+ let fEstimate = evaluateFresnel( HdotL, surf.eta, vec3f( surf.f0 ), vec3f( 1.0 ) ).x;
+
+ let transSpecularProb = mix( max( 0.25, fEstimate ), 1.0, metalness );
+ let diffSpecularProb = 0.5 + 0.5 * metalness;
+
+ var weights: LobeWeights;
+ weights.diffuse = ( 1.0 - transmission ) * ( 1.0 - diffSpecularProb );
+ weights.specular = transmission * transSpecularProb + ( 1.0 - transmission ) * diffSpecularProb;
+ weights.transmission = transmission * ( 1.0 - transSpecularProb );
+ weights.clearcoat = surf.clearcoat * schlickFresnel( clearcoatWo.z, 0.04 );
+
+ let totalWeight = weights.diffuse + weights.specular; // + weights.transmission + weights.clearcoat;
+ weights.diffuse /= totalWeight;
+ weights.specular /= totalWeight;
+ // weights.transmission /= totalWeight;
+ // weights.clearcoat /= totalWeight;
+
+ return weights;
+
+ }
+
+`, [ evaluateFresnelFunc, lobeWeightsStruct ] );
+
const equirectDirectionToUvFn = wgslFn( /* wgsl */`
+
fn equirectDirectionToUv(direction: vec3f) -> vec2f {
// from Spherical.setFromCartesianCoords
@@ -32,14 +104,17 @@ const equirectDirectionToUvFn = wgslFn( /* wgsl */`
return uv;
}
+
` );
const sampleEquirectColorFn = wgslFn( /* wgsl */ `
+
fn sampleEquirectColor( envMap: texture_2d, envMapSampler: sampler, direction: vec3f ) -> vec4f {
return textureSampleLevel( envMap, envMapSampler, equirectDirectionToUv( direction ), 0 );
}
+
`, [ equirectDirectionToUvFn ] );
const sampleHemisphereFn = wgslFn( /* wgsl */ `
@@ -85,6 +160,7 @@ export const sampleEnvironmentFn = wgslFn( /* wgsl */ `
`, [ sampleEquirectColorFn, sampleHemisphereFn, environmentInfoStruct ] );
export const weightedAlphaBlendFn = wgslFn( /* wgsl */`
+
fn weightedAlphaBlend( prevColor: vec4f, newColor: vec4f, weight: f32 ) -> vec4f {
let invWeight = 1.0 - weight;
@@ -101,4 +177,5 @@ export const weightedAlphaBlendFn = wgslFn( /* wgsl */`
return blendedColor;
}
+
` );
diff --git a/src/webgpu/nodes/structs.wgsl.js b/src/webgpu/nodes/structs.wgsl.js
index 5e391ee5..ae107f39 100644
--- a/src/webgpu/nodes/structs.wgsl.js
+++ b/src/webgpu/nodes/structs.wgsl.js
@@ -2,9 +2,10 @@ import { wgsl } from 'three/tsl';
import { StructTypeNode } from 'three/webgpu';
export const constants = wgsl( /* wgsl */ `
- // TODO: expose modification of this value
- const filterGlossyFactor: f32 = 0.5;
+
const PI: f32 = 3.141592653589793;
+ const EPSILON: f32 = 1e-5;
+
` );
export const scatterRecordStruct = new StructTypeNode( {
@@ -149,7 +150,6 @@ export const surfaceRecordStruct = new StructTypeNode( {
// material
roughness: 'f32',
- filteredRoughness: 'f32',
metalness: 'f32',
color: 'vec3f',
emission: 'vec3f',
@@ -167,7 +167,6 @@ export const surfaceRecordStruct = new StructTypeNode( {
clearcoatInvBasis: 'mat3x3f',
clearcoat: 'f32',
clearcoatRoughness: 'f32',
- filteredClearcoatRoughness: 'f32',
// sheen
sheen: 'f32',
@@ -184,15 +183,6 @@ export const surfaceRecordStruct = new StructTypeNode( {
specularIntensity: 'f32',
}, 'SurfaceRecord' );
-// TODO: write a proposal for a storage-backed structs and arrays in structs for three.js
-//
-// const hitResultQueueStruct = wgsl( /* wgsl */ `
-// struct HitResultQueue {
-// currentSize: atomic,
-// queue: array,
-// };
-// `, [ hitResultQueueElementStruct ] );
-
export const rayQueueElementStruct = new StructTypeNode( {
origin: 'vec3',
_alignment0: 'uint',
@@ -203,19 +193,15 @@ export const rayQueueElementStruct = new StructTypeNode( {
pixel: 'vec2u',
}, 'RayQueueElement' );
-export const hitResultQueueElementStruct = new StructTypeNode( {
- normal: 'vec3f',
- pixel_x: 'uint',
- position: 'vec3f',
- pixel_y: 'uint',
- view: 'vec3f',
- currentBounce: 'uint',
- throughputColor: 'vec3f',
- vertexIndex: 'uint',
-}, 'HitResultQueueElement' );
-
export const environmentInfoStruct = new StructTypeNode( {
rotation: 'mat3x3f',
intensity: 'float',
blur: 'float',
}, 'EnvironmentInfo' );
+
+export const lobeWeightsStruct = new StructTypeNode( {
+ diffuse: 'float',
+ specular: 'float',
+ transmission: 'float',
+ clearcoat: 'float',
+}, 'LobeWeights' );
diff --git a/src/webgpu/nodes/utils.wgsl.js b/src/webgpu/nodes/utils.wgsl.js
index 67c8e2f7..34219541 100644
--- a/src/webgpu/nodes/utils.wgsl.js
+++ b/src/webgpu/nodes/utils.wgsl.js
@@ -45,3 +45,58 @@ export const getBasisFromNormalFunc = wgslFn( /* wgsl */ `
}
` );
+
+export const iorToF0Func = wgslFn( /* wgsl */ `
+
+ fn iorToF0( ior: f32 ) -> f32 {
+ return pow( ( 1 - ior ) / ( 1 + ior ), 2 );
+ }
+
+` );
+
+export const schlickFresnelFunc = wgslFn( /* wgsl */ `
+
+ fn schlickFresnel( cosine: f32, f0: f32 ) -> f32 {
+
+ return f0 + ( 1.0 - f0 ) * pow( 1.0 - cosine, 5.0 );
+
+ }
+
+` );
+
+export const schlickFresnelVecFunc = wgslFn( /* wgsl */ `
+
+ fn schlickFresnelVec( cosine: f32, f0: vec3f, f90: vec3f ) -> vec3f {
+
+ return f0 + ( f90 - f0 ) * pow( 1.0 - cosine, 5.0 );
+
+ }
+
+` );
+
+export const totalInternalReflectionFunc = wgslFn( /* wgsl */ `
+
+ fn totalInternalReflection( cosTheta: f32, eta: f32 ) -> bool {
+
+ let sinTheta = sqrt( 1.0 - cosTheta * cosTheta );
+ return eta * sinTheta > 1.0;
+
+ }
+
+` );
+
+export const evaluateFresnelFunc = wgslFn( /* wgsl */ `
+
+ fn evaluateFresnel( cosine: f32, eta: f32, f0: vec3f, f90: vec3f ) -> vec3f {
+
+ if ( totalInternalReflection( cosine, eta ) ) {
+
+ return f90;
+
+ }
+
+ return f0 + ( f90 - f0 ) * pow( 1.0 - cosine, 5.0 );
+ }
+
+`, [ totalInternalReflectionFunc ] );
+