diff --git a/example/viewerTest.js b/example/viewerTest.js index 6945e88b..87253d44 100644 --- a/example/viewerTest.js +++ b/example/viewerTest.js @@ -18,7 +18,7 @@ import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; import { WebGLPathTracer } from 'three-gpu-pathtracer'; -import { WebGPUPathTracer } from 'three-gpu-pathtracer/webgpu'; +import { RNG_PCG, RNG_SOBOL, RNG_SOBOL_TEXTURE, WebGPUPathTracer } from 'three-gpu-pathtracer/webgpu'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { ParallelMeshBVHWorker } from 'three-mesh-bvh/worker'; import { LoaderElement } from './utils/LoaderElement.js'; @@ -37,6 +37,7 @@ const params = { isWebGPU, useMegakernel: true, + webgpuPRNG: RNG_SOBOL, enable: true, bounces: 10, @@ -328,14 +329,28 @@ function buildGui() { } ); - webgpuOptions = pathTracingFolder.add( params, 'useMegakernel' ); - webgpuOptions.onChange( () => { + webgpuOptions = pathTracingFolder.addFolder( 'WebGPU Options' ); + + webgpuOptions.add( params, 'useMegakernel' ).onChange( () => { pathTracer.useMegakernel( params.useMegakernel ); pathTracer.reset(); detailedSampleCount = null; } ); + + webgpuOptions.add( params, 'webgpuPRNG', { + PCG: RNG_PCG, + SOBOL: RNG_SOBOL, + SOBOL_TEXTURE: RNG_SOBOL_TEXTURE + } ).onChange( () => { + + pathTracer.setPRNGType( params.webgpuPRNG ); + pathTracer.reset(); + + } ); + + webgpuOptions.show( params.isWebGPU ); pathTracingFolder.add( params, 'enable' ); diff --git a/src/webgpu/MegaKernelPathTracer.js b/src/webgpu/MegaKernelPathTracer.js index 8c81e03d..0ada9f48 100644 --- a/src/webgpu/MegaKernelPathTracer.js +++ b/src/webgpu/MegaKernelPathTracer.js @@ -5,9 +5,9 @@ import { PathTracerBackend } from './PathTracerBackend.js'; export class MegaKernelPathTracer extends PathTracerBackend { - constructor( renderer ) { + constructor( renderer, rngData ) { - super( renderer ); + super( renderer, rngData ); // options this.tiles = new Vector2( 2, 2 ); @@ -15,8 +15,7 @@ export class MegaKernelPathTracer extends PathTracerBackend { this.samples = 0; // kernels - this.kernel = new PathTracerMegaKernel().setWorkgroupSize( 8, 8, 1 ); - this.setMaterial( this.material ); + this.kernel = new PathTracerMegaKernel( this.rngData ).setWorkgroupSize( 8, 8, 1 ); } @@ -32,12 +31,7 @@ export class MegaKernelPathTracer extends PathTracerBackend { this.kernel.textures = texture; this.kernel.kernel.computeNode.parameters.textureSampler.node.value = texture; - - } - - getMaterial() { - - return this.material; + this.reset(); } diff --git a/src/webgpu/PathTracerBackend.js b/src/webgpu/PathTracerBackend.js index e94b0a1e..7167449d 100644 --- a/src/webgpu/PathTracerBackend.js +++ b/src/webgpu/PathTracerBackend.js @@ -1,11 +1,10 @@ import { ColorManagement, FloatType, 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 { - constructor( renderer ) { + constructor( renderer, rngData ) { this.renderer = renderer; this.camera = null; @@ -38,7 +37,7 @@ export class PathTracerBackend { this.sampleCountClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); this.outputTargetClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); - this.material = new GltfCompliantMaterial(); + this.rngData = rngData; } @@ -122,13 +121,6 @@ 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/RNGData.js b/src/webgpu/RNGData.js new file mode 100644 index 00000000..d96ac14a --- /dev/null +++ b/src/webgpu/RNGData.js @@ -0,0 +1,22 @@ +import { proxyFn } from './lib/nodes/NodeProxy'; + +export class RNGData { + + constructor() { + + this.init = proxyFn( 'fns.init', this ); + this.nextBounce = proxyFn( 'fns.nextBounce', this ); + this.f32 = proxyFn( 'fns.f32', this ); + this.vec2f = proxyFn( 'fns.vec2f', this ); + this.vec3f = proxyFn( 'fns.vec3f', this ); + this.vec4f = proxyFn( 'fns.vec4f', this ); + + } + + setFunctions( fns ) { + + this.fns = fns; + + } + +} diff --git a/src/webgpu/SobolNumberMapGenerator.js b/src/webgpu/SobolNumberMapGenerator.js new file mode 100644 index 00000000..10488352 --- /dev/null +++ b/src/webgpu/SobolNumberMapGenerator.js @@ -0,0 +1,51 @@ +import { FloatType, MeshBasicNodeMaterial, NearestFilter, RenderTarget, RGBAFormat } from 'three/webgpu'; +import { FullScreenQuad } from 'three/examples/jsm/Addons.js'; +import { generateSobolPointFunc } from './nodes/random.wgsl'; +import { uv } from 'three/tsl'; + +const _quad = new FullScreenQuad( new MeshBasicNodeMaterial() ); +export class SobolNumberMapGenerator { + + constructor( renderer, dimensions ) { + + this.target = new RenderTarget( dimensions, dimensions, { + + type: FloatType, + format: RGBAFormat, + minFilter: NearestFilter, + maxFilter: NearestFilter, + generateMipmaps: false, + + } ); + + this.renderer = renderer; + this.dimensions = dimensions; + this.isGenerated = false; + + } + + get texture() { + + return this.target.texture; + + } + + generate() { + + const { renderer, dimensions, target } = this; + + const ogTarget = renderer.getRenderTarget(); + renderer.setRenderTarget( target ); + + _quad.material.colorNode = generateSobolPointFunc( + uv().x.mul( dimensions ).toUint().add( uv().y.mul( dimensions ).toUint().mul( dimensions ) ) + ); + _quad.render( renderer ); + + renderer.setRenderTarget( ogTarget ); + + this.isGenerated = true; + + } + +} diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index 25f43f8b..093fdd0b 100644 --- a/src/webgpu/WaveFrontPathTracer.js +++ b/src/webgpu/WaveFrontPathTracer.js @@ -18,9 +18,9 @@ const MAX_HIT_COUNT = Math.floor( MAX_BUFFER_SIZE / ( queuedHitStruct.getLength( export class WaveFrontPathTracer extends PathTracerBackend { - constructor( renderer ) { + constructor( renderer, rngData ) { - super( renderer ); + super( renderer, rngData ); // options this.tiles = new Vector2( 3, 3 ); @@ -44,10 +44,10 @@ export class WaveFrontPathTracer extends PathTracerBackend { // kernels this.primeRayGenerationDispatchKernel = new PrimeRayGenerationDispatchKernel().setWorkgroupSize( 1, 1, 1 ); - this.enqueueRaysKernel = new RayGenerationKernel().setWorkgroupSize( 8, 8, 1 ); - this.rayIntersectionKernel = new RayIntersectionKernel().setWorkgroupSize( 64, 1, 1 ); + this.enqueueRaysKernel = new RayGenerationKernel( this.rngData ).setWorkgroupSize( 8, 8, 1 ); + this.rayIntersectionKernel = new RayIntersectionKernel( this.rngData ).setWorkgroupSize( 64, 1, 1 ); this.updateRayQueueParamsKernel = new UpdateRayQueueParamsKernel().setWorkgroupSize( 1, 1, 1 ); - this.hitProcessKernel = new ProcessHitsKernel( this.material ).setWorkgroupSize( 64, 1, 1 ); + this.hitProcessKernel = new ProcessHitsKernel( this.rngData ).setWorkgroupSize( 64, 1, 1 ); // clear kernels this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 ); @@ -58,6 +58,19 @@ export class WaveFrontPathTracer extends PathTracerBackend { } + setRandomFunctions( randomFunctions ) { + + this.enqueueRaysKernel.random = randomFunctions; + this.enqueueRaysKernel.needsUpdate = true; + + this.rayIntersectionKernel.random = randomFunctions; + this.rayIntersectionKernel.needsUpdate = true; + + this.hitProcessKernel.random = randomFunctions; + this.hitProcessKernel.needsUpdate = true; + + } + setBVHData( bvhData ) { this.rayIntersectionKernel.bvhData = bvhData; @@ -79,8 +92,8 @@ export class WaveFrontPathTracer extends PathTracerBackend { setMaterial( material ) { - this.material = material; - this.hitProcessKernel = new ProcessHitsKernel( this.material ).setWorkgroupSize( 64, 1, 1 ); + this.hitProcessKernel.material = material.getData(); + this.hitProcessKernel.needsUpdate = true; this.reset(); } @@ -172,6 +185,8 @@ export class WaveFrontPathTracer extends PathTracerBackend { super.reset(); + this.enqueueRaysKernel.seed = 0; + const { width, height } = sampleCountTarget; const dispatchSize = sampleCountClearKernel.getDispatchSize( width, height ); diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 42033c95..a2f77db0 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -8,9 +8,18 @@ import { CubeToEquirectGenerator } from '../utils/CubeToEquirectGenerator.js'; import { PathtracerBVHComputeData } from './nodes/PathtracerBVHComputeData.js'; import { RenderTarget2DArray } from './RenderTarget2DArray.js'; import { setCommonAttributes } from '../core/utils/GeometryPreparationUtils.js'; +import { pcgFunctions, sobolFunctions, sobolTextureFunctions } from './nodes/random.wgsl.js'; +import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js'; +import { SobolNumberMapGenerator } from './SobolNumberMapGenerator.js'; +import { RNGData } from './RNGData.js'; const _resolution = new Vector2(); const _color = new Color(); + +export const RNG_PCG = 0; +export const RNG_SOBOL = 1; +export const RNG_SOBOL_TEXTURE = 2; + export class WebGPUPathTracer { get bounces() { @@ -41,9 +50,10 @@ export class WebGPUPathTracer { useMegakernel( value ) { this._pathTracer.dispose(); - this._pathTracer = value ? new MegaKernelPathTracer( this._renderer ) : new WaveFrontPathTracer( this._renderer ); + this._pathTracer = value ? new MegaKernelPathTracer( this._renderer, this.rngData ) : new WaveFrontPathTracer( this._renderer, this.rngData ); this._pathTracer.setBVHData( this._bvhData ); this._pathTracer.setTextures( this.textureArray.texture ); + this._pathTracer.setMaterial( this.material ); this.setCamera( this.camera ); this.updateEnvironment(); @@ -53,7 +63,6 @@ export class WebGPUPathTracer { // members this._renderer = renderer; - this._pathTracer = new MegaKernelPathTracer( renderer ); this._timer = new Timer(); this._envColorTexture = new DataTexture( ); @@ -91,6 +100,14 @@ export class WebGPUPathTracer { this.textureArray = new RenderTarget2DArray( 1024, 1024 ); + this._sobolMap = new SobolNumberMapGenerator( renderer, 256 ); + this.rngData = new RNGData(); + this.setPRNGType( RNG_SOBOL_TEXTURE ); + + this.material = new GltfCompliantMaterial(); + this.material.setRNGData( this.rngData ); + this._pathTracer = new MegaKernelPathTracer( renderer, this.rngData ); + // initialize the scene so it doesn't fail this.setScene( new Scene(), new PerspectiveCamera() ); @@ -135,7 +152,7 @@ export class WebGPUPathTracer { } ); // Build TLAS and compute functions - const bvhData = new PathtracerBVHComputeData( scene ); + const bvhData = new PathtracerBVHComputeData( scene, this.rngData ); bvhData.update(); bvhData.useTransparencyRaycastFn( this.textureArray.texture ); @@ -152,12 +169,34 @@ export class WebGPUPathTracer { getMaterial() { - return this._pathTracer.getMaterial(); + return this.material; + + } + + setPRNGType( type ) { + + switch ( type ) { + + case RNG_PCG: + this.rngData.setFunctions( pcgFunctions ); + break; + + case RNG_SOBOL_TEXTURE: + this.rngData.setFunctions( sobolTextureFunctions( this._sobolMap.texture ) ); + break; + + case RNG_SOBOL: + default: + this.rngData.setFunctions( sobolFunctions ); + + } } setMaterial( material ) { + this.material = material; + this.material.setRNGData( this.rngData ); this._pathTracer.setMaterial( material ); } @@ -256,6 +295,19 @@ export class WebGPUPathTracer { } + if ( ! this.material.initialized ) { + + this.material.init( renderer ); + this.material.initialized = true; + + } + + if ( ! this._sobolMap.isGenerated ) { + + this._sobolMap.generate(); + + } + const delta = 1000 * timer.getDelta(); this._resetTime += delta; diff --git a/src/webgpu/compute/PathTracerMegaKernel.js b/src/webgpu/compute/PathTracerMegaKernel.js index 67b92ce7..05648c73 100644 --- a/src/webgpu/compute/PathTracerMegaKernel.js +++ b/src/webgpu/compute/PathTracerMegaKernel.js @@ -2,7 +2,7 @@ import { DataTexture, Matrix3, Matrix4, Vector2, StorageTexture } from 'three/we import { ndcToCameraRay } from '../lib/wgsl/common.wgsl.js'; import { ComputeKernel } from './ComputeKernel.js'; import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl'; -import { pcgRand2, pcgInit } from '../nodes/random.wgsl.js'; +import { RNG_INDEX_RAY_JITTER, RNG_INDEX_ENVIRONMENT_SAMPLE } from '../nodes/random.wgsl.js'; import { getSurfaceRecordFunc } from '../nodes/material.wgsl.js'; import { sampleEnvironmentFn, weightedAlphaBlendFn } from '../nodes/sampling.wgsl.js'; import { proxy, proxyFn } from '../lib/nodes/NodeProxy.js'; @@ -11,11 +11,10 @@ import { isTerminatingScatterFunc } from '../nodes/utils.wgsl.js'; export class PathTracerMegaKernel extends ComputeKernel { - constructor( ) { + constructor( rngData ) { const params = { bvhData: { value: null }, - material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), @@ -54,6 +53,7 @@ export class PathTracerMegaKernel extends ComputeKernel { const raycastFirstHitFn = proxyFn( 'bvhData.value.fns.raycastFirstHit', params ); const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params ); const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params ); + const rng = rngData; const shader = wgslTagFn/* wgsl */` @@ -123,10 +123,11 @@ export class PathTracerMegaKernel extends ComputeKernel { let uv = vec2f( indexUV ) / vec2f( targetDimensions ); let ndc = uv * 2.0 - vec2f( 1.0 ); - ${ pcgInit }( indexUV, seed ); + let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; + ${ rng.init }( pixelIndex, seed, 0 ); // scene ray - var jitter = 2.0 * ${ pcgRand2 }() / vec2f( targetDimensions.xy ); + var jitter = 2.0 * ${ rng.vec2f }( ${ RNG_INDEX_RAY_JITTER } ) / vec2f( targetDimensions.xy ); var ray = ${ ndcToCameraRay }( ndc + jitter, cameraToModelMatrix * inverseProjectionMatrix ); ray.direction = normalize( ray.direction ); @@ -135,6 +136,8 @@ export class PathTracerMegaKernel extends ComputeKernel { for ( var bounce = 0u; bounce < bounces; bounce ++ ) { + ${ rng.nextBounce }(); + var hitResult: ${ raycastOutput }; if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) { @@ -172,13 +175,14 @@ export class PathTracerMegaKernel extends ComputeKernel { } else { + let rng = ${ rng.vec2f }( ${ RNG_INDEX_ENVIRONMENT_SAMPLE } ); if ( bounce > 0u ) { - resultColor += ${ sampleEnvironmentFn }( envMap, envMapSampler, envInfo, ray.direction, pcgRand2() ) * vec4f( throughputColor, 0.0 ); + resultColor += ${ sampleEnvironmentFn }( envMap, envMapSampler, envInfo, ray.direction, rng ) * vec4f( throughputColor, 0.0 ); } else { - resultColor = ${ sampleEnvironmentFn }( background, backgroundSampler, backgroundInfo, ray.direction, pcgRand2() ); + resultColor = ${ sampleEnvironmentFn }( background, backgroundSampler, backgroundInfo, ray.direction, rng ); } diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index 117cd2fc..d1ccd245 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -6,15 +6,15 @@ import { queuedRayStruct, queuedHitStruct } from './structs.js'; import { proxy, proxyFn } from '../../lib/nodes/NodeProxy.js'; import { weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; -import { getPcgSeed, setPcgSeed } from '../../nodes/random.wgsl.js'; import { isTerminatingScatterFunc } from '../../nodes/utils.wgsl.js'; export class ProcessHitsKernel extends ComputeKernel { - constructor( material ) { + constructor( rngData ) { const params = { bvhData: { value: null }, + material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(), @@ -36,6 +36,8 @@ export class ProcessHitsKernel extends ComputeKernel { }; const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params ); + const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params ); + const rng = rngData; const fn = wgslTagFn/* wgsl */` @@ -71,7 +73,8 @@ export class ProcessHitsKernel extends ComputeKernel { let input = hitQueue[ hitIndex ]; let indexUV = vec2u( input.pixel_x, input.pixel_y ); - ${ setPcgSeed }( input.pcgStateS0 ); + let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; + ${ rng.init }( pixelIndex, input.seed, input.currentBounce ); let object = transforms[ input.objectIndex ]; var material = materials[ object.materialIndex ]; @@ -80,13 +83,14 @@ export class ProcessHitsKernel extends ComputeKernel { material.color *= object.color.rgb; material.opacity *= object.color.a; - var vertexData = ${ sampleTrianglePointFn }( input.barycoord, input.indices.xyz ); + let barycoord = vec3( input.barycoord, 1.0 - input.barycoord.x - input.barycoord.y ); + var vertexData = ${ sampleTrianglePointFn }( barycoord, input.indices.xyz ); vertexData.normal = normalize( transpose( object.inverseMatrixWorld ) * vertexData.normal ); vertexData.position = object.matrixWorld * vertexData.position; let surface = ${ getSurfaceRecordFunc }( material, vertexData, input.side, input.normal, textures, textureSampler ); - let scatterRec = ${ material.getBsdfNode() }( input.view, surface ); + let scatterRec = ${ bsdfSampleFn }( input.view, surface ); let resultColor = input.resultColor + vec4f( input.throughputColor * surface.emission, 0.0 ); @@ -110,8 +114,8 @@ export class ProcessHitsKernel extends ComputeKernel { rayQueue[ index ].pixel = indexUV; rayQueue[ index ].throughputColor = input.throughputColor * scatterRec.color / scatterRec.pdf; rayQueue[ index ].currentBounce = input.currentBounce + 1; - rayQueue[ index ].pcgStateS0 = ${ getPcgSeed }(); rayQueue[ index ].resultColor = resultColor; + rayQueue[ index ].seed = input.seed; } diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index 9f562482..f087751f 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -3,13 +3,13 @@ import { IndirectStorageBufferAttribute, StorageTexture } from 'three/webgpu'; import { uniform, storage, globalId, textureStore } from 'three/tsl'; import { ComputeKernel } from '../ComputeKernel.js'; import { ndcToCameraRay } from '../../lib/wgsl/common.wgsl.js'; -import { getPcgSeed, pcgInit, pcgRand2 } from '../../nodes/random.wgsl.js'; +import { RNG_INDEX_RAY_JITTER } from '../../nodes/random.wgsl.js'; import { queuedRayStruct } from './structs.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; export class RayGenerationKernel extends ComputeKernel { - constructor() { + constructor( rngData ) { const params = { cameraToModelMatrix: uniform( new Matrix4() ), @@ -28,6 +28,8 @@ export class RayGenerationKernel extends ComputeKernel { globalId: globalId, }; + const rng = rngData; + const fn = wgslTagFn /* wgsl */` fn compute( cameraToModelMatrix: mat4x4f, @@ -80,10 +82,11 @@ export class RayGenerationKernel extends ComputeKernel { let queueCapacity = arrayLength( rayQueue ); let index = atomicAdd( &queueSizes[ 1 ], 1 ) % queueCapacity; - ${ pcgInit }( indexUV, seed ); + let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; + ${ rng.init }( pixelIndex, seed, 0 ); // write the ray data - var jitter = 2.0 * ${ pcgRand2 }() / vec2f( targetDimensions.xy ); + var jitter = 2.0 * ${ rng.vec2f }( ${ RNG_INDEX_RAY_JITTER } ) / vec2f( targetDimensions.xy ); var ray = ${ ndcToCameraRay }( ndc + jitter, cameraToModelMatrix * inverseProjectionMatrix ); ray.direction = normalize( ray.direction ); @@ -92,8 +95,8 @@ export class RayGenerationKernel extends ComputeKernel { rayQueue[ index ].pixel = indexUV; rayQueue[ index ].throughputColor = vec3f( 1.0 ); rayQueue[ index ].currentBounce = 0; - rayQueue[ index ].pcgStateS0 = ${ getPcgSeed }(); rayQueue[ index ].resultColor = vec4f( 0.0, 0.0, 0.0, 1.0 ); + rayQueue[ index ].seed = seed; // write the active params textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ACTIVE_FLAG | samples ) ); diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js index a7ffd83d..9aeb76cb 100644 --- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js +++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js @@ -1,7 +1,7 @@ import { DataTexture, Matrix3, IndirectStorageBufferAttribute, StorageTexture } from 'three/webgpu'; import { ComputeKernel } from '../ComputeKernel.js'; import { uniform, texture, sampler, storage, textureStore, globalId } from 'three/tsl'; -import { pcgRand2, setPcgSeed } from '../../nodes/random.wgsl.js'; +import { RNG_INDEX_ENVIRONMENT_SAMPLE } from '../../nodes/random.wgsl.js'; import { queuedRayStruct, queuedHitStruct } from './structs.js'; import { proxy } from '../../lib/nodes/NodeProxy.js'; import { sampleEnvironmentFn, weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js'; @@ -9,7 +9,7 @@ import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; export class RayIntersectionKernel extends ComputeKernel { - constructor() { + constructor( rngData ) { const params = { bvhData: { value: null }, @@ -40,6 +40,7 @@ export class RayIntersectionKernel extends ComputeKernel { const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params ); const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params ); + const rng = rngData; const fn = wgslTagFn /* wgsl */` @@ -88,9 +89,9 @@ export class RayIntersectionKernel extends ComputeKernel { let ACTIVE_FLAG = 0xF0000000u; let input = rayQueue[ rayIndex % queueCapacity ]; let indexUV = input.pixel; - let seed = ( textureLoad( ${ params.sampleCountTarget }, indexUV ).r & ( ~ ACTIVE_FLAG ) ) + input.currentBounce; - ${ setPcgSeed }( input.pcgStateS0 ); + let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; + ${ rng.init }( pixelIndex, input.seed, input.currentBounce ); // run intersection let ray = Ray( input.origin, input.direction ); @@ -101,27 +102,28 @@ export class RayIntersectionKernel extends ComputeKernel { let index = atomicAdd( &queueSizes[ 3 ], 1 ); hitQueue[ index ].view = - input.direction; hitQueue[ index ].indices = hitResult.indices.xyz; - hitQueue[ index ].barycoord = hitResult.barycoord; + hitQueue[ index ].barycoord = hitResult.barycoord.xy; hitQueue[ index ].normal = hitResult.normal.xyz; hitQueue[ index ].side = hitResult.side; - hitQueue[ index ].pixel_x = input.pixel.x; - hitQueue[ index ].pixel_y = input.pixel.y; + hitQueue[ index ].pixel_x = indexUV.x; + hitQueue[ index ].pixel_y = indexUV.y; hitQueue[ index ].objectIndex = hitResult.objectIndex; hitQueue[ index ].throughputColor = input.throughputColor; hitQueue[ index ].currentBounce = input.currentBounce; - hitQueue[ index ].pcgStateS0 = input.pcgStateS0; hitQueue[ index ].resultColor = input.resultColor; + hitQueue[ index ].seed = input.seed; } else { + let rng = ${ rng.vec2f }( ${ RNG_INDEX_ENVIRONMENT_SAMPLE } ); var resultColor = input.resultColor; if ( input.currentBounce > 0u ) { - resultColor += ${ sampleEnvironmentFn }( envMap, envMapSampler, envInfo, input.direction, ${ pcgRand2 }() ) * vec4f( input.throughputColor, 0.0 ); + resultColor += ${ sampleEnvironmentFn }( envMap, envMapSampler, envInfo, input.direction, rng ) * vec4f( input.throughputColor, 0.0 ); } else { - resultColor = ${ sampleEnvironmentFn }( background, backgroundSampler, backgroundInfo, input.direction, ${ pcgRand2 }() ); + resultColor = ${ sampleEnvironmentFn }( background, backgroundSampler, backgroundInfo, input.direction, rng ); } diff --git a/src/webgpu/compute/wavefront/structs.js b/src/webgpu/compute/wavefront/structs.js index f7b2bd81..a6d9bf91 100644 --- a/src/webgpu/compute/wavefront/structs.js +++ b/src/webgpu/compute/wavefront/structs.js @@ -3,18 +3,16 @@ import { StructTypeNode } from 'three/webgpu'; export const queuedRayStruct = new StructTypeNode( { origin: 'vec3f', - _alignment0: 'uint', + seed: 'uint', direction: 'vec3f', - _alignment1: 'uint', + _alignment0: 'uint', throughputColor: 'vec3f', currentBounce: 'uint', pixel: 'vec2u', - pcgStateS0: 'vec4u', - resultColor: 'vec4f', }, 'QueuedRay' ); @@ -22,9 +20,10 @@ export const queuedRayStruct = new StructTypeNode( { export const queuedHitStruct = new StructTypeNode( { indices: 'vec3u', - pixel_x: 'uint', + seed: 'uint', - barycoord: 'vec3f', + barycoord: 'vec2f', + pixel_x: 'uint', pixel_y: 'uint', view: 'vec3f', @@ -36,8 +35,6 @@ export const queuedHitStruct = new StructTypeNode( { normal: 'vec3f', side: 'float', - pcgStateS0: 'vec4u', - resultColor: 'vec4f', }, 'QueuedHit' ); diff --git a/src/webgpu/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js index 1c2f44e8..bf5c2a4e 100644 --- a/src/webgpu/materials/GltfCompliantMaterial.js +++ b/src/webgpu/materials/GltfCompliantMaterial.js @@ -2,18 +2,25 @@ import { texture, textureStore, globalId, float } from 'three/tsl'; import { StorageTexture, RedFormat, LinearFilter, TextureLoader, HalfFloatType } from 'three/webgpu'; import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode'; import { PathtracingMaterial } from './PathtracingMaterial'; -import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc, albedoIntegralMetallic, fresnelCoatFunc } from '../nodes/material.wgsl.js'; +import { + specularBrdfFunc, + diffuseBrdfFunc, + fresnelMixFunc, + conductorFresnelFunc, + albedoIntegralMetallic, + fresnelCoatFunc, + MIN_INCIDENT_COS, +} from '../nodes/material.wgsl.js'; import { diffuseDirectionFunc, getLobeWeightsFunc } from '../nodes/sampling.wgsl.js'; import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc } from '../nodes/ggx.wgsl.js'; import { bxdfContextStruct, scatterRecordStruct, surfaceRecordStruct } from '../nodes/structs.wgsl.js'; -import { pcgRand, pcgRand2 } from '../nodes/random.wgsl.js'; +import { RNG_INDEX_SCATTER_DIRECTION, RNG_INDEX_SCATTER_TYPE } from '../nodes/random.wgsl.js'; import { ComputeKernel } from '../compute/ComputeKernel'; const TURQUIN_METAL_URL = new URL( '../../textures/turquinMetal.png', import.meta.url ).toString(); const TURQUIN_METAL_TEXTURE = await new TextureLoader().loadAsync( TURQUIN_METAL_URL ); const CLEARCOAT_IOR = float( 1.5 ); -const MIN_INCIDENT_COS = float( 1e-3 ); export class GltfCompliantMaterial extends PathtracingMaterial { @@ -139,8 +146,9 @@ export class GltfCompliantMaterial extends PathtracingMaterial { cdf.z = weights.clearcoat + cdf.y; cdf.w = 0; // weights.transmission + cdf.z; - let r = ${ pcgRand }() * cdf.z; + let r = ${ this.rng.f32 }( ${ RNG_INDEX_SCATTER_TYPE } ) * cdf.z; + let directionUV = ${ this.rng.vec2f }( ${ RNG_INDEX_SCATTER_DIRECTION } ); var wi: vec3f; var wiClearcoat: vec3f; var wh: vec3f; @@ -148,7 +156,7 @@ export class GltfCompliantMaterial extends PathtracingMaterial { if ( r <= cdf.x ) { // diffuse - wi = ${ diffuseDirectionFunc }( wo, surf ); + wi = ${ diffuseDirectionFunc }( wo, directionUV ); wh = normalize( wi + wo ); wiClearcoat = normalize( invClearcoatBasis * normalBasis * wi ); @@ -156,7 +164,7 @@ export class GltfCompliantMaterial extends PathtracingMaterial { } else if ( r <= cdf.y ) { // specular - wh = ${ ggxDirectionFunc }( wo, vec2( alpha ), ${ pcgRand2 }() ); + wh = ${ ggxDirectionFunc }( wo, vec2( alpha ), directionUV ); wi = - normalize( reflect( wo, wh ) ); wiClearcoat = normalize( invClearcoatBasis * normalBasis * wi ); @@ -164,7 +172,7 @@ export class GltfCompliantMaterial extends PathtracingMaterial { } else if ( r <= cdf.z ) { // clearcoat - whClearcoat = ${ ggxDirectionFunc }( woClearcoat, vec2( clearcoatAlpha ), ${ pcgRand2 }() ); + whClearcoat = ${ ggxDirectionFunc }( woClearcoat, vec2( clearcoatAlpha ), directionUV ); wiClearcoat = - normalize( reflect( woClearcoat, whClearcoat ) ); wi = normalize( invBasis * clearcoatBasis * wiClearcoat ); diff --git a/src/webgpu/materials/PathtracingMaterial.js b/src/webgpu/materials/PathtracingMaterial.js index 506c1c51..913ae7b9 100644 --- a/src/webgpu/materials/PathtracingMaterial.js +++ b/src/webgpu/materials/PathtracingMaterial.js @@ -1,10 +1,18 @@ -import { lambertBsdfFunc } from '../nodes/material.wgsl.js'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; +import { RNG_INDEX_SCATTER_DIRECTION } from '../nodes/random.wgsl.js'; +import { diffuseDirectionFunc } from '../nodes/sampling.wgsl.js'; /** * Defines a material sampled by the pathtracer */ export class PathtracingMaterial { + constructor( ) { + + this.rng = null; + + } + /** * * Called once per material @@ -23,7 +31,29 @@ export class PathtracingMaterial { */ getBsdfNode() { - return lambertBsdfFunc; + return wgslTagFn` + + fn bsdfSample( worldWo: vec3f, surf: SurfaceRecord ) -> ScatterRecord { + + var record: ScatterRecord; + + let wo = normalize( surf.normalInvBasis * worldWo ); + let wi = ${ diffuseDirectionFunc }( wo, ${ this.rng.vec2f }( ${ RNG_INDEX_SCATTER_DIRECTION } ) ); + record.color = surf.color * max( wi.z, 0.0 ); + record.pdf = max( wi.z, 0.0 ) / PI; + record.direction = normalize( surf.normalBasis * wi ); + + return record; + + } + + `; + + } + + setRNGData( rngData ) { + + this.rng = rngData; } diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index c1fb04f6..da64a46e 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -6,7 +6,7 @@ import { materialStruct } from './structs.wgsl.js'; import { getTextureHash } from '../../core/utils/sceneUpdateUtils.js'; import { bvhNodeBoundsStruct, bvhNodeStruct, rayStruct } from '../lib/wgsl/structs.wgsl.js'; import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; -import { pcgRand } from './random.wgsl.js'; +import { RNG_INDEX_ALPHA_TEST } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; const _colorVec = new Vector4(); @@ -23,7 +23,7 @@ const transformStruct = new StructTypeNode( { // Pathtracer-specific version of the BVHComputeData tht includes material mapping, property structs export class PathtracerBVHComputeData extends BVHComputeData { - constructor( bvh, options = {} ) { + constructor( bvh, rngObject, options = {} ) { // TODO: once supported we should use the appropriately-sized member sizes super( bvh, { @@ -42,9 +42,11 @@ export class PathtracerBVHComputeData extends BVHComputeData { this.storage.materials = null; this.materials = []; this.bvhMap = new Map(); + this.rng = rngObject; } + useTransparencyRaycastFn( textures ) { const texturesNode = texture( textures ); @@ -169,7 +171,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { } - if ( material.transparent != 0 && opacity < ${ pcgRand }() ) { + if ( material.transparent != 0 && opacity < ${ this.rng.f32 }( ${ RNG_INDEX_ALPHA_TEST } + ti ) ) { continue; diff --git a/src/webgpu/nodes/material.wgsl.js b/src/webgpu/nodes/material.wgsl.js index a8b964f2..7ebb92bf 100644 --- a/src/webgpu/nodes/material.wgsl.js +++ b/src/webgpu/nodes/material.wgsl.js @@ -1,4 +1,4 @@ -import { wgslFn } from 'three/tsl'; +import { wgslFn, float } from 'three/tsl'; import { inverseMat3x3Func, getBasisFromNormalFunc, @@ -13,9 +13,12 @@ import { ggxDirectionFunc, ggxReflectionAdjustedPDFFunc, } from './ggx.wgsl.js'; -import { constants, surfaceRecordStruct, scatterRecordStruct } from './structs.wgsl.js'; -import { sampleSphereCosineFn } from './sampling.wgsl.js'; -import { pcgInit, pcgRand2 } from './random.wgsl.js'; +import { constants, surfaceRecordStruct } from './structs.wgsl.js'; +import { pcgFunctions } from './random.wgsl.js'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; + +// Cook-Torrance BRDF in this file expects NdotV and NdotL to be bigger than this constant +export const MIN_INCIDENT_COS = float( 1e-3 ); export const getSurfaceRecordFunc = wgslFn( /* wgsl */ ` @@ -271,24 +274,6 @@ export const getSurfaceRecordFunc = wgslFn( /* wgsl */ ` constants, ] ); -export const lambertBsdfFunc = wgslFn( /* wgsl */` - - fn bsdfSample( worldWo: vec3f, surf: SurfaceRecord ) -> ScatterRecord { - - var record: ScatterRecord; - - // Return bsdfValue / pdf, not bsdfValue and pdf separatly? - let res = sampleSphereCosine( pcgRand2(), surf.normal ); - record.direction = res.xyz; - record.pdf = res.w; - record.color = surf.color * dot( record.direction, surf.normal ) / PI; - - return record; - - } - -`, [ scatterRecordStruct, sampleSphereCosineFn, pcgRand2, constants, surfaceRecordStruct ] ); - /* * * N : Macronormal of the surface @@ -383,7 +368,7 @@ export const fresnelCoatFunc = wgslFn( /* wgsl */ ` // GGX Multibounce compensation using Turquin's method -export const albedoIntegralMetallic = wgslFn( /* wgsl */ ` +export const albedoIntegralMetallic = wgslTagFn/* wgsl */ ` fn albedo( texture: texture_storage_2d, @@ -392,7 +377,8 @@ export const albedoIntegralMetallic = wgslFn( /* wgsl */ ` ) -> void { const INTEGRATION_SAMPLES = ( 1 << 20 ); - pcgInitialize( globalId.xy, 0 ); + let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; + ${ pcgFunctions.init }( pixelIndex, 0, 0 ); let dimensions = textureDimensions( texture ).xy; let uv = ( vec2f( globalId.xy ) + vec2f( 0.5 ) ) / vec2f( dimensions ); @@ -407,15 +393,15 @@ export const albedoIntegralMetallic = wgslFn( /* wgsl */ ` var result = 0.0; for ( var i = 0; i < INTEGRATION_SAMPLES; i++ ) { - let wh = ggxDirection( wo, vec2( alpha ), pcgRand2() ); + let wh = ${ ggxDirectionFunc }( wo, vec2( alpha ), ${ pcgFunctions.vec2f }() ); var wi = - reflect( wo, wh ); - let NdotV = max( wo.z, EPSILON ); - let NdotL = saturate( wi.z ); + let NdotV = max( wo.z, ${ MIN_INCIDENT_COS } ); + let NdotL = max( wi.z, ${ MIN_INCIDENT_COS } ); let NdotH = saturate( wh.z ); - let specular = specularBrdf( NdotL, NdotV, NdotH, alpha ); - let pdf = ggxReflectionAdjustedPDF( NdotV, NdotH, alpha ); + let specular = ${ specularBrdfFunc }( NdotL, NdotV, NdotH, alpha ); + let pdf = ${ ggxReflectionAdjustedPDFFunc }( NdotV, NdotH, alpha ); var weight = 0.0; if ( pdf != 0.0 ) { @@ -432,4 +418,4 @@ export const albedoIntegralMetallic = wgslFn( /* wgsl */ ` } -`, [ pcgInit, pcgRand2, constants, specularBrdfFunc, ggxDirectionFunc, ggxReflectionAdjustedPDFFunc ] ); +`; diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index d9315338..f6a7337b 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -1,40 +1,37 @@ -import { wgsl, wgslFn } from 'three/tsl'; +import { uint, float, wgsl, wgslFn, texture } from 'three/tsl'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode'; + +// Alpha test should be the last index as it is summed with triangle index +export const RNG_INDEX_RAY_JITTER = 0; +export const RNG_INDEX_ENVIRONMENT_SAMPLE = 1; +export const RNG_INDEX_SCATTER_TYPE = 2; +export const RNG_INDEX_SCATTER_DIRECTION = 3; +export const RNG_INDEX_ALPHA_TEST = 4; export const pcgStateStruct = wgsl( /* wgsl */` + struct PcgState { s0: vec4u, s1: vec4u, - pixel: vec2i, }; var g_state: PcgState; ` ); -export const pcgInit = wgslFn( /* wgsl */` - fn pcgInitialize(p: vec2u, frame: u32) -> void { - g_state.pixel = vec2i( p ); +const pcgInit = wgslFn( /* wgsl */` + fn pcgInitialize( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { + let pixel = vec2( ( pixelIndex >> 16 ) & 0xFF, pixelIndex & 0xFF ); //white noise seed - g_state.s0 = vec4u(p, frame, u32(p.x) + u32(p.y)); + g_state.s0 = vec4u( pixel | vec2( bounceIndex << 16 ), pathIndex, pixel.x + pixel.y); //blue noise seed - g_state.s1 = vec4u(frame, frame*15843, frame*31 + 4566, frame*2345 + 58585); - } -`, [ pcgStateStruct ] ); + g_state.s1 = vec4u(pathIndex, pathIndex*15843, pathIndex*31 + 4566, pathIndex*2345 + 58585); -export const getPcgSeed = wgslFn( /* wgsl */` - fn pcgGetSeed() -> vec4u { - return g_state.s0; } `, [ pcgStateStruct ] ); -export const setPcgSeed = wgslFn( /* wgsl */` - fn pcgSetSeed( s0: vec4u ) -> void { - g_state.s0 = s0; - } -`, [ pcgStateStruct ] ); - -export const pcg4d = wgslFn( /* wgsl */ ` +const pcg4d = wgslFn( /* wgsl */ ` fn pcg4d(v: ptr) -> void { *v = *v * 1664525u + 1013904223u; *v = *v + v.yzxy * v.wxyz; @@ -43,23 +40,375 @@ export const pcg4d = wgslFn( /* wgsl */ ` } ` ); -export const pcgRand = wgslFn( /*wgsl*/` - fn pcgRand() -> f32 { +const pcgRand = wgslFn( /*wgsl*/` + fn pcgRand( _id: u32 ) -> f32 { pcg4d(&g_state.s0); return abs( f32( g_state.s0.x ) / f32(0xffffffffu) ); } `, [ pcg4d, pcgStateStruct ] ); -export const pcgRand2 = wgslFn( /*wgsl*/` - fn pcgRand2() -> vec2f { +const pcgRand2 = wgslFn( /*wgsl*/` + fn pcgRand2( _id: u32 ) -> vec2f { pcg4d(&g_state.s0); - return abs( vec2f(g_state.s0.xy) / f32(0xffffffffu) ); + return abs( vec2f( g_state.s0.xy ) / f32(0xffffffffu) ); } `, [ pcg4d, pcgStateStruct ] ); -export const pcgRand3 = wgslFn( /*wgsl*/` - fn pcgRand3() -> vec3f { +const pcgRand3 = wgslFn( /*wgsl*/` + fn pcgRand3( _id: u32 ) -> vec3f { pcg4d(&g_state.s0); - return abs( vec3f(g_state.s0.xyz) / f32(0xffffffffu) ); + return abs( vec3f( g_state.s0.xyz ) / f32(0xffffffffu) ); } `, [ pcg4d, pcgStateStruct ] ); + +const pcgRand4 = wgslFn( /*wgsl*/` + fn pcgRand4( _id: u32 ) -> vec4f { + pcg4d(&g_state.s0); + return abs( vec4f( g_state.s0 ) / f32(0xffffffffu) ); + } +`, [ pcg4d, pcgStateStruct ] ); + +export const pcgFunctions = { + init: pcgInit, + nextBounce: wgslFn( /* wgsl */ 'fn noop() -> void {}' ), + f32: pcgRand, + vec2f: pcgRand2, + vec3f: pcgRand3, + vec4f: pcgRand4, +}; + +// References +// - https://jcgt.org/published/0009/04/01/ +// - Code from https://www.shadertoy.com/view/WtGyDm +// Ported from WebGL version at sobol.glsl.js + +const SOBOL_MAX_POINTS = uint( 256 * 256 ); +const SOBOL_FACTOR = float( 1.0 / 16777216.0 ); + +const sobolConstants = wgsl( /* wgsl */ ` + + const SOBOL_DIRECTIONS_1 = array( + 0x80000000u, 0xc0000000u, 0xa0000000u, 0xf0000000u, + 0x88000000u, 0xcc000000u, 0xaa000000u, 0xff000000u, + 0x80800000u, 0xc0c00000u, 0xa0a00000u, 0xf0f00000u, + 0x88880000u, 0xcccc0000u, 0xaaaa0000u, 0xffff0000u, + 0x80008000u, 0xc000c000u, 0xa000a000u, 0xf000f000u, + 0x88008800u, 0xcc00cc00u, 0xaa00aa00u, 0xff00ff00u, + 0x80808080u, 0xc0c0c0c0u, 0xa0a0a0a0u, 0xf0f0f0f0u, + 0x88888888u, 0xccccccccu, 0xaaaaaaaau, 0xffffffffu + ); + + const SOBOL_DIRECTIONS_2 = array( + 0x80000000u, 0xc0000000u, 0x60000000u, 0x90000000u, + 0xe8000000u, 0x5c000000u, 0x8e000000u, 0xc5000000u, + 0x68800000u, 0x9cc00000u, 0xee600000u, 0x55900000u, + 0x80680000u, 0xc09c0000u, 0x60ee0000u, 0x90550000u, + 0xe8808000u, 0x5cc0c000u, 0x8e606000u, 0xc5909000u, + 0x6868e800u, 0x9c9c5c00u, 0xeeee8e00u, 0x5555c500u, + 0x8000e880u, 0xc0005cc0u, 0x60008e60u, 0x9000c590u, + 0xe8006868u, 0x5c009c9cu, 0x8e00eeeeu, 0xc5005555u + ); + + const SOBOL_DIRECTIONS_3 = array( + 0x80000000u, 0xc0000000u, 0x20000000u, 0x50000000u, + 0xf8000000u, 0x74000000u, 0xa2000000u, 0x93000000u, + 0xd8800000u, 0x25400000u, 0x59e00000u, 0xe6d00000u, + 0x78080000u, 0xb40c0000u, 0x82020000u, 0xc3050000u, + 0x208f8000u, 0x51474000u, 0xfbea2000u, 0x75d93000u, + 0xa0858800u, 0x914e5400u, 0xdbe79e00u, 0x25db6d00u, + 0x58800080u, 0xe54000c0u, 0x79e00020u, 0xb6d00050u, + 0x800800f8u, 0xc00c0074u, 0x200200a2u, 0x50050093u + ); + + const SOBOL_DIRECTIONS_4 = array( + 0x80000000u, 0x40000000u, 0x20000000u, 0xb0000000u, + 0xf8000000u, 0xdc000000u, 0x7a000000u, 0x9d000000u, + 0x5a800000u, 0x2fc00000u, 0xa1600000u, 0xf0b00000u, + 0xda880000u, 0x6fc40000u, 0x81620000u, 0x40bb0000u, + 0x22878000u, 0xb3c9c000u, 0xfb65a000u, 0xddb2d000u, + 0x78022800u, 0x9c0b3c00u, 0x5a0fb600u, 0x2d0ddb00u, + 0xa2878080u, 0xf3c9c040u, 0xdb65a020u, 0x6db2d0b0u, + 0x800228f8u, 0x400b3cdcu, 0x200fb67au, 0xb00ddb9du + ); + +` ); + +const sobolPixelIndex = uint( 0 ).toVar( 'sobolPixelIndex' ); +const sobolPathIndex = uint( 0 ).toVar( 'sobolPathIndex' ); +const sobolBounceIndex = uint( 0 ).toVar( 'sobolBounceIndex' ); + +const getMaskedSobolFunc = wgslFn( /* wgsl */ ` + + fn getMaskedSobol( index: u32, directions: array ) -> u32 { + + var X = 0u; + for ( var bit = 0u; bit < 32u; bit ++ ) { + + let mask = ( index >> bit ) & 1u; + X ^= mask * directions[ bit ]; + + } + return X; + + } + +` ); + +// functions to generate multi-dimensions variables of the same functions +// to support 1, 2, 3, and 4 dimensional sobol sampling. +const sobolScrambleNodesGenerator = ( dim = 1 ) => { + + if ( dim <= 0 ) { + + return; + + } + + const type = dim > 1 ? `vec${dim}u` : 'u32'; + + const sobolReverseBitsFunc = wgslFn( /* wgsl */ ` + + fn sobolReverseBits_${type}( in: ${type} ) -> ${type} { + + var x = in; + x = ( ( ( x & ${type}( 0xaaaaaaaau ) ) >> ${type}( 1 ) ) | ( ( x & ${type}( 0x55555555u ) ) << ${type}( 1 ) ) ); + x = ( ( ( x & ${type}( 0xccccccccu ) ) >> ${type}( 2 ) ) | ( ( x & ${type}( 0x33333333u ) ) << ${type}( 2 ) ) ); + x = ( ( ( x & ${type}( 0xf0f0f0f0u ) ) >> ${type}( 4 ) ) | ( ( x & ${type}( 0x0f0f0f0fu ) ) << ${type}( 4 ) ) ); + x = ( ( ( x & ${type}( 0xff00ff00u ) ) >> ${type}( 8 ) ) | ( ( x & ${type}( 0x00ff00ffu ) ) << ${type}( 8 ) ) ); + return ( ( x >> ${type}( 16 ) ) | ( x << ${type}( 16 ) ) ); + + } + + ` ); + + const sobolHashCombineFunc = wgslFn( /* wgsl */ ` + + fn sobolHashCombine_${type}( seed: u32, v: ${type} ) -> ${type} { + + return ${type}( seed ) ^ ( v + ${ type }( ( seed << 6 ) + ( seed >> 2 ) ) ); + + } + + ` ); + + const sobolLaineKarrasPermutationFunc = wgslFn( /* wgsl */ ` + + fn sobolLaineKarrasPermutation_${type}( in: ${type}, seed: ${type} ) -> ${type} { + + var x = in; + x += seed; + x ^= x * 0x6c50b47cu; + x ^= x * 0xb82f1e52u; + x ^= x * 0xc7afe638u; + x ^= x * 0x8d22f6e6u; + return x; + + } + + ` ); + + const nestedUniformScrambleBase2Func = wgslTagFn/* wgsl */ ` + + fn nestedUniformScrambleBase2_${type}( x: ${type}, seed: ${type} ) -> ${type} { + + var res = ${ sobolLaineKarrasPermutationFunc }( x, seed ); + res = ${ sobolReverseBitsFunc }( res ); + return res; + + } + + `; + + return { + reverseBits: sobolReverseBitsFunc, + hashCombine: sobolHashCombineFunc, + lkPermutation: sobolLaineKarrasPermutationFunc, + scramble: nestedUniformScrambleBase2Func, + }; + +}; + +// 0th node is intentionally empty to make access pattern more intuitive: +// sobolNodes[ i ] are nodes that are needed to sample i-dimensional vectors +// 1-dimensional vector = f32 +const sobolNodes = Array.from( { length: 5 }, ( _, i ) => sobolScrambleNodesGenerator( i ) ); + +export const generateSobolPointFunc = wgslTagFn` + ${ [ sobolConstants ] } + + fn generateSobolPoint( id: u32 ) -> vec4f { + + var index = id; + if ( index >= ${ SOBOL_MAX_POINTS } ) { + + index = index % ${ SOBOL_MAX_POINTS }; + // return vec4( 0.0 ); + + } + + // NOTE: this sobol "direction" is also available but we can't write out 5 components + // uint x = index & 0x00ffffffu; + let x = ${ sobolNodes[ 1 ].reverseBits }( ${ getMaskedSobolFunc }( index, SOBOL_DIRECTIONS_1 ) ) & 0x00ffffffu; + let y = ${ sobolNodes[ 1 ].reverseBits }( ${ getMaskedSobolFunc }( index, SOBOL_DIRECTIONS_2 ) ) & 0x00ffffffu; + let z = ${ sobolNodes[ 1 ].reverseBits }( ${ getMaskedSobolFunc }( index, SOBOL_DIRECTIONS_3 ) ) & 0x00ffffffu; + let w = ${ sobolNodes[ 1 ].reverseBits }( ${ getMaskedSobolFunc }( index, SOBOL_DIRECTIONS_4 ) ) & 0x00ffffffu; + + return vec4( f32( x ), f32( y ), f32( z ), f32( w ) ) * ${ SOBOL_FACTOR }; + + } + +`; + +const sobolHashFunc = wgslFn( /* wgsl */ ` + + fn sobolHash( in: u32 ) -> u32 { + + var x = in; + // finalizer from murmurhash3 + x ^= x >> 16; + x *= 0x85ebca6bu; + x ^= x >> 13; + x *= 0xc2b2ae35u; + x ^= x >> 16; + return x; + + } + +` ); + +const sobolGetSeedFunc = wgslTagFn` + + fn sobolGetSeed( effect: u32 ) -> u32 { + + return ${ sobolHashFunc }( + ${ sobolNodes[ 1 ].hashCombine }( + ${ sobolNodes[ 1 ].hashCombine }( + ${ sobolHashFunc }( ${ sobolBounceIndex } ), + ${ sobolPixelIndex } + ), + effect + ) + ); + + } + +`; + +const sobolGenerator = ( dim = 1, sobolPointFunc = generateSobolPointFunc ) => { + + if ( dim <= 0 ) { + + return; + + } + + const utype = dim > 1 ? `vec${dim}u` : 'u32'; + const ftype = dim > 1 ? `vec${dim}f` : 'f32'; + + let components = '.r'; + let combineValues = '1u'; + if ( dim === 2 ) { + + components = '.rg'; + combineValues = 'vec2( 1u, 2u )'; + + } else if ( dim === 3 ) { + + components = '.rgb'; + combineValues = 'vec3( 1u, 2u, 3u )'; + + } else if ( dim === 4 ) { + + components = ''; + combineValues = 'vec4( 1u, 2u, 3u, 4u )'; + + } + + return wgslTagFn` + + fn sobol${dim}( effect: u32 ) -> ${ftype} { + + let seed = ${sobolGetSeedFunc}( effect ); + let index = ${sobolPathIndex}; + + let shuffle_seed = ${ sobolNodes[ 1 ].hashCombine }( seed, 0u ); + let shuffled_index = ${ sobolNodes[ 1 ].scramble }( ${ sobolNodes[ 1 ].reverseBits }( index ), shuffle_seed ); + let sobol_pt = ${ sobolPointFunc }( shuffled_index )${ components }; + var result = ${ utype }( sobol_pt * 16777216.0 ); + + let seed2 = ${ sobolNodes[ dim ].hashCombine }( seed, ${ combineValues } ); + result = ${ sobolNodes[ dim ].scramble }( result, seed2 ); + + return ${ SOBOL_FACTOR } * ${ ftype }( result >> ${utype}( 8 ) ); + + } + + `; + +}; + +export const sobolInitFunc = wgslTagFn` + + fn sobolInit( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { + + ${ sobolPixelIndex } = pixelIndex; + ${ sobolPathIndex } = pathIndex; + ${ sobolBounceIndex } = bounceIndex; + + } + +`; + +export const sobolNextBounceFunc = wgslTagFn` + + fn sobolNextBounce() -> void { + + ${ sobolBounceIndex }++; + + } + +`; + +export const sobolFunctions = { + init: sobolInitFunc, + nextBounce: sobolNextBounceFunc, + f32: sobolGenerator( 1 ), + vec2f: sobolGenerator( 2 ), + vec3f: sobolGenerator( 3 ), + vec4f: sobolGenerator( 4 ), +}; + +export const sobolTextureFunctions = ( sobolTexture ) => { + + const textureNode = texture( sobolTexture ); + const sampleTextureFunc = wgslTagFn/* wgsl */` + + fn sampleSobolPoint( id: u32 ) -> vec4f { + + var index = id; + if ( index >= ${ SOBOL_MAX_POINTS } ) { + + index = index % ${ SOBOL_MAX_POINTS }; + + } + + let dim = textureDimensions( ${ textureNode } ); + let y = index / dim.x; + let x = index - y * dim.x; + + return textureLoad( ${ textureNode }, vec2( x, y ), 0 ); + + } + + `; + + return { + init: sobolInitFunc, + nextBounce: sobolNextBounceFunc, + f32: sobolGenerator( 1, sampleTextureFunc ), + vec2f: sobolGenerator( 2, sampleTextureFunc ), + vec3f: sobolGenerator( 3, sampleTextureFunc ), + vec4f: sobolGenerator( 4, sampleTextureFunc ), + }; + +}; diff --git a/src/webgpu/nodes/sampling.wgsl.js b/src/webgpu/nodes/sampling.wgsl.js index ddd883f0..101af469 100644 --- a/src/webgpu/nodes/sampling.wgsl.js +++ b/src/webgpu/nodes/sampling.wgsl.js @@ -1,6 +1,5 @@ import { wgslFn } from 'three/tsl'; import { environmentInfoStruct, constants, lobeWeightsStruct } from './structs.wgsl.js'; -import { pcgRand2 } from './random.wgsl.js'; import { evaluateFresnelFunc, iorToF0Func, schlickFresnelFunc } from './utils.wgsl.js'; /* @@ -11,26 +10,6 @@ Vectors above are assumed to be in tangent space. i.e. +z is along macronormal o 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! -// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf -// 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; - let b = sqrt( 1 - a * a ) * 0.99999; - let phi = 2 * PI * rng.y; - let direction = normalize( vec3f(n.x + b * cos( phi ), n.y + b * sin( phi ), n.z + a) ); - let pdf = dot( direction, n ) / PI; - - return vec4f( direction, pdf ); - } - -`, [ constants ] ); - export const sampleSphereFunc = wgslFn( /* wgsl */ ` fn sampleSphere( uv: vec2f ) -> vec3f { @@ -45,11 +24,13 @@ export const sampleSphereFunc = wgslFn( /* wgsl */ ` `, [ constants ] ); +// TODO: Investigate sampling directly in tagent space? +// See 16.6.1 in https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf export const diffuseDirectionFunc = wgslFn( /* wgsl */ ` - fn diffuseDirection( wo: vec3f, surf: SurfaceRecord ) -> vec3f { + fn diffuseDirection( wo: vec3f, uv: vec2f ) -> vec3f { - var lightDirection = sampleSphere( pcgRand2() ); + var lightDirection = sampleSphere( uv ); lightDirection.z += 1.0; lightDirection = normalize( lightDirection ); @@ -57,7 +38,7 @@ export const diffuseDirectionFunc = wgslFn( /* wgsl */ ` } -`, [ sampleSphereFunc, pcgRand2 ] ); +`, [ sampleSphereFunc ] ); export const getLobeWeightsFunc = wgslFn( /* wgsl */ `