three-gpu-pathtracer 0.0.24 → 0.0.25

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 +28 -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,371 @@
1
+ import { Vector2 } from 'three';
2
+ import { StorageBufferAttribute, StorageTexture } from 'three/webgpu';
3
+ import { ComputeKernel } from '../ComputeKernel.js';
4
+ import { uniform, storage, textureStore, globalId } from 'three/tsl';
5
+ import { proxy, proxyFn, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
6
+ import { rngInit, rand1, rand2, RNG_INDEX_RAY_JITTER, RNG_INDEX_ALPHA_TEST, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH } from '../../nodes/random.wgsl.js';
7
+ import { rayDataStruct, rayQueueAtomicStruct, pixelQueueStruct } from './structs.js';
8
+ import { SAMPLE_ACTIVE_FLAG, SAMPLE_COUNT_MASK, SAMPLE_DISPATCHED_FLAG } from '../../constants.js';
9
+ import { applyDispersionFunc, dispersionColorWeightFunc, DISPERSION_MIN_WAVELENGTH, DISPERSION_MAX_WAVELENGTH, transmissionAttenuationFunc } from '../../nodes/material.wgsl.js';
10
+ import { isTerminatingScatterFunc, offsetRayOriginFunc } from '../../nodes/utils.wgsl.js';
11
+ import { LIGHT_EPSILON } from '../../nodes/lights.wgsl.js';
12
+
13
+ // Pure material evaluation and ray generation: terminated slots pull a recycled pixel and emit a
14
+ // fresh camera ray; live slots evaluate the surface staged by LogicKernel, sample the bsdf, and
15
+ // enqueue the next bounce ray plus the NEE shadow ray toward the light LogicKernel selected.
16
+ export class MaterialKernel extends ComputeKernel {
17
+
18
+ constructor( ) {
19
+
20
+ const params = {
21
+ bvhData: { value: null },
22
+ material: { value: null },
23
+
24
+ seed: uniform( 0, 'uint' ),
25
+ targetDimensions: uniform( new Vector2() ),
26
+ maxSamples: uniform( 0, 'uint' ),
27
+ rayCount: uniform( 0, 'uint' ),
28
+ filterGlossy: uniform( 1 ),
29
+ maxTransparentBounces: uniform( 5, 'uint' ),
30
+ maxBounces: uniform( 5, 'uint' ),
31
+
32
+ sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
33
+
34
+ rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
35
+ rayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueAtomicStruct ),
36
+ shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueAtomicStruct ),
37
+ pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), pixelQueueStruct ),
38
+
39
+ globalId: globalId,
40
+ };
41
+
42
+ const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
43
+ const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
44
+ const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
45
+ const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
46
+ const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
47
+
48
+ const fn = wgslTagFn/* wgsl */`
49
+
50
+ fn compute(
51
+ seed: u32,
52
+ targetDimensions: vec2u,
53
+ maxSamples: u32,
54
+ rayCount: u32,
55
+ filterGlossy: f32,
56
+ maxTransparentBounces: u32,
57
+ maxBounces: u32,
58
+
59
+ globalId: vec3u
60
+ ) -> void {
61
+
62
+ let rayDataStorage = &${ params.rayDataStorage };
63
+ let rayQueue = &${ params.rayQueue };
64
+ let shadowRayQueue = &${ params.shadowRayQueue };
65
+ let pixelQueue = &${ params.pixelQueue };
66
+
67
+ let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
68
+ let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
69
+
70
+ // bound by "rayCount" rather than the pool length. The dispatch rounds up to the
71
+ // workgroup size and those extra slots hold a zeroed pixel index
72
+ let index = globalId.x;
73
+ if ( index >= rayCount ) {
74
+
75
+ return;
76
+
77
+ }
78
+
79
+ let input = ( *rayDataStorage )[ index ];
80
+ if ( input.objectIndex < 0 ) {
81
+
82
+ // the slot's path has terminated: recycle the pixel through the overflow queue and
83
+ // generate a fresh camera ray
84
+ var pixelIndex = input.pixelIndex;
85
+ let elementCount = atomicLoad( &pixelQueue.elementCount );
86
+ if ( elementCount > 0u ) {
87
+
88
+ // TODO: If we've pulled off a pixel that's already finished we currently just
89
+ // write a no-op ray, wasting a frame. It may be better to iterate over a few
90
+ // points in the queue to see if we can find one we can use.
91
+ let queueIndex = atomicAdd( &pixelQueue.current, 1u ) % elementCount;
92
+ pixelIndex = atomicExchange( &pixelQueue.elements[ queueIndex ], pixelIndex );
93
+
94
+ }
95
+
96
+ let indexUV = vec2u( pixelIndex >> 16, pixelIndex & 0xFFFF );
97
+
98
+ // skip the pixel if it has hit the sample limit
99
+ let combinedField = textureLoad( ${ params.sampleCountTarget }, indexUV ).r;
100
+ let samples = ( ${ SAMPLE_COUNT_MASK }u & combinedField );
101
+ let isComplete = maxSamples != 0u && samples >= maxSamples;
102
+
103
+ if ( isComplete ) {
104
+
105
+ rayDataStorage[ index ].pixelIndex = pixelIndex;
106
+ rayDataStorage[ index ].rayIntersectionIndex = - 1;
107
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
108
+ return;
109
+
110
+ }
111
+
112
+ ${ rngInit }( indexUV, seed + samples, 0 );
113
+
114
+ let uv = vec2f( indexUV ) / vec2f( targetDimensions );
115
+ let jitteredUv = uv + ${ rand2 }( ${ RNG_INDEX_RAY_JITTER } ) / vec2f( targetDimensions );
116
+ var ray: ${ rayStruct };
117
+ if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
118
+
119
+ // the camera declined the pixel, so leave the slot dormant for this round
120
+ // TODO: same as above - this work is a bit wasteful and leaves slots empty for
121
+ // a frame. Is it possible to quickly skip rays outside the mask or are finished?
122
+ rayDataStorage[ index ].pixelIndex = pixelIndex;
123
+ rayDataStorage[ index ].rayIntersectionIndex = - 1;
124
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
125
+ return;
126
+
127
+ }
128
+
129
+ ray.direction = normalize( ray.direction );
130
+
131
+ let rayIndex = atomicAdd( &rayQueue.length, 1u );
132
+ rayQueue.elements[ rayIndex ].origin = ray.origin;
133
+ rayQueue.elements[ rayIndex ].direction = ray.direction;
134
+ rayQueue.elements[ rayIndex ].pixelIndex = pixelIndex;
135
+ rayQueue.elements[ rayIndex ].currentBounce = 0u;
136
+ rayQueue.elements[ rayIndex ].seed = seed + samples;
137
+ rayQueue.elements[ rayIndex ].alphaDepth = 0u;
138
+ rayQueue.elements[ rayIndex ].maxDist = ray.maxDist;
139
+
140
+ rayDataStorage[ index ].origin = ray.origin;
141
+ rayDataStorage[ index ].direction = ray.direction;
142
+ rayDataStorage[ index ].pixelIndex = pixelIndex;
143
+ rayDataStorage[ index ].seed = seed + samples;
144
+ rayDataStorage[ index ].currentBounce = 0u;
145
+ rayDataStorage[ index ].throughputColor = vec3f( 1.0 );
146
+ rayDataStorage[ index ].resultColor = vec4f( 0.0, 0.0, 0.0, 1.0 );
147
+ rayDataStorage[ index ].scatterColor = vec3f( 1.0 );
148
+ rayDataStorage[ index ].scatterPdf = 1.0;
149
+ rayDataStorage[ index ].minPdf = 1.0;
150
+ rayDataStorage[ index ].isFullyTransmissive = 1u;
151
+ rayDataStorage[ index ].emission = vec3f( 0.0 );
152
+ rayDataStorage[ index ].lightPdf = 0.0;
153
+ rayDataStorage[ index ].alphaDepth = 0u;
154
+ rayDataStorage[ index ].maxDist = ray.maxDist;
155
+ rayDataStorage[ index ].rayIntersectionIndex = i32( rayIndex );
156
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
157
+ rayDataStorage[ index ].dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
158
+
159
+ // write the active params & dispatched flag
160
+ textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ${ SAMPLE_ACTIVE_FLAG }u | ${ SAMPLE_DISPATCHED_FLAG }u | samples ) );
161
+
162
+ } else {
163
+
164
+ // evaluate the surface staged by LogicKernel
165
+ let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
166
+ ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
167
+
168
+ let objectInfo = transforms[ u32( input.objectIndex ) ];
169
+ var materialInfo = ( *materials )[ objectInfo.materialIndex ];
170
+
171
+ // a matte surface hit by the camera ray renders as a fully transparent
172
+ let isMatte = materialInfo.matte != 0 && input.currentBounce == 0u;
173
+ if ( isMatte ) {
174
+
175
+ rayDataStorage[ index ].resultColor = vec4f( 0.0 );
176
+ rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
177
+ rayDataStorage[ index ].emission = vec3f( 0.0 );
178
+ rayDataStorage[ index ].lightPdf = 0.0;
179
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
180
+ return;
181
+
182
+ }
183
+
184
+ // apply per-object colors
185
+ materialInfo.color *= objectInfo.color.rgb;
186
+ materialInfo.opacity *= objectInfo.color.a;
187
+
188
+ var vertexData = ${ sampleTrianglePointFn }( input.barycoord, input.indices );
189
+ vertexData.normal = normalize( transpose( objectInfo.inverseMatrixWorld ) * vertexData.normal );
190
+ vertexData.tangent = vec4f( ( objectInfo.matrixWorld * vec4f( vertexData.tangent.xyz, 0.0 ) ).xyz, vertexData.tangent.w );
191
+ vertexData.position = objectInfo.matrixWorld * vertexData.position;
192
+
193
+ let view = - input.direction;
194
+
195
+ // blur glossy surfaces after low-probability bounces to suppress fireflies,
196
+ // from the Cycles "filter glossy" approach in integrator/surface_shader.h
197
+ let blurRoughness = sqrt( clamp( 1.0 - filterGlossy * input.minPdf, 0.0, 1.0 ) ) * 0.5;
198
+
199
+ var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, input.side, input.normal, view, blurRoughness );
200
+
201
+ // Stochastically pass through partially transparent surfaces by re-enqueueing
202
+ // the ray at the hit point, advancing the alpha depth but not the bounce count.
203
+ let passesThrough = ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity;
204
+ if ( passesThrough ) {
205
+
206
+ // out of transparent bounces, so stop rather than shading a surface that
207
+ // should be invisible. A zeroed throughput terminates in LogicKernel.
208
+ if ( input.alphaDepth >= maxTransparentBounces ) {
209
+
210
+ rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
211
+ rayDataStorage[ index ].emission = vec3f( 0.0 );
212
+ rayDataStorage[ index ].lightPdf = 0.0;
213
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
214
+ return;
215
+
216
+ }
217
+
218
+ let alphaIndex = atomicAdd( &rayQueue.length, 1u );
219
+ rayQueue.elements[ alphaIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.direction, input.normal );
220
+ rayQueue.elements[ alphaIndex ].direction = input.direction;
221
+ rayQueue.elements[ alphaIndex ].pixelIndex = input.pixelIndex;
222
+ rayQueue.elements[ alphaIndex ].currentBounce = input.currentBounce;
223
+ rayQueue.elements[ alphaIndex ].seed = input.seed;
224
+ rayQueue.elements[ alphaIndex ].alphaDepth = input.alphaDepth + 1u;
225
+ // the origin advanced to the hit point, so the remaining budget shrinks by the
226
+ // distance already traced
227
+ rayQueue.elements[ alphaIndex ].maxDist = max( input.maxDist - input.dist, 0.0 );
228
+
229
+ // the surface is skipped, so no scatter or emission is staged for LogicKernel.
230
+ // "pdf" is left alone so the previous scatter still weights the forward MIS,
231
+ // and "bsdf" matches it so applying the scatter leaves the throughput as is.
232
+ rayDataStorage[ index ].alphaDepth = input.alphaDepth + 1u;
233
+ rayDataStorage[ index ].emission = vec3f( 0.0 );
234
+ rayDataStorage[ index ].scatterColor = vec3f( input.scatterPdf );
235
+ rayDataStorage[ index ].lightPdf = 0.0;
236
+ rayDataStorage[ index ].origin = rayQueue.elements[ alphaIndex ].origin;
237
+ rayDataStorage[ index ].rayIntersectionIndex = i32( alphaIndex );
238
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
239
+ return;
240
+
241
+ }
242
+
243
+ // apply the hero wavelength to dispersive surfaces, folding the spectral weight
244
+ // into the throughput at the path's first dispersive interaction
245
+ var throughputColor = input.throughputColor;
246
+ let isDispersive = materialInfo.dispersion > 0.0 && surface.ior > 1.0 && surface.transmission > 0.0 && ! surface.thinWall;
247
+ if ( isDispersive ) {
248
+
249
+ let wavelength = abs( input.dispersionWavelength );
250
+ ${ applyDispersionFunc }( &surface, materialInfo.dispersion, wavelength );
251
+ if ( input.dispersionWavelength < 0.0 ) {
252
+
253
+ rayDataStorage[ index ].dispersionWavelength = wavelength;
254
+ throughputColor *= ${ dispersionColorWeightFunc }( wavelength );
255
+ rayDataStorage[ index ].throughputColor = throughputColor;
256
+
257
+ }
258
+
259
+ }
260
+
261
+ // attenuate the light transmitted through the volume when exiting a backface so
262
+ // the surface's emission and NEE resolve against the attenuated throughput
263
+ if ( input.side < 0.0 && materialInfo.transmission > 0.0 ) {
264
+
265
+ throughputColor *= ${ transmissionAttenuationFunc }( input.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
266
+ rayDataStorage[ index ].throughputColor = throughputColor;
267
+
268
+ }
269
+
270
+ // sample the next bounce direction and stage the scatter state for LogicKernel
271
+ var scatterRec = ${ bsdfSampleFn }( view, surface );
272
+ let newBounce = input.currentBounce + 1u;
273
+
274
+ // decide termination now so finished paths skip the bounce trace entirely - a
275
+ // zeroed pdf reads as a terminating scatter in LogicKernel, which still resolves
276
+ // the surface's emission and NEE before freeing the slot
277
+ var isTerminated = newBounce >= maxBounces || all( scatterRec.color == vec3f( 0.0 ) ) || ${ isTerminatingScatterFunc }( scatterRec );
278
+
279
+ // russian roulette early out:
280
+ // Matches Cycles path_state_continuation_probability in integrator/path_state.h
281
+ if ( ! isTerminated && newBounce >= 3u ) {
282
+
283
+ let rrThroughput = throughputColor * scatterRec.color / scatterRec.pdf;
284
+ let rrProb = saturate( sqrt( max( max( rrThroughput.r, rrThroughput.g ), rrThroughput.b ) ) );
285
+ isTerminated = rrProb <= 0.0 || ${ rand1 }( ${ RNG_INDEX_RUSSIAN_ROULETTE } ) > rrProb;
286
+ if ( ! isTerminated ) {
287
+
288
+ // fold the survival boost into the scatter color so LogicKernel's
289
+ // throughput update applies it without a separate division
290
+ scatterRec.color /= rrProb;
291
+
292
+ }
293
+
294
+ }
295
+
296
+ // Write the ray storage content here since things like the emissive value is read
297
+ // in the logic kernel on the subsequent frame.
298
+ rayDataStorage[ index ].scatterColor = scatterRec.color;
299
+ rayDataStorage[ index ].scatterPdf = select( scatterRec.pdf, 0.0, isTerminated );
300
+ rayDataStorage[ index ].minPdf = min( input.minPdf, scatterRec.pdf );
301
+ rayDataStorage[ index ].isFullyTransmissive = input.isFullyTransmissive & select( 0u, 1u, scatterRec.isTransmissive );
302
+ rayDataStorage[ index ].emission = surface.emission;
303
+ rayDataStorage[ index ].currentBounce = newBounce;
304
+
305
+ // the NEE shadow ray below still resolves the surface's direct light, so only
306
+ // the bounce segment is skipped for finished paths
307
+ if ( ! isTerminated ) {
308
+
309
+ let rayIndex = atomicAdd( &rayQueue.length, 1u );
310
+ rayQueue.elements[ rayIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, input.normal );
311
+ rayQueue.elements[ rayIndex ].direction = scatterRec.direction;
312
+ rayQueue.elements[ rayIndex ].pixelIndex = input.pixelIndex;
313
+ rayQueue.elements[ rayIndex ].currentBounce = newBounce;
314
+ rayQueue.elements[ rayIndex ].seed = input.seed;
315
+ rayQueue.elements[ rayIndex ].alphaDepth = input.alphaDepth;
316
+ rayQueue.elements[ rayIndex ].maxDist = 0.0;
317
+ rayDataStorage[ index ].rayIntersectionIndex = i32( rayIndex );
318
+
319
+ rayDataStorage[ index ].origin = rayQueue.elements[ rayIndex ].origin;
320
+ rayDataStorage[ index ].direction = scatterRec.direction;
321
+
322
+ }
323
+
324
+ // evaluate the bsdf toward the light LogicKernel selected and enqueue the shadow ray.
325
+ // the light pdf will be 0 if NEE is disabled.
326
+ var lightPdf = input.lightPdf;
327
+ if ( lightPdf > 0.0 ) {
328
+
329
+ let evalRec = ${ bsdfEvalPdfFn }( view, input.lightDirection, surface );
330
+ if ( evalRec.pdf > 0.0 ) {
331
+
332
+ rayDataStorage[ index ].lightBsdf = evalRec.color;
333
+ rayDataStorage[ index ].lightBsdfPdf = evalRec.pdf;
334
+
335
+ let shadowIndex = atomicAdd( &shadowRayQueue.length, 1u );
336
+ shadowRayQueue.elements[ shadowIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.lightDirection, input.normal );
337
+ shadowRayQueue.elements[ shadowIndex ].direction = input.lightDirection;
338
+ shadowRayQueue.elements[ shadowIndex ].pixelIndex = input.pixelIndex;
339
+ shadowRayQueue.elements[ shadowIndex ].currentBounce = input.currentBounce;
340
+ shadowRayQueue.elements[ shadowIndex ].seed = input.seed;
341
+ shadowRayQueue.elements[ shadowIndex ].alphaDepth = input.alphaDepth;
342
+ shadowRayQueue.elements[ shadowIndex ].maxDist = input.lightDist - ${ LIGHT_EPSILON };
343
+ rayDataStorage[ index ].shadowRayIntersectionIndex = i32( shadowIndex );
344
+
345
+ } else {
346
+
347
+ lightPdf = 0.0;
348
+
349
+ }
350
+
351
+ }
352
+
353
+ if ( lightPdf <= 0.0 ) {
354
+
355
+ rayDataStorage[ index ].lightPdf = 0.0;
356
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
357
+
358
+ }
359
+
360
+ }
361
+
362
+ }
363
+ `;
364
+
365
+ super( fn( params ) );
366
+
367
+ this.defineUniformAccessors( params );
368
+
369
+ }
370
+
371
+ }
@@ -0,0 +1,72 @@
1
+ import { Vector2 } from 'three';
2
+ import { StorageBufferAttribute } from 'three/webgpu';
3
+ import { uniform, storage, globalId } from 'three/tsl';
4
+ import { ComputeKernel } from '../ComputeKernel.js';
5
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
6
+ import { rayDataStruct, pixelQueueNonAtomicStruct } from './structs.js';
7
+
8
+ // Runs once per reset: assigns each of the first rayData-pool-count pixels to a path slot and parks
9
+ // the overflow pixel indices in the pixel queue. Slots are initialized so LogicKernel skips them and
10
+ // MaterialKernel immediately generates fresh camera rays. Later budget changes are handled by
11
+ // resizing the pool in the render loop, with ResetSlotsKernel returning retired slots' pixels.
12
+ export class PopulatePixelIndicesKernel extends ComputeKernel {
13
+
14
+ constructor( ) {
15
+
16
+ const params = {
17
+ rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
18
+ pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), pixelQueueNonAtomicStruct ),
19
+ targetDimensions: uniform( new Vector2() ),
20
+ frameBudget: uniform( 0, 'uint' ),
21
+ globalId: globalId,
22
+ };
23
+
24
+ const fn = wgslTagFn/* wgsl */`
25
+ fn compute( targetDimensions: vec2u, frameBudget: u32, globalId: vec3u ) -> void {
26
+
27
+ let rayDataStorage = &${ params.rayDataStorage };
28
+ let pixelQueue = &${ params.pixelQueue };
29
+
30
+ if ( globalId.x >= targetDimensions.x || globalId.y >= targetDimensions.y ) {
31
+
32
+ return;
33
+
34
+ }
35
+
36
+ let rayCount = min( frameBudget, arrayLength( rayDataStorage ) );
37
+ let pixelCount = targetDimensions.x * targetDimensions.y;
38
+ if ( globalId.x == 0u && globalId.y == 0u ) {
39
+
40
+ pixelQueue.current = 0u;
41
+ pixelQueue.elementCount = select( 0u, pixelCount - rayCount, pixelCount >= rayCount );
42
+
43
+ }
44
+
45
+ let pixelHash = ( globalId.x << 16 ) | globalId.y;
46
+ let pixelIndex = globalId.x + globalId.y * targetDimensions.x;
47
+ if ( pixelIndex < rayCount ) {
48
+
49
+ rayDataStorage[ pixelIndex ].pixelIndex = pixelHash;
50
+ rayDataStorage[ pixelIndex ].resultColor = vec4f( 0.0 );
51
+ rayDataStorage[ pixelIndex ].throughputColor = vec3f( 0.0 );
52
+ rayDataStorage[ pixelIndex ].objectIndex = - 1;
53
+ rayDataStorage[ pixelIndex ].alphaDepth = 0u;
54
+ rayDataStorage[ pixelIndex ].rayIntersectionIndex = - 1;
55
+ rayDataStorage[ pixelIndex ].shadowRayIntersectionIndex = - 1;
56
+
57
+ } else {
58
+
59
+ pixelQueue.elements[ pixelIndex - rayCount ] = pixelHash;
60
+
61
+ }
62
+
63
+ }
64
+ `;
65
+
66
+ super( fn( params ) );
67
+
68
+ this.defineUniformAccessors( params );
69
+
70
+ }
71
+
72
+ }
@@ -0,0 +1,38 @@
1
+ import { IndirectStorageBufferAttribute, StorageBufferAttribute } from 'three/webgpu';
2
+ import { storage } from 'three/tsl';
3
+ import { ComputeKernel } from '../ComputeKernel.js';
4
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+ import { rayQueueAtomicStruct } from './structs.js';
6
+
7
+ // Converts a queue's atomic length counter into indirect dispatch arguments so the trace kernels can
8
+ // be dispatched with exactly as many threads as there are queued rays.
9
+ export class QueueLengthToDispatchKernel extends ComputeKernel {
10
+
11
+ constructor( queueStruct = rayQueueAtomicStruct, workgroupSize = 64 ) {
12
+
13
+ const params = {
14
+ queue: storage( new StorageBufferAttribute( 1, 1 ), queueStruct ),
15
+ outputDispatch: storage( new IndirectStorageBufferAttribute( 3, 1 ), 'u32' ).setName( 'outputDispatch' ),
16
+ };
17
+
18
+ const fn = wgslTagFn/* wgsl */`
19
+ fn compute() -> void {
20
+
21
+ let queue = &${ params.queue };
22
+ let outputDispatch = &${ params.outputDispatch };
23
+
24
+ let queueLength = atomicLoad( &queue.length );
25
+ outputDispatch[ 0 ] = ( queueLength + ${ workgroupSize - 1 }u ) / ${ workgroupSize }u;
26
+ outputDispatch[ 1 ] = 1u;
27
+ outputDispatch[ 2 ] = 1u;
28
+
29
+ }
30
+ `;
31
+
32
+ super( fn( params ) );
33
+
34
+ this.defineUniformAccessors( params );
35
+
36
+ }
37
+
38
+ }
@@ -0,0 +1,66 @@
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
+ import { rayDataStruct, pixelQueueStruct } from './structs.js';
6
+
7
+ // Moves pixels between a range of path slots and the queue when the pool is resized. Slots added
8
+ // by a grow take a pixel from the queue tail and start idle, as MaterialKernel expects. Slots
9
+ // dropped by a shrink push their pixel onto the tail before the pool is discarded. Runs between
10
+ // frames, so nothing else touches the queue meanwhile.
11
+ export class ResetSlotsKernel extends ComputeKernel {
12
+
13
+ constructor( ) {
14
+
15
+ const params = {
16
+ rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
17
+ pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), pixelQueueStruct ),
18
+ start: uniform( 0, 'uint' ),
19
+ end: uniform( 0, 'uint' ),
20
+ addingSlots: uniform( 0, 'uint' ),
21
+ globalId: globalId,
22
+ };
23
+
24
+ const fn = wgslTagFn/* wgsl */`
25
+ fn compute( start: u32, end: u32, addingSlots: u32, globalId: vec3u ) -> void {
26
+
27
+ let rayDataStorage = &${ params.rayDataStorage };
28
+ let pixelQueue = &${ params.pixelQueue };
29
+
30
+ let index = start + globalId.x;
31
+ if ( index >= end ) {
32
+
33
+ return;
34
+
35
+ }
36
+
37
+ if ( addingSlots == 0u ) {
38
+
39
+ // slots being removed hand their pixel back before the pool is discarded
40
+ let queueIndex = atomicAdd( &pixelQueue.elementCount, 1u );
41
+ atomicStore( &pixelQueue.elements[ queueIndex ], rayDataStorage[ index ].pixelIndex );
42
+
43
+ } else {
44
+
45
+ // the pool never exceeds the pixel count, so the queue holds enough for every new slot
46
+ let previousCount = atomicSub( &pixelQueue.elementCount, 1u );
47
+ rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue.elements[ previousCount - 1u ] );
48
+ rayDataStorage[ index ].resultColor = vec4f( 0.0 );
49
+ rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
50
+ rayDataStorage[ index ].objectIndex = - 1;
51
+ rayDataStorage[ index ].alphaDepth = 0u;
52
+ rayDataStorage[ index ].rayIntersectionIndex = - 1;
53
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
54
+
55
+ }
56
+
57
+ }
58
+ `;
59
+
60
+ super( fn( params ) );
61
+
62
+ this.defineUniformAccessors( params );
63
+
64
+ }
65
+
66
+ }
@@ -0,0 +1,71 @@
1
+ import { StorageBufferAttribute } from 'three/webgpu';
2
+ import { ComputeKernel } from '../ComputeKernel.js';
3
+ import { storage, globalId } from 'three/tsl';
4
+ import { proxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+ import { rngInit } from '../../nodes/random.wgsl.js';
6
+ import { rayQueueStruct, intersectionResultStruct } from './structs.js';
7
+
8
+ // Pure BVH traversal over the queued bounce rays: one thread per queued ray, writing a compact
9
+ // intersection result at the ray's queue index for LogicKernel to consume next frame.
10
+ export class TraceRayKernel extends ComputeKernel {
11
+
12
+ constructor( ) {
13
+
14
+ const params = {
15
+ bvhData: { value: null },
16
+
17
+ rayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueStruct ),
18
+ rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
19
+
20
+ globalId: globalId,
21
+ };
22
+
23
+ const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
24
+ const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params );
25
+
26
+ const fn = wgslTagFn /* wgsl */`
27
+
28
+ fn compute( globalId: vec3u ) -> void {
29
+
30
+ let rayQueue = &${ params.rayQueue };
31
+ let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
32
+
33
+ let index = globalId.x;
34
+ if ( index >= rayQueue.length ) {
35
+
36
+ return;
37
+
38
+ }
39
+
40
+ let queuedRay = ( *rayQueue ).elements[ index ];
41
+ let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
42
+ ${ rngInit }( indexUV, queuedRay.seed, queuedRay.currentBounce + queuedRay.alphaDepth );
43
+
44
+ let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
45
+ var hitResult: ${ raycastOutput };
46
+ if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) {
47
+
48
+ rayIntersectionsStorage[ index ].barycoord = hitResult.barycoord;
49
+ rayIntersectionsStorage[ index ].objectIndex = i32( hitResult.objectIndex );
50
+ rayIntersectionsStorage[ index ].position = ray.origin + ray.direction * hitResult.dist;
51
+ rayIntersectionsStorage[ index ].dist = hitResult.dist;
52
+ rayIntersectionsStorage[ index ].normal = hitResult.normal.xyz;
53
+ rayIntersectionsStorage[ index ].side = hitResult.side;
54
+ rayIntersectionsStorage[ index ].indices = hitResult.indices.xyz;
55
+
56
+ } else {
57
+
58
+ rayIntersectionsStorage[ index ].objectIndex = - 1;
59
+
60
+ }
61
+
62
+ }
63
+ `;
64
+
65
+ super( fn( params ) );
66
+
67
+ this.defineUniformAccessors( params );
68
+
69
+ }
70
+
71
+ }
@@ -0,0 +1,67 @@
1
+ import { StorageBufferAttribute } from 'three/webgpu';
2
+ import { ComputeKernel } from '../ComputeKernel.js';
3
+ import { storage, globalId } from 'three/tsl';
4
+ import { proxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+ import { rngInit } from '../../nodes/random.wgsl.js';
6
+ import { rayQueueStruct, intersectionResultStruct } from './structs.js';
7
+
8
+ // Pure BVH traversal over the queued shadow rays. Uses the same first-hit traversal as the bounce
9
+ // rays ( no dedicated any-hit traversal exists yet ); LogicKernel decides occlusion by comparing the
10
+ // hit distance against the light distance.
11
+ export class TraceShadowRayKernel extends ComputeKernel {
12
+
13
+ constructor( ) {
14
+
15
+ const params = {
16
+ bvhData: { value: null },
17
+
18
+ shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueStruct ),
19
+ shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
20
+
21
+ globalId: globalId,
22
+ };
23
+
24
+ const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
25
+ const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params );
26
+
27
+ const fn = wgslTagFn /* wgsl */`
28
+
29
+ fn compute( globalId: vec3u ) -> void {
30
+
31
+ let shadowRayQueue = &${ params.shadowRayQueue };
32
+ let shadowRayIntersectionsStorage = &${ params.shadowRayIntersectionsStorage };
33
+
34
+ let index = globalId.x;
35
+ if ( index >= shadowRayQueue.length ) {
36
+
37
+ return;
38
+
39
+ }
40
+
41
+ let queuedRay = ( *shadowRayQueue ).elements[ index ];
42
+ let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
43
+ ${ rngInit }( indexUV, queuedRay.seed, queuedRay.currentBounce + queuedRay.alphaDepth );
44
+
45
+ let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
46
+ var hitResult: ${ raycastOutput };
47
+ if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) {
48
+
49
+ shadowRayIntersectionsStorage[ index ].objectIndex = i32( hitResult.objectIndex );
50
+ shadowRayIntersectionsStorage[ index ].dist = hitResult.dist;
51
+
52
+ } else {
53
+
54
+ shadowRayIntersectionsStorage[ index ].objectIndex = - 1;
55
+
56
+ }
57
+
58
+ }
59
+ `;
60
+
61
+ super( fn( params ) );
62
+
63
+ this.defineUniformAccessors( params );
64
+
65
+ }
66
+
67
+ }