From bc7051ae1f89c0445e6a2d2be392b791e438452c Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:50:02 +0700 Subject: [PATCH 01/12] Implement sobol random number generation --- src/webgpu/nodes/random.wgsl.js | 298 +++++++++++++++++++++++++++++++- 1 file changed, 297 insertions(+), 1 deletion(-) diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index d9315338..513d496b 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -1,4 +1,5 @@ -import { wgsl, wgslFn } from 'three/tsl'; +import { uint, wgsl, wgslFn } from 'three/tsl'; +import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode'; export const pcgStateStruct = wgsl( /* wgsl */` struct PcgState { @@ -63,3 +64,298 @@ export const pcgRand3 = wgslFn( /*wgsl*/` return abs( vec3f(g_state.s0.xyz) / f32(0xffffffffu) ); } `, [ pcg4d, pcgStateStruct ] ); + +// References +// - https://jcgt.org/published/0009/04/01/ +// - Code from https://www.shadertoy.com/view/WtGyDm +// Ported from WebGL version at sobol.glsl.js + +export const SOBOL_INDEX_RAY_JITTER = 0; +export const SOBOL_INDEX_ENVIRONMENT_SAMPLE = 1; +export const SOBOL_INDEX_SCATTER_TYPE = 2; +export const SOBOL_INDEX_SCATTER_DIRECTION = 3; + +const sobolConstants = wgsl( /* wgsl */ ` + + const SOBOL_FACTOR: f32 = 1.0 / 16777216.0; + const SOBOL_MAX_POINTS: u32 = 256u * 256u; + + 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 ) ); + +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 ) => { + + 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 = ${ generateSobolPointFunc }( shuffled_index )${ components }; + // TODO: cache sobol point in a texture + // let sobol_pt = sobolGetTexturePoint( 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 sobolInit = wgslTagFn` + + fn sobolInit( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { + + ${ sobolPixelIndex } = pixelIndex; + ${ sobolPathIndex } = pathIndex; + ${ sobolBounceIndex } = bounceIndex; + + } + +`; + +// 0th node is intentionally empty to make access pattern more intuitive: +// sobolFuncs[ i ] is a function node to sample i-dimensional vector +// 1-dimensional vector = f32 +export const sobolFuncs = Array.from( { length: 5 }, ( _, i ) => sobolGenerator( i ) ); + From bf56f175dfbfdaf3bdd84dfa8763f1988599aad1 Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:57:41 +0700 Subject: [PATCH 02/12] Use sobol numbers in megakernel --- src/webgpu/compute/PathTracerMegaKernel.js | 14 +++++++++----- src/webgpu/materials/GltfCompliantMaterial.js | 11 ++++++----- src/webgpu/nodes/sampling.wgsl.js | 6 +++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/webgpu/compute/PathTracerMegaKernel.js b/src/webgpu/compute/PathTracerMegaKernel.js index 67b92ce7..5519a125 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 { pcgRand2, pcgInit, sobolInit, sobolFuncs, SOBOL_INDEX_RAY_JITTER, SOBOL_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'; @@ -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; + ${ sobolInit }( pixelIndex, seed, 0 ); // scene ray - var jitter = 2.0 * ${ pcgRand2 }() / vec2f( targetDimensions.xy ); + var jitter = 2.0 * ${ sobolFuncs[ 2 ] }( ${ SOBOL_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 ++ ) { + ${ sobolInit }( pixelIndex, seed, bounce ); + var hitResult: ${ raycastOutput }; if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) { @@ -172,13 +175,14 @@ export class PathTracerMegaKernel extends ComputeKernel { } else { + let rng = ${ sobolFuncs[ 2 ] }( ${ SOBOL_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/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js index f4aa3a04..37875027 100644 --- a/src/webgpu/materials/GltfCompliantMaterial.js +++ b/src/webgpu/materials/GltfCompliantMaterial.js @@ -6,7 +6,7 @@ import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc 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 { SOBOL_INDEX_SCATTER_DIRECTION, SOBOL_INDEX_SCATTER_TYPE, sobolFuncs } from '../nodes/random.wgsl.js'; import { ComputeKernel } from '../compute/ComputeKernel'; const TURQUIN_METAL_URL = new URL( '../../textures/turquinMetal.png', import.meta.url ).toString(); @@ -139,8 +139,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 = ${ sobolFuncs[ 1 ] }( ${ SOBOL_INDEX_SCATTER_TYPE } ) * cdf.z; + let directionUV = ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_SCATTER_DIRECTION } ); var wi: vec3f; var wiClearcoat: vec3f; var wh: vec3f; @@ -148,7 +149,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 +157,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 = - reflect( wo, wh ); wiClearcoat = normalize( invClearcoatBasis * normalBasis * wi ); @@ -164,7 +165,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 = - reflect( woClearcoat, whClearcoat ); wi = normalize( invBasis * clearcoatBasis * wiClearcoat ); diff --git a/src/webgpu/nodes/sampling.wgsl.js b/src/webgpu/nodes/sampling.wgsl.js index ddd883f0..685e1e3f 100644 --- a/src/webgpu/nodes/sampling.wgsl.js +++ b/src/webgpu/nodes/sampling.wgsl.js @@ -47,9 +47,9 @@ export const sampleSphereFunc = wgslFn( /* wgsl */ ` 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 +57,7 @@ export const diffuseDirectionFunc = wgslFn( /* wgsl */ ` } -`, [ sampleSphereFunc, pcgRand2 ] ); +`, [ sampleSphereFunc ] ); export const getLobeWeightsFunc = wgslFn( /* wgsl */ ` From e0662cc2c8f05cb8f6167471c0cdde633fa1dc05 Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:09:17 +0700 Subject: [PATCH 03/12] Use sobol random numbers in wavefront pathtracer --- src/webgpu/WaveFrontPathTracer.js | 8 +++++++- .../compute/wavefront/ProcessHitsKernel.js | 9 ++++++--- .../compute/wavefront/RayGenerationKernel.js | 8 ++++---- .../wavefront/RayIntersectionKernel.js | 20 +++++++++++-------- src/webgpu/compute/wavefront/structs.js | 4 ---- src/webgpu/nodes/PathtracerBVHComputeData.js | 4 ++-- src/webgpu/nodes/random.wgsl.js | 2 ++ src/webgpu/nodes/sampling.wgsl.js | 1 - 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index 25f43f8b..df276f40 100644 --- a/src/webgpu/WaveFrontPathTracer.js +++ b/src/webgpu/WaveFrontPathTracer.js @@ -25,6 +25,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { // options this.tiles = new Vector2( 3, 3 ); this.envInfo = new EquirectHdrInfoUniform(); + this.seed = 0; // queues this.rayQueue = new IndirectStorageBufferAttribute( MAX_RAY_COUNT, queuedRayStruct.getLength() ); @@ -172,6 +173,8 @@ export class WaveFrontPathTracer extends PathTracerBackend { super.reset(); + this.seed = 0; + const { width, height } = sampleCountTarget; const dispatchSize = sampleCountClearKernel.getDispatchSize( width, height ); @@ -257,7 +260,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { primeRayGenerationDispatchKernel.outputDispatch = rayGenerationDispatch; // set up the ray generation kernel - enqueueRaysKernel.seed ++; + enqueueRaysKernel.seed = this.seed; enqueueRaysKernel.cameraToModelMatrix.copy( camera.matrixWorld ); enqueueRaysKernel.inverseProjectionMatrix.copy( camera.projectionMatrixInverse ); enqueueRaysKernel.tileIndexBuffer = tileIndexBuffer; @@ -282,6 +285,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { rayIntersectionKernel.rayQueue = rayQueue; rayIntersectionKernel.queueSizes = queueSizes; rayIntersectionKernel.hitQueue = hitQueue; + rayIntersectionKernel.seed = this.seed; renderer.compute( rayIntersectionKernel.kernel, intersectDispatch ); // mark the rays as consumed @@ -298,6 +302,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { hitProcessKernel.rayQueue = rayQueue; hitProcessKernel.queueSizes = queueSizes; hitProcessKernel.hitQueue = hitQueue; + hitProcessKernel.seed = this.seed; renderer.compute( hitProcessKernel.kernel, hitProcessKernel.getDispatchSize( processed, 1, 1 ) ); // Note: hit queue size ([2] and [3]) is reset at the top of the next iteration by PrimeRayGenerationDispatchKernel @@ -312,6 +317,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { samples += samplesPerIteration; this.samples = Math.floor( samples ); + this.seed ++; } diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index 117cd2fc..29afdd40 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -6,8 +6,8 @@ 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'; +import { sobolInit } from '../../nodes/random.wgsl.js'; export class ProcessHitsKernel extends ComputeKernel { @@ -23,6 +23,7 @@ export class ProcessHitsKernel extends ComputeKernel { // settings smoothNormals: uniform( 1 ), bounces: uniform( 1 ), + seed: uniform( 0 ), // rays rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ), @@ -40,6 +41,8 @@ export class ProcessHitsKernel extends ComputeKernel { const fn = wgslTagFn/* wgsl */` fn compute( + seed: u32, + // settings smoothNormals: u32, bounces: u32, @@ -71,7 +74,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; + ${ sobolInit }( pixelIndex, seed, input.currentBounce ); let object = transforms[ input.objectIndex ]; var material = materials[ object.materialIndex ]; @@ -110,7 +114,6 @@ 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; } diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index 9f562482..2099d68b 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -3,7 +3,7 @@ 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 { getPcgSeed, SOBOL_INDEX_RAY_JITTER, sobolFuncs, sobolInit } from '../../nodes/random.wgsl.js'; import { queuedRayStruct } from './structs.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; @@ -80,10 +80,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; + ${ sobolInit }( pixelIndex, seed, 0 ); // write the ray data - var jitter = 2.0 * ${ pcgRand2 }() / vec2f( targetDimensions.xy ); + var jitter = 2.0 * ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_RAY_JITTER } ) / vec2f( targetDimensions.xy ); var ray = ${ ndcToCameraRay }( ndc + jitter, cameraToModelMatrix * inverseProjectionMatrix ); ray.direction = normalize( ray.direction ); @@ -92,7 +93,6 @@ 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 ); // write the active params diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js index a7ffd83d..8e32a8f3 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 { SOBOL_INDEX_ENVIRONMENT_SAMPLE, sobolFuncs, sobolInit } 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'; @@ -23,6 +23,8 @@ export class RayIntersectionKernel extends ComputeKernel { hitQueue: storage( new IndirectStorageBufferAttribute( 1, queuedHitStruct.getLength() ), queuedHitStruct ), queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ).toAtomic(), + seed: uniform( 0 ), + // environment envMap: texture( new DataTexture() ), envMapSampler: sampler( new DataTexture() ), @@ -44,6 +46,8 @@ export class RayIntersectionKernel extends ComputeKernel { const fn = wgslTagFn /* wgsl */` fn compute( + seed: u32, + // environment envMap: texture_2d, envMapSampler: sampler, @@ -88,9 +92,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; + ${ sobolInit }( pixelIndex, seed, input.currentBounce ); // run intersection let ray = Ray( input.origin, input.direction ); @@ -104,24 +108,24 @@ export class RayIntersectionKernel extends ComputeKernel { hitQueue[ index ].barycoord = hitResult.barycoord; 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; } else { + let rng = ${ sobolFuncs[ 2 ] }( ${ SOBOL_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..cdeb82b0 100644 --- a/src/webgpu/compute/wavefront/structs.js +++ b/src/webgpu/compute/wavefront/structs.js @@ -13,8 +13,6 @@ export const queuedRayStruct = new StructTypeNode( { pixel: 'vec2u', - pcgStateS0: 'vec4u', - resultColor: 'vec4f', }, 'QueuedRay' ); @@ -36,8 +34,6 @@ export const queuedHitStruct = new StructTypeNode( { normal: 'vec3f', side: 'float', - pcgStateS0: 'vec4u', - resultColor: 'vec4f', }, 'QueuedHit' ); diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index c1fb04f6..4f0cb254 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 { SOBOL_INDEX_ALPHA_TEST, sobolFuncs } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; const _colorVec = new Vector4(); @@ -169,7 +169,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { } - if ( material.transparent != 0 && opacity < ${ pcgRand }() ) { + if ( material.transparent != 0 && opacity < ${ sobolFuncs[ 1 ] }( ${ SOBOL_INDEX_ALPHA_TEST } + ti ) ) { continue; diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index 513d496b..3d6c469b 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -70,10 +70,12 @@ export const pcgRand3 = wgslFn( /*wgsl*/` // - Code from https://www.shadertoy.com/view/WtGyDm // Ported from WebGL version at sobol.glsl.js +// Alpha test should be the last index as it is summed with triangle index export const SOBOL_INDEX_RAY_JITTER = 0; export const SOBOL_INDEX_ENVIRONMENT_SAMPLE = 1; export const SOBOL_INDEX_SCATTER_TYPE = 2; export const SOBOL_INDEX_SCATTER_DIRECTION = 3; +export const SOBOL_INDEX_ALPHA_TEST = 4; const sobolConstants = wgsl( /* wgsl */ ` diff --git a/src/webgpu/nodes/sampling.wgsl.js b/src/webgpu/nodes/sampling.wgsl.js index 685e1e3f..26af005a 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'; /* From adf2b310e5ddd46ff8efa277baba4c1ac63fa26d Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:51:24 +0700 Subject: [PATCH 04/12] Refactor random number usage across the board to support changing it in one place. --- src/webgpu/MegaKernelPathTracer.js | 15 ++++---- src/webgpu/PathTracerBackend.js | 12 ++----- src/webgpu/WaveFrontPathTracer.js | 19 ++++++++-- src/webgpu/WebGPUPathTracer.js | 36 +++++++++++++++++-- src/webgpu/compute/PathTracerMegaKernel.js | 17 +++++---- .../compute/wavefront/ProcessHitsKernel.js | 14 +++++--- .../compute/wavefront/RayGenerationKernel.js | 14 ++++++-- .../wavefront/RayIntersectionKernel.js | 13 ++++--- src/webgpu/materials/GltfCompliantMaterial.js | 6 ++-- src/webgpu/materials/PathtracingMaterial.js | 24 +++++++++++++ src/webgpu/nodes/PathtracerBVHComputeData.js | 6 ++-- src/webgpu/nodes/random.wgsl.js | 36 ++++++++++++------- 12 files changed, 155 insertions(+), 57 deletions(-) diff --git a/src/webgpu/MegaKernelPathTracer.js b/src/webgpu/MegaKernelPathTracer.js index 8c81e03d..734c43ff 100644 --- a/src/webgpu/MegaKernelPathTracer.js +++ b/src/webgpu/MegaKernelPathTracer.js @@ -16,7 +16,13 @@ export class MegaKernelPathTracer extends PathTracerBackend { // kernels this.kernel = new PathTracerMegaKernel().setWorkgroupSize( 8, 8, 1 ); - this.setMaterial( this.material ); + + } + + setRandomFunctions( randomFunctions ) { + + this.kernel.random = randomFunctions; + this.kernel.needsUpdate = true; } @@ -32,12 +38,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..e2d94dba 100644 --- a/src/webgpu/PathTracerBackend.js +++ b/src/webgpu/PathTracerBackend.js @@ -1,7 +1,6 @@ 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 { @@ -38,7 +37,9 @@ export class PathTracerBackend { this.sampleCountClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); this.outputTargetClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); - this.material = new GltfCompliantMaterial(); + } + + setRandomFunctions( randomFunctions ) { } @@ -122,13 +123,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/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index df276f40..545fba51 100644 --- a/src/webgpu/WaveFrontPathTracer.js +++ b/src/webgpu/WaveFrontPathTracer.js @@ -48,7 +48,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( this.material ).setWorkgroupSize( 64, 1, 1 ); + this.hitProcessKernel = new ProcessHitsKernel().setWorkgroupSize( 64, 1, 1 ); // clear kernels this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 ); @@ -59,6 +59,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; @@ -80,8 +93,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(); } diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 42033c95..00442dc7 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -8,6 +8,8 @@ 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 { sobolFuncs, sobolInitFunc, sobolNextBounceFunc } from './nodes/random.wgsl.js'; +import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js'; const _resolution = new Vector2(); const _color = new Color(); @@ -42,8 +44,10 @@ export class WebGPUPathTracer { this._pathTracer.dispose(); this._pathTracer = value ? new MegaKernelPathTracer( this._renderer ) : new WaveFrontPathTracer( this._renderer ); + this._pathTracer.setRandomFunctions( this.randomFunctions ); this._pathTracer.setBVHData( this._bvhData ); this._pathTracer.setTextures( this.textureArray.texture ); + this._pathTracer.setMaterial( this.material ); this.setCamera( this.camera ); this.updateEnvironment(); @@ -91,6 +95,9 @@ export class WebGPUPathTracer { this.textureArray = new RenderTarget2DArray( 1024, 1024 ); + this.material = new GltfCompliantMaterial(); + this.setRandomFunctions( 0 ); + // initialize the scene so it doesn't fail this.setScene( new Scene(), new PerspectiveCamera() ); @@ -137,7 +144,7 @@ export class WebGPUPathTracer { // Build TLAS and compute functions const bvhData = new PathtracerBVHComputeData( scene ); bvhData.update(); - bvhData.useTransparencyRaycastFn( this.textureArray.texture ); + bvhData.useTransparencyRaycastFn( this.textureArray.texture, this.randomFunctions ); this.textureArray.setTextures( this._renderer, bvhData.textures ); this._pathTracer.setTextures( this.textureArray.texture ); @@ -152,12 +159,30 @@ export class WebGPUPathTracer { getMaterial() { - return this._pathTracer.getMaterial(); + return this.material; + + } + + setRandomFunctions( enumeration ) { + + this.randomFunctions = { + init: sobolInitFunc, + nextBounce: sobolNextBounceFunc, + f32: sobolFuncs[ 1 ], + vec2f: sobolFuncs[ 2 ], + vec3f: sobolFuncs[ 3 ], + vec4f: sobolFuncs[ 4 ], + }; + + this.material.setRandomFunctions( this.randomFunctions ); + this._pathTracer.setRandomFunctions( this.randomFunctions ); } setMaterial( material ) { + this.material = material; + this.material.setRandomFunctions( this.randomFunctions ); this._pathTracer.setMaterial( material ); } @@ -256,6 +281,13 @@ export class WebGPUPathTracer { } + if ( ! this.material.initialized ) { + + this.material.init( renderer ); + this.material.initialized = true; + + } + const delta = 1000 * timer.getDelta(); this._resetTime += delta; diff --git a/src/webgpu/compute/PathTracerMegaKernel.js b/src/webgpu/compute/PathTracerMegaKernel.js index 5519a125..e601995c 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, sobolInit, sobolFuncs, SOBOL_INDEX_RAY_JITTER, SOBOL_INDEX_ENVIRONMENT_SAMPLE } 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'; @@ -15,7 +15,7 @@ export class PathTracerMegaKernel extends ComputeKernel { const params = { bvhData: { value: null }, - + random: { value: null }, material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), @@ -54,6 +54,11 @@ 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 = { + init: proxyFn( 'random.value.init', params ), + nextBounce: proxyFn( 'random.value.nextBounce', params ), + vec2f: proxyFn( 'random.value.vec2f', params ), + }; const shader = wgslTagFn/* wgsl */` @@ -124,10 +129,10 @@ export class PathTracerMegaKernel extends ComputeKernel { let ndc = uv * 2.0 - vec2f( 1.0 ); let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ sobolInit }( pixelIndex, seed, 0 ); + ${ rng.init }( pixelIndex, seed, 0 ); // scene ray - var jitter = 2.0 * ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_RAY_JITTER } ) / 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 ); @@ -136,7 +141,7 @@ export class PathTracerMegaKernel extends ComputeKernel { for ( var bounce = 0u; bounce < bounces; bounce ++ ) { - ${ sobolInit }( pixelIndex, seed, bounce ); + ${ rng.nextBounce }(); var hitResult: ${ raycastOutput }; if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) { @@ -175,7 +180,7 @@ export class PathTracerMegaKernel extends ComputeKernel { } else { - let rng = ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_ENVIRONMENT_SAMPLE } ); + let rng = ${ rng.vec2f }( ${ RNG_INDEX_ENVIRONMENT_SAMPLE } ); if ( bounce > 0u ) { resultColor += ${ sampleEnvironmentFn }( envMap, envMapSampler, envInfo, ray.direction, rng ) * vec4f( throughputColor, 0.0 ); diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index 29afdd40..ea0bd088 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -7,14 +7,15 @@ import { proxy, proxyFn } from '../../lib/nodes/NodeProxy.js'; import { weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; import { isTerminatingScatterFunc } from '../../nodes/utils.wgsl.js'; -import { sobolInit } from '../../nodes/random.wgsl.js'; export class ProcessHitsKernel extends ComputeKernel { - constructor( material ) { + constructor() { const params = { bvhData: { value: null }, + random: { value: null }, + material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(), @@ -37,6 +38,11 @@ export class ProcessHitsKernel extends ComputeKernel { }; const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params ); + const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params ); + const rng = { + init: proxyFn( 'random.value.init', params ), + vec2f: proxyFn( 'random.value.vec2f', params ), + }; const fn = wgslTagFn/* wgsl */` @@ -75,7 +81,7 @@ export class ProcessHitsKernel extends ComputeKernel { let indexUV = vec2u( input.pixel_x, input.pixel_y ); let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ sobolInit }( pixelIndex, seed, input.currentBounce ); + ${ rng.init }( pixelIndex, seed, input.currentBounce ); let object = transforms[ input.objectIndex ]; var material = materials[ object.materialIndex ]; @@ -90,7 +96,7 @@ export class ProcessHitsKernel extends ComputeKernel { 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 ); diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index 2099d68b..a5cd591c 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -3,15 +3,18 @@ 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, SOBOL_INDEX_RAY_JITTER, sobolFuncs, sobolInit } 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'; +import { proxyFn } from '../../lib/nodes/NodeProxy.js'; export class RayGenerationKernel extends ComputeKernel { constructor() { const params = { + random: { value: null }, + cameraToModelMatrix: uniform( new Matrix4() ), inverseProjectionMatrix: uniform( new Matrix4() ), @@ -28,6 +31,11 @@ export class RayGenerationKernel extends ComputeKernel { globalId: globalId, }; + const rng = { + init: proxyFn( 'random.value.init', params ), + vec2f: proxyFn( 'random.value.vec2f', params ), + }; + const fn = wgslTagFn /* wgsl */` fn compute( cameraToModelMatrix: mat4x4f, @@ -81,10 +89,10 @@ export class RayGenerationKernel extends ComputeKernel { let index = atomicAdd( &queueSizes[ 1 ], 1 ) % queueCapacity; let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ sobolInit }( pixelIndex, seed, 0 ); + ${ rng.init }( pixelIndex, seed, 0 ); // write the ray data - var jitter = 2.0 * ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_RAY_JITTER } ) / 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 ); diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js index 8e32a8f3..1a3cb3bc 100644 --- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js +++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js @@ -1,9 +1,9 @@ import { DataTexture, Matrix3, IndirectStorageBufferAttribute, StorageTexture } from 'three/webgpu'; import { ComputeKernel } from '../ComputeKernel.js'; import { uniform, texture, sampler, storage, textureStore, globalId } from 'three/tsl'; -import { SOBOL_INDEX_ENVIRONMENT_SAMPLE, sobolFuncs, sobolInit } 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 { proxy, proxyFn } from '../../lib/nodes/NodeProxy.js'; import { sampleEnvironmentFn, weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; @@ -13,6 +13,7 @@ export class RayIntersectionKernel extends ComputeKernel { const params = { bvhData: { value: null }, + random: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(), @@ -42,6 +43,10 @@ export class RayIntersectionKernel extends ComputeKernel { const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params ); const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params ); + const rng = { + init: proxyFn( 'random.value.init', params ), + vec2f: proxyFn( 'random.value.vec2f', params ), + }; const fn = wgslTagFn /* wgsl */` @@ -94,7 +99,7 @@ export class RayIntersectionKernel extends ComputeKernel { let indexUV = input.pixel; let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ sobolInit }( pixelIndex, seed, input.currentBounce ); + ${ rng.init }( pixelIndex, seed, input.currentBounce ); // run intersection let ray = Ray( input.origin, input.direction ); @@ -117,7 +122,7 @@ export class RayIntersectionKernel extends ComputeKernel { } else { - let rng = ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_ENVIRONMENT_SAMPLE } ); + let rng = ${ rng.vec2f }( ${ RNG_INDEX_ENVIRONMENT_SAMPLE } ); var resultColor = input.resultColor; if ( input.currentBounce > 0u ) { diff --git a/src/webgpu/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js index 37875027..e39f9e2d 100644 --- a/src/webgpu/materials/GltfCompliantMaterial.js +++ b/src/webgpu/materials/GltfCompliantMaterial.js @@ -6,7 +6,7 @@ import { specularBrdfFunc, diffuseBrdfFunc, fresnelMixFunc, conductorFresnelFunc 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 { SOBOL_INDEX_SCATTER_DIRECTION, SOBOL_INDEX_SCATTER_TYPE, sobolFuncs } 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(); @@ -139,9 +139,9 @@ export class GltfCompliantMaterial extends PathtracingMaterial { cdf.z = weights.clearcoat + cdf.y; cdf.w = 0; // weights.transmission + cdf.z; - let r = ${ sobolFuncs[ 1 ] }( ${ SOBOL_INDEX_SCATTER_TYPE } ) * cdf.z; + let r = ${ this.rng.f32 }( ${ RNG_INDEX_SCATTER_TYPE } ) * cdf.z; - let directionUV = ${ sobolFuncs[ 2 ] }( ${ SOBOL_INDEX_SCATTER_DIRECTION } ); + let directionUV = ${ this.rng.vec2f }( ${ RNG_INDEX_SCATTER_DIRECTION } ); var wi: vec3f; var wiClearcoat: vec3f; var wh: vec3f; diff --git a/src/webgpu/materials/PathtracingMaterial.js b/src/webgpu/materials/PathtracingMaterial.js index 506c1c51..14024a02 100644 --- a/src/webgpu/materials/PathtracingMaterial.js +++ b/src/webgpu/materials/PathtracingMaterial.js @@ -1,10 +1,25 @@ import { lambertBsdfFunc } from '../nodes/material.wgsl.js'; +import { proxyFn } from '../lib/nodes/NodeProxy.js'; /** * Defines a material sampled by the pathtracer */ export class PathtracingMaterial { + constructor() { + + this.rng = { + _value: null, + init: proxyFn( 'rng._value.init', this ), + nextBounce: proxyFn( 'rng._value.nextBounce', this ), + f32: proxyFn( 'rng._value.f32', this ), + vec2f: proxyFn( 'rng._value.vec2f', this ), + vec3f: proxyFn( 'rng._value.vec3f', this ), + vec4f: proxyFn( 'rng._value.vec4f', this ), + }; + + } + /** * * Called once per material @@ -27,6 +42,15 @@ export class PathtracingMaterial { } + /** + * rng: { init, nextBounce, f32, vec2f, vec3f, vec4f } + */ + setRandomFunctions( rng ) { + + this.rng._value = rng; + + } + getData() { return { diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index 4f0cb254..f3b7089a 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 { SOBOL_INDEX_ALPHA_TEST, sobolFuncs } from './random.wgsl.js'; +import { RNG_INDEX_ALPHA_TEST, sobolFuncs } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; const _colorVec = new Vector4(); @@ -45,7 +45,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { } - useTransparencyRaycastFn( textures ) { + useTransparencyRaycastFn( textures, randomFunctions ) { const texturesNode = texture( textures ); const samplerNode = sampler( textures ); @@ -169,7 +169,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { } - if ( material.transparent != 0 && opacity < ${ sobolFuncs[ 1 ] }( ${ SOBOL_INDEX_ALPHA_TEST } + ti ) ) { + if ( material.transparent != 0 && opacity < ${ randomFunctions.f32 }( ${ RNG_INDEX_ALPHA_TEST } + ti ) ) { continue; diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index 3d6c469b..5b7a8abd 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -1,7 +1,15 @@ import { uint, wgsl, wgslFn } 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, @@ -70,13 +78,6 @@ export const pcgRand3 = wgslFn( /*wgsl*/` // - Code from https://www.shadertoy.com/view/WtGyDm // Ported from WebGL version at sobol.glsl.js -// Alpha test should be the last index as it is summed with triangle index -export const SOBOL_INDEX_RAY_JITTER = 0; -export const SOBOL_INDEX_ENVIRONMENT_SAMPLE = 1; -export const SOBOL_INDEX_SCATTER_TYPE = 2; -export const SOBOL_INDEX_SCATTER_DIRECTION = 3; -export const SOBOL_INDEX_ALPHA_TEST = 4; - const sobolConstants = wgsl( /* wgsl */ ` const SOBOL_FACTOR: f32 = 1.0 / 16777216.0; @@ -228,7 +229,7 @@ const sobolScrambleNodesGenerator = ( dim = 1 ) => { // 1-dimensional vector = f32 const sobolNodes = Array.from( { length: 5 }, ( _, i ) => sobolScrambleNodesGenerator( i ) ); -const generateSobolPointFunc = wgslTagFn` +export const generateSobolPointFunc = wgslTagFn` ${ [ sobolConstants ] } fn generateSobolPoint( id: u32 ) -> vec4f { @@ -344,7 +345,12 @@ const sobolGenerator = ( dim = 1 ) => { }; -export const sobolInit = wgslTagFn` +// 0th node is intentionally empty to make access pattern more intuitive: +// sobolFuncs[ i ] is a function node to sample i-dimensional vector +// 1-dimensional vector = f32 +export const sobolFuncs = Array.from( { length: 5 }, ( _, i ) => sobolGenerator( i ) ); + +export const sobolInitFunc = wgslTagFn` fn sobolInit( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { @@ -356,8 +362,12 @@ export const sobolInit = wgslTagFn` `; -// 0th node is intentionally empty to make access pattern more intuitive: -// sobolFuncs[ i ] is a function node to sample i-dimensional vector -// 1-dimensional vector = f32 -export const sobolFuncs = Array.from( { length: 5 }, ( _, i ) => sobolGenerator( i ) ); +export const sobolNextBounceFunc = wgslTagFn` + + fn sobolNextBounce() -> void { + ${ sobolBounceIndex }++; + + } + +`; From 2ced6b925c4d9662fa54f6bda26a3331071c104e Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:37:14 +0700 Subject: [PATCH 05/12] Add configuration option to choose PCG random numbers. Remove unused functions, inline lambertBsdf to use correct random. --- src/webgpu/WebGPUPathTracer.js | 30 ++++++---- src/webgpu/materials/GltfCompliantMaterial.js | 11 +++- src/webgpu/materials/PathtracingMaterial.js | 23 ++++++- src/webgpu/nodes/PathtracerBVHComputeData.js | 2 +- src/webgpu/nodes/material.wgsl.js | 46 +++++--------- src/webgpu/nodes/random.wgsl.js | 60 ++++++++++++------- 6 files changed, 106 insertions(+), 66 deletions(-) diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 00442dc7..6dcc30b8 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -8,11 +8,16 @@ 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 { sobolFuncs, sobolInitFunc, sobolNextBounceFunc } from './nodes/random.wgsl.js'; +import { pcgFunctions, sobolFunctions } from './nodes/random.wgsl.js'; import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js'; +import { PathtracingMaterial } from './materials/PathtracingMaterial.js'; const _resolution = new Vector2(); const _color = new Color(); + +export const RNG_PCG = 0; +export const RNG_SOBOL = 1; + export class WebGPUPathTracer { get bounces() { @@ -96,7 +101,7 @@ export class WebGPUPathTracer { this.textureArray = new RenderTarget2DArray( 1024, 1024 ); this.material = new GltfCompliantMaterial(); - this.setRandomFunctions( 0 ); + this.setRandomFunctions( RNG_SOBOL ); // initialize the scene so it doesn't fail this.setScene( new Scene(), new PerspectiveCamera() ); @@ -163,16 +168,19 @@ export class WebGPUPathTracer { } - setRandomFunctions( enumeration ) { + setRandomFunctions( type ) { - this.randomFunctions = { - init: sobolInitFunc, - nextBounce: sobolNextBounceFunc, - f32: sobolFuncs[ 1 ], - vec2f: sobolFuncs[ 2 ], - vec3f: sobolFuncs[ 3 ], - vec4f: sobolFuncs[ 4 ], - }; + switch ( type ) { + + case RNG_PCG: + this.randomFunctions = pcgFunctions; + break; + + case RNG_SOBOL: + default: + this.randomFunctions = sobolFunctions; + + } this.material.setRandomFunctions( this.randomFunctions ); this._pathTracer.setRandomFunctions( this.randomFunctions ); diff --git a/src/webgpu/materials/GltfCompliantMaterial.js b/src/webgpu/materials/GltfCompliantMaterial.js index e39f9e2d..24c4e8d4 100644 --- a/src/webgpu/materials/GltfCompliantMaterial.js +++ b/src/webgpu/materials/GltfCompliantMaterial.js @@ -2,7 +2,15 @@ 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'; @@ -13,7 +21,6 @@ const TURQUIN_METAL_URL = new URL( '../../textures/turquinMetal.png', import.met 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 { diff --git a/src/webgpu/materials/PathtracingMaterial.js b/src/webgpu/materials/PathtracingMaterial.js index 14024a02..34392d3e 100644 --- a/src/webgpu/materials/PathtracingMaterial.js +++ b/src/webgpu/materials/PathtracingMaterial.js @@ -1,5 +1,8 @@ -import { lambertBsdfFunc } from '../nodes/material.wgsl.js'; +// import { lambertBsdfFunc } from '../nodes/material.wgsl.js'; import { proxyFn } from '../lib/nodes/NodeProxy.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 @@ -38,7 +41,23 @@ 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; + + } + + `; } diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index f3b7089a..31a6b8c6 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 { RNG_INDEX_ALPHA_TEST, sobolFuncs } from './random.wgsl.js'; +import { RNG_INDEX_ALPHA_TEST } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; const _colorVec = new Vector4(); diff --git a/src/webgpu/nodes/material.wgsl.js b/src/webgpu/nodes/material.wgsl.js index 98b8e47c..74e4095d 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 5b7a8abd..67b06e5d 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -13,24 +13,24 @@ 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, pathIndex, pixel.x + pixel.y); //blue noise seed - g_state.s1 = vec4u(frame, frame*15843, frame*31 + 4566, frame*2345 + 58585); + g_state.s1 = vec4u(pathIndex, pathIndex*15843, pathIndex*31 + 4566, pathIndex*2345 + 58585); } `, [ pcgStateStruct ] ); +// Unneeded? export const getPcgSeed = wgslFn( /* wgsl */` fn pcgGetSeed() -> vec4u { return g_state.s0; @@ -43,7 +43,7 @@ export const setPcgSeed = wgslFn( /* wgsl */` } `, [ 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; @@ -52,27 +52,43 @@ 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 pcgRand3( _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 @@ -345,11 +361,6 @@ const sobolGenerator = ( dim = 1 ) => { }; -// 0th node is intentionally empty to make access pattern more intuitive: -// sobolFuncs[ i ] is a function node to sample i-dimensional vector -// 1-dimensional vector = f32 -export const sobolFuncs = Array.from( { length: 5 }, ( _, i ) => sobolGenerator( i ) ); - export const sobolInitFunc = wgslTagFn` fn sobolInit( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { @@ -371,3 +382,12 @@ export const sobolNextBounceFunc = wgslTagFn` } `; + +export const sobolFunctions = { + init: sobolInitFunc, + nextBounce: sobolNextBounceFunc, + f32: sobolGenerator( 1 ), + vec2f: sobolGenerator( 2 ), + vec3f: sobolGenerator( 3 ), + vec4f: sobolGenerator( 4 ), +}; From 29fdab5cd6f1c5f4f8583c9d39e697dc62fa1caa Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:16:02 +0700 Subject: [PATCH 06/12] Implement sobol random functions with cached sobol points in a texture. --- src/webgpu/SobolNumberMapGenerator.js | 48 ++++++++++++++++++++++ src/webgpu/WebGPUPathTracer.js | 18 +++++++-- src/webgpu/nodes/random.wgsl.js | 57 +++++++++++++++++++++------ 3 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 src/webgpu/SobolNumberMapGenerator.js diff --git a/src/webgpu/SobolNumberMapGenerator.js b/src/webgpu/SobolNumberMapGenerator.js new file mode 100644 index 00000000..c786873c --- /dev/null +++ b/src/webgpu/SobolNumberMapGenerator.js @@ -0,0 +1,48 @@ +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; + + } + + 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 ); + + } + +} diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 6dcc30b8..425afc22 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -8,15 +8,16 @@ 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 } from './nodes/random.wgsl.js'; +import { pcgFunctions, sobolFunctions, sobolTextureFunctions } from './nodes/random.wgsl.js'; import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js'; -import { PathtracingMaterial } from './materials/PathtracingMaterial.js'; +import { SobolNumberMapGenerator } from './SobolNumberMapGenerator.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 { @@ -101,7 +102,8 @@ export class WebGPUPathTracer { this.textureArray = new RenderTarget2DArray( 1024, 1024 ); this.material = new GltfCompliantMaterial(); - this.setRandomFunctions( RNG_SOBOL ); + this._sobolMap = new SobolNumberMapGenerator( renderer, 256 ); + this.setRandomFunctions( RNG_SOBOL_TEXTURE ); // initialize the scene so it doesn't fail this.setScene( new Scene(), new PerspectiveCamera() ); @@ -176,6 +178,10 @@ export class WebGPUPathTracer { this.randomFunctions = pcgFunctions; break; + case RNG_SOBOL_TEXTURE: + this.randomFunctions = sobolTextureFunctions( this._sobolMap.texture ); + break; + case RNG_SOBOL: default: this.randomFunctions = sobolFunctions; @@ -296,6 +302,12 @@ export class WebGPUPathTracer { } + if ( ! this._sobolMap.isGenerated ) { + + this._sobolMap.generate(); + + } + const delta = 1000 * timer.getDelta(); this._resetTime += delta; diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index 67b06e5d..9c4c856e 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -1,4 +1,4 @@ -import { uint, 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 @@ -94,10 +94,10 @@ export const pcgFunctions = { // - Code from https://www.shadertoy.com/view/WtGyDm // Ported from WebGL version at sobol.glsl.js -const sobolConstants = wgsl( /* wgsl */ ` +const SOBOL_MAX_POINTS = uint( 256 * 256 ); +const SOBOL_FACTOR = float( 1.0 / 16777216.0 ); - const SOBOL_FACTOR: f32 = 1.0 / 16777216.0; - const SOBOL_MAX_POINTS: u32 = 256u * 256u; +const sobolConstants = wgsl( /* wgsl */ ` const SOBOL_DIRECTIONS_1 = array( 0x80000000u, 0xc0000000u, 0xa0000000u, 0xf0000000u, @@ -251,9 +251,9 @@ export const generateSobolPointFunc = wgslTagFn` fn generateSobolPoint( id: u32 ) -> vec4f { var index = id; - if ( index >= SOBOL_MAX_POINTS ) { + if ( index >= ${ SOBOL_MAX_POINTS } ) { - index = index % SOBOL_MAX_POINTS; + index = index % ${ SOBOL_MAX_POINTS }; // return vec4( 0.0 ); } @@ -265,7 +265,7 @@ export const generateSobolPointFunc = wgslTagFn` 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; + return vec4( f32( x ), f32( y ), f32( z ), f32( w ) ) * ${ SOBOL_FACTOR }; } @@ -306,7 +306,7 @@ const sobolGetSeedFunc = wgslTagFn` `; -const sobolGenerator = ( dim = 1 ) => { +const sobolGenerator = ( dim = 1, sobolPointFunc = generateSobolPointFunc ) => { if ( dim <= 0 ) { @@ -345,15 +345,13 @@ const sobolGenerator = ( dim = 1 ) => { let shuffle_seed = ${ sobolNodes[ 1 ].hashCombine }( seed, 0u ); let shuffled_index = ${ sobolNodes[ 1 ].scramble }( ${ sobolNodes[ 1 ].reverseBits }( index ), shuffle_seed ); - let sobol_pt = ${ generateSobolPointFunc }( shuffled_index )${ components }; - // TODO: cache sobol point in a texture - // let sobol_pt = sobolGetTexturePoint( shuffled_index )${ components }; + 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 ) ); + return ${ SOBOL_FACTOR } * ${ ftype }( result >> ${utype}( 8 ) ); } @@ -391,3 +389,38 @@ export const sobolFunctions = { 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 ), + }; + +}; From 0b832c636d5737f9bf2906ab35441d44dd5f40ce Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:52:56 +0700 Subject: [PATCH 07/12] Fix bvhData not updating random functions correctly. --- src/webgpu/SobolNumberMapGenerator.js | 3 +++ src/webgpu/WebGPUPathTracer.js | 9 +++++---- src/webgpu/nodes/PathtracerBVHComputeData.js | 18 +++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/webgpu/SobolNumberMapGenerator.js b/src/webgpu/SobolNumberMapGenerator.js index c786873c..10488352 100644 --- a/src/webgpu/SobolNumberMapGenerator.js +++ b/src/webgpu/SobolNumberMapGenerator.js @@ -20,6 +20,7 @@ export class SobolNumberMapGenerator { this.renderer = renderer; this.dimensions = dimensions; + this.isGenerated = false; } @@ -43,6 +44,8 @@ export class SobolNumberMapGenerator { renderer.setRenderTarget( ogTarget ); + this.isGenerated = true; + } } diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index 425afc22..d0c1a3a0 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -103,7 +103,7 @@ export class WebGPUPathTracer { this.material = new GltfCompliantMaterial(); this._sobolMap = new SobolNumberMapGenerator( renderer, 256 ); - this.setRandomFunctions( RNG_SOBOL_TEXTURE ); + this.setPRNGType( RNG_SOBOL_TEXTURE ); // initialize the scene so it doesn't fail this.setScene( new Scene(), new PerspectiveCamera() ); @@ -149,9 +149,9 @@ export class WebGPUPathTracer { } ); // Build TLAS and compute functions - const bvhData = new PathtracerBVHComputeData( scene ); + const bvhData = new PathtracerBVHComputeData( scene, this.randomFunctions ); bvhData.update(); - bvhData.useTransparencyRaycastFn( this.textureArray.texture, this.randomFunctions ); + bvhData.useTransparencyRaycastFn( this.textureArray.texture ); this.textureArray.setTextures( this._renderer, bvhData.textures ); this._pathTracer.setTextures( this.textureArray.texture ); @@ -170,7 +170,7 @@ export class WebGPUPathTracer { } - setRandomFunctions( type ) { + setPRNGType( type ) { switch ( type ) { @@ -189,6 +189,7 @@ export class WebGPUPathTracer { } this.material.setRandomFunctions( this.randomFunctions ); + this._bvhData?.setRandomFunctions( this.randomFunctions ); this._pathTracer.setRandomFunctions( this.randomFunctions ); } diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index 31a6b8c6..fed586f7 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -8,6 +8,7 @@ import { bvhNodeBoundsStruct, bvhNodeStruct, rayStruct } from '../lib/wgsl/struc import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; import { RNG_INDEX_ALPHA_TEST } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; +import { proxyFn } from '../lib/nodes/NodeProxy.js'; const _colorVec = new Vector4(); const transformStruct = new StructTypeNode( { @@ -23,7 +24,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, randomFunctions, options = {} ) { // TODO: once supported we should use the appropriately-sized member sizes super( bvh, { @@ -43,9 +44,20 @@ export class PathtracerBVHComputeData extends BVHComputeData { this.materials = []; this.bvhMap = new Map(); + this.rng = { + _value: randomFunctions, + f32: proxyFn( 'rng._value.f32', this ), + }; + + } + + setRandomFunctions( randomFunctions ) { + + this.rng._value = randomFunctions; + } - useTransparencyRaycastFn( textures, randomFunctions ) { + useTransparencyRaycastFn( textures ) { const texturesNode = texture( textures ); const samplerNode = sampler( textures ); @@ -169,7 +181,7 @@ export class PathtracerBVHComputeData extends BVHComputeData { } - if ( material.transparent != 0 && opacity < ${ randomFunctions.f32 }( ${ RNG_INDEX_ALPHA_TEST } + ti ) ) { + if ( material.transparent != 0 && opacity < ${ this.rng.f32 }( ${ RNG_INDEX_ALPHA_TEST } + ti ) ) { continue; From 6ba1ab6dd7330da525a0b38d82ca9a8ce68ba9f0 Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:53:16 +0700 Subject: [PATCH 08/12] Add configuration option for webgpu prng in viewerTest --- example/viewerTest.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/example/viewerTest.js b/example/viewerTest.js index 6945e88b..4d1329b1 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,27 @@ 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 ); + + } ); + + webgpuOptions.show( params.isWebGPU ); pathTracingFolder.add( params, 'enable' ); From cadfe4c3f863a18852c654765bacef44ffe2e19b Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:00:51 +0700 Subject: [PATCH 09/12] Remove unused function --- src/webgpu/nodes/sampling.wgsl.js | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/webgpu/nodes/sampling.wgsl.js b/src/webgpu/nodes/sampling.wgsl.js index 26af005a..101af469 100644 --- a/src/webgpu/nodes/sampling.wgsl.js +++ b/src/webgpu/nodes/sampling.wgsl.js @@ -10,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 { @@ -44,6 +24,8 @@ 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, uv: vec2f ) -> vec3f { From c8ba7c75bbf7ef7e3278c134c6b229583f0c269c Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Sat, 30 May 2026 17:45:46 +0700 Subject: [PATCH 10/12] Extract proxyFns into a separate object and pass it instead of raw functions. --- src/webgpu/MegaKernelPathTracer.js | 13 +++------- src/webgpu/PathTracerBackend.js | 6 ++--- src/webgpu/RNGData.js | 22 ++++++++++++++++ src/webgpu/WaveFrontPathTracer.js | 10 ++++---- src/webgpu/WebGPUPathTracer.js | 25 +++++++++---------- src/webgpu/compute/PathTracerMegaKernel.js | 9 ++----- .../compute/wavefront/ProcessHitsKernel.js | 8 ++---- .../compute/wavefront/RayGenerationKernel.js | 10 ++------ .../wavefront/RayIntersectionKernel.js | 10 +++----- src/webgpu/materials/PathtracingMaterial.js | 21 +++------------- src/webgpu/nodes/PathtracerBVHComputeData.js | 14 ++--------- 11 files changed, 59 insertions(+), 89 deletions(-) create mode 100644 src/webgpu/RNGData.js diff --git a/src/webgpu/MegaKernelPathTracer.js b/src/webgpu/MegaKernelPathTracer.js index 734c43ff..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,14 +15,7 @@ export class MegaKernelPathTracer extends PathTracerBackend { this.samples = 0; // kernels - this.kernel = new PathTracerMegaKernel().setWorkgroupSize( 8, 8, 1 ); - - } - - setRandomFunctions( randomFunctions ) { - - this.kernel.random = randomFunctions; - this.kernel.needsUpdate = true; + this.kernel = new PathTracerMegaKernel( this.rngData ).setWorkgroupSize( 8, 8, 1 ); } diff --git a/src/webgpu/PathTracerBackend.js b/src/webgpu/PathTracerBackend.js index e2d94dba..7167449d 100644 --- a/src/webgpu/PathTracerBackend.js +++ b/src/webgpu/PathTracerBackend.js @@ -4,7 +4,7 @@ import { ZeroOutKernel } from './compute/ZeroOutKernel.js'; export class PathTracerBackend { - constructor( renderer ) { + constructor( renderer, rngData ) { this.renderer = renderer; this.camera = null; @@ -37,9 +37,7 @@ export class PathTracerBackend { this.sampleCountClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); this.outputTargetClearKernel = new ZeroOutKernel().setWorkgroupSize( 8, 8, 1 ); - } - - setRandomFunctions( randomFunctions ) { + this.rngData = rngData; } 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/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index 545fba51..1f3133b8 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 ); @@ -45,10 +45,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().setWorkgroupSize( 64, 1, 1 ); + this.hitProcessKernel = new ProcessHitsKernel( this.rngData ).setWorkgroupSize( 64, 1, 1 ); // clear kernels this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 ); diff --git a/src/webgpu/WebGPUPathTracer.js b/src/webgpu/WebGPUPathTracer.js index d0c1a3a0..a2f77db0 100644 --- a/src/webgpu/WebGPUPathTracer.js +++ b/src/webgpu/WebGPUPathTracer.js @@ -11,6 +11,7 @@ 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(); @@ -49,8 +50,7 @@ export class WebGPUPathTracer { useMegakernel( value ) { this._pathTracer.dispose(); - this._pathTracer = value ? new MegaKernelPathTracer( this._renderer ) : new WaveFrontPathTracer( this._renderer ); - this._pathTracer.setRandomFunctions( this.randomFunctions ); + 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 ); @@ -63,7 +63,6 @@ export class WebGPUPathTracer { // members this._renderer = renderer; - this._pathTracer = new MegaKernelPathTracer( renderer ); this._timer = new Timer(); this._envColorTexture = new DataTexture( ); @@ -101,10 +100,14 @@ export class WebGPUPathTracer { this.textureArray = new RenderTarget2DArray( 1024, 1024 ); - this.material = new GltfCompliantMaterial(); 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() ); @@ -149,7 +152,7 @@ export class WebGPUPathTracer { } ); // Build TLAS and compute functions - const bvhData = new PathtracerBVHComputeData( scene, this.randomFunctions ); + const bvhData = new PathtracerBVHComputeData( scene, this.rngData ); bvhData.update(); bvhData.useTransparencyRaycastFn( this.textureArray.texture ); @@ -175,29 +178,25 @@ export class WebGPUPathTracer { switch ( type ) { case RNG_PCG: - this.randomFunctions = pcgFunctions; + this.rngData.setFunctions( pcgFunctions ); break; case RNG_SOBOL_TEXTURE: - this.randomFunctions = sobolTextureFunctions( this._sobolMap.texture ); + this.rngData.setFunctions( sobolTextureFunctions( this._sobolMap.texture ) ); break; case RNG_SOBOL: default: - this.randomFunctions = sobolFunctions; + this.rngData.setFunctions( sobolFunctions ); } - this.material.setRandomFunctions( this.randomFunctions ); - this._bvhData?.setRandomFunctions( this.randomFunctions ); - this._pathTracer.setRandomFunctions( this.randomFunctions ); - } setMaterial( material ) { this.material = material; - this.material.setRandomFunctions( this.randomFunctions ); + this.material.setRNGData( this.rngData ); this._pathTracer.setMaterial( material ); } diff --git a/src/webgpu/compute/PathTracerMegaKernel.js b/src/webgpu/compute/PathTracerMegaKernel.js index e601995c..05648c73 100644 --- a/src/webgpu/compute/PathTracerMegaKernel.js +++ b/src/webgpu/compute/PathTracerMegaKernel.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 }, - random: { value: null }, material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), @@ -54,11 +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 = { - init: proxyFn( 'random.value.init', params ), - nextBounce: proxyFn( 'random.value.nextBounce', params ), - vec2f: proxyFn( 'random.value.vec2f', params ), - }; + const rng = rngData; const shader = wgslTagFn/* wgsl */` diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index ea0bd088..b8abca66 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -10,11 +10,10 @@ import { isTerminatingScatterFunc } from '../../nodes/utils.wgsl.js'; export class ProcessHitsKernel extends ComputeKernel { - constructor() { + constructor( rngData ) { const params = { bvhData: { value: null }, - random: { value: null }, material: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), @@ -39,10 +38,7 @@ export class ProcessHitsKernel extends ComputeKernel { const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params ); const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params ); - const rng = { - init: proxyFn( 'random.value.init', params ), - vec2f: proxyFn( 'random.value.vec2f', params ), - }; + const rng = rngData; const fn = wgslTagFn/* wgsl */` diff --git a/src/webgpu/compute/wavefront/RayGenerationKernel.js b/src/webgpu/compute/wavefront/RayGenerationKernel.js index a5cd591c..29529be0 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -6,15 +6,12 @@ import { ndcToCameraRay } from '../../lib/wgsl/common.wgsl.js'; import { RNG_INDEX_RAY_JITTER } from '../../nodes/random.wgsl.js'; import { queuedRayStruct } from './structs.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; -import { proxyFn } from '../../lib/nodes/NodeProxy.js'; export class RayGenerationKernel extends ComputeKernel { - constructor() { + constructor( rngData ) { const params = { - random: { value: null }, - cameraToModelMatrix: uniform( new Matrix4() ), inverseProjectionMatrix: uniform( new Matrix4() ), @@ -31,10 +28,7 @@ export class RayGenerationKernel extends ComputeKernel { globalId: globalId, }; - const rng = { - init: proxyFn( 'random.value.init', params ), - vec2f: proxyFn( 'random.value.vec2f', params ), - }; + const rng = rngData; const fn = wgslTagFn /* wgsl */` fn compute( diff --git a/src/webgpu/compute/wavefront/RayIntersectionKernel.js b/src/webgpu/compute/wavefront/RayIntersectionKernel.js index 1a3cb3bc..041c3be9 100644 --- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js +++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js @@ -3,17 +3,16 @@ import { ComputeKernel } from '../ComputeKernel.js'; import { uniform, texture, sampler, storage, textureStore, globalId } from 'three/tsl'; import { RNG_INDEX_ENVIRONMENT_SAMPLE } from '../../nodes/random.wgsl.js'; import { queuedRayStruct, queuedHitStruct } from './structs.js'; -import { proxy, proxyFn } from '../../lib/nodes/NodeProxy.js'; +import { proxy } from '../../lib/nodes/NodeProxy.js'; import { sampleEnvironmentFn, weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js'; import { wgslTagFn } from '../../lib/nodes/WGSLTagFnNode.js'; export class RayIntersectionKernel extends ComputeKernel { - constructor() { + constructor( rngData ) { const params = { bvhData: { value: null }, - random: { value: null }, prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(), outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(), @@ -43,10 +42,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 = { - init: proxyFn( 'random.value.init', params ), - vec2f: proxyFn( 'random.value.vec2f', params ), - }; + const rng = rngData; const fn = wgslTagFn /* wgsl */` diff --git a/src/webgpu/materials/PathtracingMaterial.js b/src/webgpu/materials/PathtracingMaterial.js index 34392d3e..913ae7b9 100644 --- a/src/webgpu/materials/PathtracingMaterial.js +++ b/src/webgpu/materials/PathtracingMaterial.js @@ -1,5 +1,3 @@ -// import { lambertBsdfFunc } from '../nodes/material.wgsl.js'; -import { proxyFn } from '../lib/nodes/NodeProxy.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'; @@ -9,17 +7,9 @@ import { diffuseDirectionFunc } from '../nodes/sampling.wgsl.js'; */ export class PathtracingMaterial { - constructor() { + constructor( ) { - this.rng = { - _value: null, - init: proxyFn( 'rng._value.init', this ), - nextBounce: proxyFn( 'rng._value.nextBounce', this ), - f32: proxyFn( 'rng._value.f32', this ), - vec2f: proxyFn( 'rng._value.vec2f', this ), - vec3f: proxyFn( 'rng._value.vec3f', this ), - vec4f: proxyFn( 'rng._value.vec4f', this ), - }; + this.rng = null; } @@ -61,12 +51,9 @@ export class PathtracingMaterial { } - /** - * rng: { init, nextBounce, f32, vec2f, vec3f, vec4f } - */ - setRandomFunctions( rng ) { + setRNGData( rngData ) { - this.rng._value = rng; + this.rng = rngData; } diff --git a/src/webgpu/nodes/PathtracerBVHComputeData.js b/src/webgpu/nodes/PathtracerBVHComputeData.js index fed586f7..da64a46e 100644 --- a/src/webgpu/nodes/PathtracerBVHComputeData.js +++ b/src/webgpu/nodes/PathtracerBVHComputeData.js @@ -8,7 +8,6 @@ import { bvhNodeBoundsStruct, bvhNodeStruct, rayStruct } from '../lib/wgsl/struc import { wgslTagFn } from '../lib/nodes/WGSLTagFnNode.js'; import { RNG_INDEX_ALPHA_TEST } from './random.wgsl.js'; import { sampleTexelFunc } from './utils.wgsl.js'; -import { proxyFn } from '../lib/nodes/NodeProxy.js'; const _colorVec = new Vector4(); const transformStruct = new StructTypeNode( { @@ -24,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, randomFunctions, options = {} ) { + constructor( bvh, rngObject, options = {} ) { // TODO: once supported we should use the appropriately-sized member sizes super( bvh, { @@ -43,19 +42,10 @@ export class PathtracerBVHComputeData extends BVHComputeData { this.storage.materials = null; this.materials = []; this.bvhMap = new Map(); - - this.rng = { - _value: randomFunctions, - f32: proxyFn( 'rng._value.f32', this ), - }; + this.rng = rngObject; } - setRandomFunctions( randomFunctions ) { - - this.rng._value = randomFunctions; - - } useTransparencyRaycastFn( textures ) { From 4ec322e1696e044426e35c608a1e198e462e4a9a Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Sat, 30 May 2026 18:24:40 +0700 Subject: [PATCH 11/12] pcg fixes --- src/webgpu/nodes/random.wgsl.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/webgpu/nodes/random.wgsl.js b/src/webgpu/nodes/random.wgsl.js index 9c4c856e..f6a7337b 100644 --- a/src/webgpu/nodes/random.wgsl.js +++ b/src/webgpu/nodes/random.wgsl.js @@ -19,27 +19,15 @@ export const pcgStateStruct = wgsl( /* wgsl */` ` ); const pcgInit = wgslFn( /* wgsl */` - fn pcgInitialize( pixelIndex: u32, pathIndex: u32, _bounceIndex: u32 ) -> void { + fn pcgInitialize( pixelIndex: u32, pathIndex: u32, bounceIndex: u32 ) -> void { let pixel = vec2( ( pixelIndex >> 16 ) & 0xFF, pixelIndex & 0xFF ); //white noise seed - g_state.s0 = vec4u(pixel, pathIndex, pixel.x + pixel.y); + g_state.s0 = vec4u( pixel | vec2( bounceIndex << 16 ), pathIndex, pixel.x + pixel.y); //blue noise seed g_state.s1 = vec4u(pathIndex, pathIndex*15843, pathIndex*31 + 4566, pathIndex*2345 + 58585); - } -`, [ pcgStateStruct ] ); - -// Unneeded? -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 ] ); @@ -74,7 +62,7 @@ const pcgRand3 = wgslFn( /*wgsl*/` `, [ pcg4d, pcgStateStruct ] ); const pcgRand4 = wgslFn( /*wgsl*/` - fn pcgRand3( _id: u32 ) -> vec4f { + fn pcgRand4( _id: u32 ) -> vec4f { pcg4d(&g_state.s0); return abs( vec4f( g_state.s0 ) / f32(0xffffffffu) ); } From 62d1e7f42e43208b2f466416b416a8462aaa567f Mon Sep 17 00:00:00 2001 From: Egor Kuklin <40146818+TheBlek@users.noreply.github.com> Date: Sat, 30 May 2026 18:43:33 +0700 Subject: [PATCH 12/12] Fix seed for rays. --- src/webgpu/WaveFrontPathTracer.js | 8 ++------ src/webgpu/compute/wavefront/ProcessHitsKernel.js | 9 ++++----- src/webgpu/compute/wavefront/RayGenerationKernel.js | 1 + src/webgpu/compute/wavefront/RayIntersectionKernel.js | 9 +++------ src/webgpu/compute/wavefront/structs.js | 9 +++++---- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/webgpu/WaveFrontPathTracer.js b/src/webgpu/WaveFrontPathTracer.js index 1f3133b8..093fdd0b 100644 --- a/src/webgpu/WaveFrontPathTracer.js +++ b/src/webgpu/WaveFrontPathTracer.js @@ -25,7 +25,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { // options this.tiles = new Vector2( 3, 3 ); this.envInfo = new EquirectHdrInfoUniform(); - this.seed = 0; // queues this.rayQueue = new IndirectStorageBufferAttribute( MAX_RAY_COUNT, queuedRayStruct.getLength() ); @@ -186,7 +185,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { super.reset(); - this.seed = 0; + this.enqueueRaysKernel.seed = 0; const { width, height } = sampleCountTarget; const dispatchSize = sampleCountClearKernel.getDispatchSize( width, height ); @@ -273,7 +272,7 @@ export class WaveFrontPathTracer extends PathTracerBackend { primeRayGenerationDispatchKernel.outputDispatch = rayGenerationDispatch; // set up the ray generation kernel - enqueueRaysKernel.seed = this.seed; + enqueueRaysKernel.seed ++; enqueueRaysKernel.cameraToModelMatrix.copy( camera.matrixWorld ); enqueueRaysKernel.inverseProjectionMatrix.copy( camera.projectionMatrixInverse ); enqueueRaysKernel.tileIndexBuffer = tileIndexBuffer; @@ -298,7 +297,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { rayIntersectionKernel.rayQueue = rayQueue; rayIntersectionKernel.queueSizes = queueSizes; rayIntersectionKernel.hitQueue = hitQueue; - rayIntersectionKernel.seed = this.seed; renderer.compute( rayIntersectionKernel.kernel, intersectDispatch ); // mark the rays as consumed @@ -315,7 +313,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { hitProcessKernel.rayQueue = rayQueue; hitProcessKernel.queueSizes = queueSizes; hitProcessKernel.hitQueue = hitQueue; - hitProcessKernel.seed = this.seed; renderer.compute( hitProcessKernel.kernel, hitProcessKernel.getDispatchSize( processed, 1, 1 ) ); // Note: hit queue size ([2] and [3]) is reset at the top of the next iteration by PrimeRayGenerationDispatchKernel @@ -330,7 +327,6 @@ export class WaveFrontPathTracer extends PathTracerBackend { samples += samplesPerIteration; this.samples = Math.floor( samples ); - this.seed ++; } diff --git a/src/webgpu/compute/wavefront/ProcessHitsKernel.js b/src/webgpu/compute/wavefront/ProcessHitsKernel.js index b8abca66..d1ccd245 100644 --- a/src/webgpu/compute/wavefront/ProcessHitsKernel.js +++ b/src/webgpu/compute/wavefront/ProcessHitsKernel.js @@ -23,7 +23,6 @@ export class ProcessHitsKernel extends ComputeKernel { // settings smoothNormals: uniform( 1 ), bounces: uniform( 1 ), - seed: uniform( 0 ), // rays rayQueue: storage( new IndirectStorageBufferAttribute( 1, queuedRayStruct.getLength() ), queuedRayStruct ), @@ -43,8 +42,6 @@ export class ProcessHitsKernel extends ComputeKernel { const fn = wgslTagFn/* wgsl */` fn compute( - seed: u32, - // settings smoothNormals: u32, bounces: u32, @@ -77,7 +74,7 @@ export class ProcessHitsKernel extends ComputeKernel { let indexUV = vec2u( input.pixel_x, input.pixel_y ); let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ rng.init }( pixelIndex, seed, input.currentBounce ); + ${ rng.init }( pixelIndex, input.seed, input.currentBounce ); let object = transforms[ input.objectIndex ]; var material = materials[ object.materialIndex ]; @@ -86,7 +83,8 @@ 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; @@ -117,6 +115,7 @@ export class ProcessHitsKernel extends ComputeKernel { rayQueue[ index ].throughputColor = input.throughputColor * scatterRec.color / scatterRec.pdf; rayQueue[ index ].currentBounce = input.currentBounce + 1; 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 29529be0..f087751f 100644 --- a/src/webgpu/compute/wavefront/RayGenerationKernel.js +++ b/src/webgpu/compute/wavefront/RayGenerationKernel.js @@ -96,6 +96,7 @@ export class RayGenerationKernel extends ComputeKernel { rayQueue[ index ].throughputColor = vec3f( 1.0 ); rayQueue[ index ].currentBounce = 0; 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 041c3be9..9aeb76cb 100644 --- a/src/webgpu/compute/wavefront/RayIntersectionKernel.js +++ b/src/webgpu/compute/wavefront/RayIntersectionKernel.js @@ -23,8 +23,6 @@ export class RayIntersectionKernel extends ComputeKernel { hitQueue: storage( new IndirectStorageBufferAttribute( 1, queuedHitStruct.getLength() ), queuedHitStruct ), queueSizes: storage( new IndirectStorageBufferAttribute( 4, 1 ), 'u32' ).toAtomic(), - seed: uniform( 0 ), - // environment envMap: texture( new DataTexture() ), envMapSampler: sampler( new DataTexture() ), @@ -47,8 +45,6 @@ export class RayIntersectionKernel extends ComputeKernel { const fn = wgslTagFn /* wgsl */` fn compute( - seed: u32, - // environment envMap: texture_2d, envMapSampler: sampler, @@ -95,7 +91,7 @@ export class RayIntersectionKernel extends ComputeKernel { let indexUV = input.pixel; let pixelIndex = ( indexUV.x << 16 ) | indexUV.y; - ${ rng.init }( pixelIndex, seed, input.currentBounce ); + ${ rng.init }( pixelIndex, input.seed, input.currentBounce ); // run intersection let ray = Ray( input.origin, input.direction ); @@ -106,7 +102,7 @@ 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 = indexUV.x; @@ -115,6 +111,7 @@ export class RayIntersectionKernel extends ComputeKernel { hitQueue[ index ].throughputColor = input.throughputColor; hitQueue[ index ].currentBounce = input.currentBounce; hitQueue[ index ].resultColor = input.resultColor; + hitQueue[ index ].seed = input.seed; } else { diff --git a/src/webgpu/compute/wavefront/structs.js b/src/webgpu/compute/wavefront/structs.js index cdeb82b0..a6d9bf91 100644 --- a/src/webgpu/compute/wavefront/structs.js +++ b/src/webgpu/compute/wavefront/structs.js @@ -3,10 +3,10 @@ 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', @@ -20,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',