three-gpu-pathtracer 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +53 -530
  2. package/build/index.module.js +262 -50
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +261 -48
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +29 -11
  7. package/src/core/WebGLPathTracer.js +8 -0
  8. package/src/core/utils/sceneUpdateUtils.js +8 -12
  9. package/src/detectors/PrecisionMaterial.js +9 -9
  10. package/src/index.js +1 -0
  11. package/src/objects/PhysicalSpotLight.js +2 -0
  12. package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
  13. package/src/textures/BlueNoiseTexture.js +6 -6
  14. package/src/textures/ProceduralEquirectTexture.js +8 -7
  15. package/src/textures/turquinMetal.png +0 -0
  16. package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
  17. package/src/uniforms/FloatAttributeTextureArray.js +11 -11
  18. package/src/uniforms/MaterialsTexture.js +9 -9
  19. package/src/webgpu/API.md +762 -0
  20. package/src/webgpu/AtlasTexture.js +471 -0
  21. package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
  22. package/src/webgpu/EquirectBackgroundInfo.js +97 -0
  23. package/src/webgpu/EquirectHdrInfoNode.js +151 -0
  24. package/src/webgpu/LightsInfoNode.js +224 -0
  25. package/src/webgpu/MegaKernelPathTracer.js +277 -0
  26. package/src/webgpu/PathTracerBackend.js +216 -0
  27. package/src/webgpu/TurquinTexture.js +169 -0
  28. package/src/webgpu/WaveFrontPathTracer.js +586 -0
  29. package/src/webgpu/WebGPUPathTracer.js +1302 -0
  30. package/src/webgpu/compute/ComputeKernel.js +92 -0
  31. package/src/webgpu/compute/CopyBufferKernel.js +40 -0
  32. package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
  33. package/src/webgpu/compute/SampleDebugKernel.js +51 -0
  34. package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
  35. package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
  36. package/src/webgpu/compute/ZeroOutKernel.js +31 -0
  37. package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
  38. package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
  39. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
  40. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
  41. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
  42. package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
  43. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
  44. package/src/webgpu/compute/wavefront/structs.js +155 -0
  45. package/src/webgpu/constants.js +62 -0
  46. package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
  47. package/src/webgpu/index.d.ts +176 -0
  48. package/src/webgpu/index.js +11 -0
  49. package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
  50. package/src/webgpu/materials/GraphMaterial.js +331 -0
  51. package/src/webgpu/materials/PathtracingMaterial.js +89 -0
  52. package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
  53. package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
  54. package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
  55. package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
  56. package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
  57. package/src/webgpu/nodes/eon.wgsl.js +262 -0
  58. package/src/webgpu/nodes/ggx.wgsl.js +190 -0
  59. package/src/webgpu/nodes/lights.wgsl.js +203 -0
  60. package/src/webgpu/nodes/material.wgsl.js +990 -0
  61. package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
  62. package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
  63. package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
  64. package/src/webgpu/nodes/random.wgsl.js +20 -0
  65. package/src/webgpu/nodes/reset.wgsl.js +21 -0
  66. package/src/webgpu/nodes/sampling.wgsl.js +193 -0
  67. package/src/webgpu/nodes/sheen.wgsl.js +148 -0
  68. package/src/webgpu/nodes/structs.wgsl.js +272 -0
  69. package/src/webgpu/nodes/utils.wgsl.js +366 -0
  70. package/src/webgpu/shims/ArrayCameraShim.js +74 -0
  71. package/src/webgpu/shims/EquirectCameraShim.js +37 -0
  72. package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
  73. package/src/webgpu/upscale/FSRUpscaler.js +108 -0
@@ -0,0 +1,92 @@
1
+ export class ComputeKernel {
2
+
3
+ get computeNode() {
4
+
5
+ return this.kernel.computeNode;
6
+
7
+ }
8
+
9
+ get workgroupSize() {
10
+
11
+ return this.kernel.workgroupSize;
12
+
13
+ }
14
+
15
+ set needsUpdate( v ) {
16
+
17
+ // TODO: hack to force the kernel to rebuild since "needsUpdate" is not respected
18
+ this.setWorkgroupSize( ...this.workgroupSize );
19
+
20
+ }
21
+
22
+ constructor( fn, options = {} ) {
23
+
24
+ const {
25
+ workgroupSize = [ 64 ],
26
+ context = {},
27
+ } = options;
28
+
29
+ // this.workgroupSize = [ ...workgroupSize ];
30
+ this._fn = fn;
31
+ this.kernel = null;
32
+ this.context = context;
33
+
34
+ this.setWorkgroupSize( ...workgroupSize );
35
+
36
+ }
37
+
38
+ defineUniformAccessors( parameters ) {
39
+
40
+ for ( const key in parameters ) {
41
+
42
+ if ( key in this ) {
43
+
44
+ throw new Error( `ComputeNode: Uniform name ${ key } is already defined.` );
45
+
46
+ }
47
+
48
+ const node = parameters[ key ];
49
+ if ( 'value' in node ) {
50
+
51
+ Object.defineProperty( this, key, {
52
+ get() {
53
+
54
+ return parameters[ key ].value;
55
+
56
+ },
57
+ set( v ) {
58
+
59
+ parameters[ key ].value = v;
60
+
61
+ },
62
+ } );
63
+
64
+ }
65
+
66
+ }
67
+
68
+ }
69
+
70
+ setWorkgroupSize( x = 64, y = 1, z = 1 ) {
71
+
72
+ const node = this._fn.context( this.context );
73
+
74
+ // forward the call parameters so they remain accessible via "computeNode.parameters"
75
+ node.parameters = this._fn.parameters;
76
+ this.kernel = node.computeKernel( [ x, y, z ] );
77
+ return this;
78
+
79
+ }
80
+
81
+ getDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {
82
+
83
+ const [ wgx, wgy, wgz ] = this.workgroupSize;
84
+ target.length = 3;
85
+ target[ 0 ] = Math.ceil( tx / wgx );
86
+ target[ 1 ] = Math.ceil( ty / wgy );
87
+ target[ 2 ] = Math.ceil( tz / wgz );
88
+ return target;
89
+
90
+ }
91
+
92
+ }
@@ -0,0 +1,40 @@
1
+ import { StorageBufferAttribute } from 'three/webgpu';
2
+ import { uniform, storage, globalId } from 'three/tsl';
3
+ import { ComputeKernel } from './ComputeKernel.js';
4
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+
6
+ // Copies the first "count" elements of one storage buffer into another of the same element type,
7
+ // for carrying live state across a buffer reallocation.
8
+ export class CopyBufferKernel extends ComputeKernel {
9
+
10
+ constructor( elementType ) {
11
+
12
+ const params = {
13
+ source: storage( new StorageBufferAttribute( 1, 1 ), elementType ).toReadOnly(),
14
+ target: storage( new StorageBufferAttribute( 1, 1 ), elementType ),
15
+ count: uniform( 0, 'uint' ),
16
+ globalId: globalId,
17
+ };
18
+
19
+ const fn = wgslTagFn/* wgsl */`
20
+ fn compute( count: u32, globalId: vec3u ) -> void {
21
+
22
+ let index = globalId.x;
23
+ if ( index >= count ) {
24
+
25
+ return;
26
+
27
+ }
28
+
29
+ ${ params.target }[ index ] = ${ params.source }[ index ];
30
+
31
+ }
32
+ `;
33
+
34
+ super( fn( params ) );
35
+
36
+ this.defineUniformAccessors( params );
37
+
38
+ }
39
+
40
+ }
@@ -0,0 +1,470 @@
1
+ import { DataTexture, Vector2, StorageTexture } from 'three/webgpu';
2
+ import { ComputeKernel } from './ComputeKernel.js';
3
+ import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl';
4
+ import { rngInit, rngNextBounce, rand1, rand2, rand3, RNG_INDEX_RAY_JITTER, RNG_INDEX_BACKGROUND_SAMPLE, RNG_INDEX_DIRECT_LIGHT_SAMPLE, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH, RNG_INDEX_ALPHA_TEST } from '../nodes/random.wgsl.js';
5
+ import { misHeuristicFn, weightedAlphaBlendFn } from '../nodes/sampling.wgsl.js';
6
+ import { proxy, proxyFn, wgslTagFn, rayStruct } from 'three-mesh-bvh/webgpu';
7
+ import { clampPathContributionFunc, isTerminatingScatterFunc, offsetRayOriginFunc } from '../nodes/utils.wgsl.js';
8
+ import { lightRecordStruct } from '../nodes/structs.wgsl.js';
9
+ import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, LIGHT_EPSILON, isMISWeightLightFn } from '../nodes/lights.wgsl.js';
10
+ import { applyDispersionFunc, dispersionColorWeightFunc, DISPERSION_MIN_WAVELENGTH, DISPERSION_MAX_WAVELENGTH, transmissionAttenuationFunc } from '../nodes/material.wgsl.js';
11
+ import { TRANSMISSIVE_BACKGROUND_ENVIRONMENT, TRANSMISSIVE_BACKGROUND_OVERLAY, TRANSMISSIVE_BACKGROUND_TRANSPARENT } from '../constants.js';
12
+
13
+ export class PathTracerMegaKernel extends ComputeKernel {
14
+
15
+ constructor( ) {
16
+
17
+ const params = {
18
+ bvhData: { value: null },
19
+ material: { value: null },
20
+ envInfo: { value: null },
21
+ lightsInfo: { value: null },
22
+
23
+ // targets
24
+ prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
25
+ outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
26
+ sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
27
+
28
+ // tiles
29
+ offset: uniform( new Vector2() ),
30
+ tileSize: uniform( new Vector2() ),
31
+
32
+ // settings
33
+ seed: uniform( 0 ),
34
+ maxBounces: uniform( 5 ),
35
+ maxTransparentBounces: uniform( 5, 'uint' ),
36
+ misEnabled: uniform( 1, 'uint' ),
37
+ maxSamples: uniform( 0, 'uint' ),
38
+ filterGlossy: uniform( 1 ),
39
+ clampDirect: uniform( 0 ),
40
+ clampIndirect: uniform( 10 ),
41
+
42
+ backgroundInfo: { value: null },
43
+
44
+ transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
45
+
46
+ textures: texture( new DataTexture() ),
47
+ textureSampler: sampler( new DataTexture() ),
48
+
49
+ // compute variables
50
+ globalId: globalId,
51
+ };
52
+
53
+ const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
54
+ const raycastFirstHitFn = proxyFn( 'bvhData.value.fns.raycastFirstHit', params );
55
+ const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
56
+ const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
57
+ const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
58
+ const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
59
+ const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
60
+
61
+ // environment resources
62
+ const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
63
+ const envIntensityNode = proxy( 'envInfo.value.intensityNode', params );
64
+ const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
65
+ const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
66
+ const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
67
+ const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
68
+
69
+ // analytic scene lights pulled off the lightsInfo provider ( LightsInfoNode )
70
+ const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
71
+ const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
72
+ const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
73
+
74
+ const shader = wgslTagFn/* wgsl */`
75
+
76
+ fn compute(
77
+
78
+ // indices and target
79
+ globalId: vec3u,
80
+
81
+ // tiles
82
+ offset: vec2u,
83
+ tileSize: vec2u,
84
+
85
+ // settings
86
+ seed: u32,
87
+ maxBounces: u32,
88
+ maxTransparentBounces: u32,
89
+ misEnabled: u32,
90
+ maxSamples: u32,
91
+ filterGlossy: f32,
92
+ clampDirect: f32,
93
+ clampIndirect: f32,
94
+
95
+ transmissiveBackground: u32,
96
+
97
+ ) -> void {
98
+
99
+ let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
100
+ let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
101
+
102
+ // make sure we don't bleed over the edge of our tile
103
+ if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
104
+
105
+ return;
106
+
107
+ }
108
+
109
+ // to screen coordinates
110
+ let indexUV = offset + globalId.xy;
111
+ let targetDimensions = textureDimensions( ${ params.outputTarget } );
112
+ if ( indexUV.x >= targetDimensions.x || indexUV.y >= targetDimensions.y ) {
113
+
114
+ return;
115
+
116
+ }
117
+
118
+ // skip the pixel once it has hit the sample limit
119
+ let sampleCount = textureLoad( ${ params.sampleCountTarget }, indexUV ).r;
120
+ if ( maxSamples != 0u && sampleCount >= maxSamples ) {
121
+
122
+ return;
123
+
124
+ }
125
+
126
+ let uv = vec2f( indexUV ) / vec2f( targetDimensions );
127
+ ${ rngInit }( indexUV.xy, seed, 0 );
128
+
129
+ // scene ray
130
+ let jitteredUv = uv + ${ rand2 }( ${ RNG_INDEX_RAY_JITTER } ) / vec2f( targetDimensions );
131
+ var ray: ${ rayStruct };
132
+ if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
133
+
134
+ return;
135
+
136
+ }
137
+
138
+ ray.direction = normalize( ray.direction );
139
+
140
+ var resultColor = vec4f( 0, 0, 0, 1 );
141
+ var throughputColor = vec3f( 1.0 );
142
+ var bsdfPdf = 0.0;
143
+ var isFullyTransmissive = true;
144
+ var minPdf = 1.0;
145
+ var transparentBounce = 0u;
146
+ // A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
147
+ var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
148
+
149
+ // one-sample NEE selects between the analytic lights and the environment -
150
+ // lightsDenom is the number of options
151
+ let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
152
+ let lightsCount = ${ lightsCountNode };
153
+ var lightsDenom = f32( lightsCount );
154
+ if ( envActive ) {
155
+
156
+ lightsDenom += 1.0;
157
+
158
+ }
159
+
160
+ for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
161
+
162
+ var hitResult: ${ raycastOutput };
163
+ let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
164
+ let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
165
+
166
+ // forward hits: a bsdf-sampled ray that lands on a area light. MIS-weighted
167
+ // only when NEE is also sampling the lights
168
+ if ( bounce > 0u ) {
169
+
170
+ for ( var li = 0u; li < lightsCount; li ++ ) {
171
+
172
+ var lightRec: ${ lightRecordStruct };
173
+ if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
174
+
175
+ var misWeight = 1.0;
176
+ if ( misEnabled != 0u ) {
177
+
178
+ let lightPdf = lightRec.pdf / lightsDenom;
179
+ misWeight = ${ misHeuristicFn }( bsdfPdf, lightPdf );
180
+
181
+ }
182
+
183
+ let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
184
+ resultColor += vec4f( lightHit, 0.0 );
185
+
186
+ }
187
+
188
+ }
189
+
190
+ }
191
+
192
+ if ( didHit ) {
193
+
194
+ let objectInfo = transforms[ hitResult.objectIndex ];
195
+ var materialInfo = ( *materials )[ objectInfo.materialIndex ];
196
+
197
+ // a matte surface hit by the camera ray renders as a fully transparent
198
+ if ( materialInfo.matte != 0 && bounce == 0u ) {
199
+
200
+ resultColor = vec4f( 0.0 );
201
+ break;
202
+
203
+ }
204
+
205
+ // apply per-object colors
206
+ materialInfo.color *= objectInfo.color.rgb;
207
+ materialInfo.opacity *= objectInfo.color.a;
208
+
209
+ let view = - ray.direction;
210
+ var vertexData = ${ sampleTrianglePointFn }( hitResult.barycoord, hitResult.indices.xyz );
211
+ vertexData.normal = normalize( transpose( objectInfo.inverseMatrixWorld ) * vertexData.normal );
212
+ vertexData.tangent = vec4f( ( objectInfo.matrixWorld * vec4f( vertexData.tangent.xyz, 0.0 ) ).xyz, vertexData.tangent.w );
213
+ vertexData.position = objectInfo.matrixWorld * vertexData.position;
214
+
215
+ // blur glossy surfaces after low-probability bounces to suppress fireflies,
216
+ // from the Cycles "filter glossy" approach in integrator/surface_shader.h
217
+ // The smallest pdf seen along the path for the glossy filter is tracked below
218
+ let blurRoughness = sqrt( clamp( 1.0 - filterGlossy * minPdf, 0.0, 1.0 ) ) * 0.5;
219
+
220
+ var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
221
+
222
+ // Stochastically pass through partially transparent surfaces by restarting
223
+ // the ray at the hit point, advancing the rng but not the bounce count.
224
+ if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
225
+
226
+ // stop once the transparent bounces run out
227
+ if ( transparentBounce >= maxTransparentBounces ) {
228
+
229
+ break;
230
+
231
+ }
232
+
233
+ ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
234
+ ${ rngNextBounce }();
235
+ transparentBounce ++;
236
+ bounce --;
237
+ continue;
238
+
239
+ }
240
+
241
+ let isDispersive = materialInfo.dispersion > 0.0 && surface.ior > 1.0 && surface.transmission > 0.0 && ! surface.thinWall;
242
+ if ( isDispersive ) {
243
+
244
+ let wavelength = abs( dispersionWavelength );
245
+ ${ applyDispersionFunc }( &surface, materialInfo.dispersion, wavelength );
246
+ if ( dispersionWavelength < 0.0 ) {
247
+
248
+ dispersionWavelength = wavelength;
249
+ throughputColor *= ${ dispersionColorWeightFunc }( wavelength );
250
+
251
+ }
252
+
253
+ }
254
+
255
+ // attenuate the light transmitted through the volume when exiting a backface
256
+ if ( hitResult.side < 0.0 && materialInfo.transmission > 0.0 ) {
257
+
258
+ throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
259
+
260
+ }
261
+
262
+ // emission, clamped at the depth of the segment that found it, as in Cycles
263
+ let emission = ${ clampPathContributionFunc }( throughputColor * surface.emission, bounce, clampDirect, clampIndirect );
264
+ resultColor += vec4f( emission, 0.0 );
265
+
266
+ // next event estimation: draw one light or the environment, each with
267
+ // probability 1 / lightsDenom
268
+ if ( misEnabled != 0u && lightsDenom > 0.0 ) {
269
+
270
+ // pick one light or the environment with a single sample
271
+ // TODO: importance-sample the selection by light intensity and solid angle
272
+ let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
273
+ let lightIndex = min( u32( ruv.x * lightsDenom ), u32( lightsDenom ) - 1u );
274
+ var lightRec: ${ lightRecordStruct };
275
+ if ( envActive && lightIndex == lightsCount ) {
276
+
277
+ // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
278
+ let envSample = ${ sampleEnvDir }( ruv.yz );
279
+ lightRec.direction = envSample.direction;
280
+ lightRec.emission = envSample.color;
281
+ lightRec.pdf = envSample.pdf;
282
+ lightRec.dist = ${ LIGHT_FAR_DISTANCE };
283
+ lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
284
+
285
+ } else {
286
+
287
+ lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
288
+
289
+ }
290
+
291
+ if ( lightRec.pdf > 0.0 ) {
292
+
293
+ let evalRec = ${ bsdfEvalPdfFn }( view, lightRec.direction, surface );
294
+ if ( evalRec.pdf > 0.0 ) {
295
+
296
+ var shadowRay: ${ rayStruct };
297
+ shadowRay.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, lightRec.direction, hitResult.normal );
298
+ shadowRay.direction = lightRec.direction;
299
+ shadowRay.maxDist = lightRec.dist - ${ LIGHT_EPSILON };
300
+
301
+ // opaque occlusion up to the light distance. A shadow-specific any hit
302
+ // traversal could support tinted shadows from transmissive and partially
303
+ // opaque objects
304
+ var shadowHit: ${ raycastOutput };
305
+ let occluded = ${ raycastFirstHitFn }( shadowRay, &shadowHit );
306
+ if ( ! occluded ) {
307
+
308
+ var lightPdf = lightRec.pdf;
309
+ lightPdf /= lightsDenom;
310
+
311
+ // env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight
312
+ let misWeight = select( 1.0, ${ misHeuristicFn }( lightPdf, evalRec.pdf ), ${ isMISWeightLightFn }( lightRec.lightType ) );
313
+ let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight / lightPdf;
314
+ let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
315
+ resultColor += vec4f( contribution, 0.0 );
316
+
317
+ }
318
+
319
+ }
320
+
321
+ }
322
+
323
+ }
324
+
325
+ let scatterRec = ${ bsdfSampleFn }( view, surface );
326
+ if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
327
+
328
+ break;
329
+
330
+ }
331
+
332
+ isFullyTransmissive = isFullyTransmissive && scatterRec.isTransmissive;
333
+
334
+ // track the smallest pdf seen along the path for the glossy filter
335
+ minPdf = min( minPdf, scatterRec.pdf );
336
+
337
+ // russian roulette early out:
338
+ // Matches Cycles path_state_continuation_probability in integrator/path_state.h
339
+ if ( bounce >= 3u ) {
340
+
341
+ let rrThroughput = throughputColor * scatterRec.color / scatterRec.pdf;
342
+ let rrProb = saturate( sqrt( max( max( rrThroughput.r, rrThroughput.g ), rrThroughput.b ) ) );
343
+ if ( rrProb <= 0.0 || ${ rand1 }( ${ RNG_INDEX_RUSSIAN_ROULETTE } ) > rrProb ) {
344
+
345
+ break;
346
+
347
+ }
348
+
349
+ throughputColor /= rrProb;
350
+
351
+ }
352
+
353
+ throughputColor *= scatterRec.color;
354
+ throughputColor /= scatterRec.pdf;
355
+ bsdfPdf = scatterRec.pdf;
356
+
357
+ // exit if our throughput is 0.0
358
+ if ( all( throughputColor == vec3f( 0.0 ) ) ) {
359
+
360
+ break;
361
+
362
+ }
363
+
364
+ ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
365
+ ray.direction = scatterRec.direction;
366
+ ray.maxDist = 0.0;
367
+
368
+ } else {
369
+
370
+ if ( bounce > 0u && ! isFullyTransmissive ) {
371
+
372
+ var misWeight = 1.0;
373
+ if ( misEnabled != 0u && envActive ) {
374
+
375
+ // match the env pdf scaling used by the NEE selection so the two estimators balance
376
+ let envPdf = ${ getEnvDirPdf }( ray.direction ) / lightsDenom;
377
+ misWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
378
+
379
+ }
380
+
381
+ let environment = ${ sampleEnvColor }( ray.direction ).rgb * throughputColor * misWeight;
382
+ let contribution = ${ clampPathContributionFunc }( environment, bounce, clampDirect, clampIndirect );
383
+ resultColor += vec4f( contribution, 0.0 );
384
+
385
+ } else {
386
+
387
+ // hit the background
388
+ // support multiple transparent background blending techniques
389
+ let rng = ${ rand2 }( ${ RNG_INDEX_BACKGROUND_SAMPLE } );
390
+ let bg = ${ sampleBackground }( ray.direction, rng );
391
+ if ( bounce == 0u ) {
392
+
393
+ // sample the background directly if this is the primary ray
394
+ let background = ${ clampPathContributionFunc }( bg.a * bg.rgb, bounce, clampDirect, clampIndirect );
395
+ resultColor = vec4f( background, bg.a );
396
+
397
+ } else {
398
+
399
+ // transmissive ray handling
400
+ let env = ${ sampleEnvColor }( ray.direction );
401
+ let avg = saturate( dot( throughputColor, vec3f( 1.0 / 3.0 ) ) );
402
+ let transparency = ( 1.0 - bg.a ) * avg;
403
+ var envMisWeight = 1.0;
404
+ if ( misEnabled != 0u && ${ envTotalSumNode } > 0.0 ) {
405
+
406
+ let envPdf = ${ getEnvDirPdf }( ray.direction );
407
+ envMisWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
408
+
409
+ }
410
+
411
+ if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_ENVIRONMENT }u ) {
412
+
413
+ // display the env map through transmissive surfaces
414
+ let background = ${ clampPathContributionFunc }( env.rgb * throughputColor * envMisWeight, bounce, clampDirect, clampIndirect );
415
+ resultColor = vec4f(
416
+ resultColor.rgb + background,
417
+ 1.0,
418
+ );
419
+
420
+ } else if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_TRANSPARENT }u ) {
421
+
422
+ // fade the background by the throughput color average
423
+ let background = ${ clampPathContributionFunc }( bg.a * bg.rgb * throughputColor * envMisWeight, bounce, clampDirect, clampIndirect );
424
+ resultColor = vec4f(
425
+ resultColor.rgb + background,
426
+ 1.0 - transparency,
427
+ );
428
+
429
+ } else {
430
+
431
+ // fade the background by the throughput color average, mixing in env lighting
432
+ var light = mix( env.rgb, bg.rgb, bg.a ) * envMisWeight;
433
+ let background = ${ clampPathContributionFunc }( light * throughputColor, bounce, clampDirect, clampIndirect );
434
+ resultColor = vec4f(
435
+ resultColor.rgb + background,
436
+ 1.0 - transparency,
437
+ );
438
+
439
+ }
440
+
441
+ }
442
+
443
+ }
444
+
445
+ break;
446
+
447
+ }
448
+
449
+ ${ rngNextBounce }();
450
+
451
+ }
452
+
453
+ let nextSampleCount = sampleCount + 1;
454
+ // store the color rows top down to match a rasterized render target
455
+ let colorIndex = vec2u( indexUV.x, targetDimensions.y - 1u - indexUV.y );
456
+
457
+ let prevColor = textureLoad( ${ params.prevOutputTarget }, colorIndex );
458
+ let blendedColor = ${ weightedAlphaBlendFn }( prevColor, resultColor, 1.0 / f32( nextSampleCount ) );
459
+ textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
460
+ textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
461
+
462
+ }`;
463
+
464
+ super( shader( params ) );
465
+
466
+ this.defineUniformAccessors( params );
467
+
468
+ }
469
+
470
+ }
@@ -0,0 +1,51 @@
1
+ import { StorageTexture } from 'three/webgpu';
2
+ import { ComputeKernel } from './ComputeKernel.js';
3
+ import { textureStore, wgslFn, globalId, uniform } from 'three/tsl';
4
+ import { SAMPLE_ACTIVE_FLAG, SAMPLE_COUNT_MASK } from '../constants.js';
5
+
6
+ // Kernel for copying count + active flag to an output target for debug visualizations
7
+ export class SampleDebugKernel extends ComputeKernel {
8
+
9
+ constructor() {
10
+
11
+ const params = {
12
+ globalId: globalId,
13
+ displaySamples: uniform( true ),
14
+ inputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
15
+ outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
16
+ };
17
+
18
+ const fn = wgslFn( /* wgsl */`
19
+
20
+ fn compute(
21
+ globalId: vec3u,
22
+ inputTarget: texture_storage_2d<r32uint, read>,
23
+ outputTarget: texture_storage_2d<rgba8unorm, read_write>,
24
+ displaySamples: u32
25
+ ) -> void {
26
+
27
+ let combined = textureLoad( inputTarget, globalId.xy ).r;
28
+ let isActive = ( ${ SAMPLE_ACTIVE_FLAG }u & combined ) != 0u;
29
+ let samples = combined & ${ SAMPLE_COUNT_MASK }u;
30
+
31
+ if ( displaySamples != 0 ) {
32
+
33
+ textureStore( outputTarget, globalId.xy, vec4f( f32( samples ) * 0.01, 0, 0, 1.0 ) );
34
+
35
+ } else {
36
+
37
+ let v = f32( isActive );
38
+ textureStore( outputTarget, globalId.xy, vec4f( v, v, v, 1.0 ) );
39
+
40
+ }
41
+
42
+ }
43
+ ` )( params );
44
+
45
+ super( fn );
46
+
47
+ this.defineUniformAccessors( params );
48
+
49
+ }
50
+
51
+ }