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 ] ); +