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,586 @@
1
+ import { Matrix4, StorageBufferAttribute, Vector2 } from 'three/webgpu';
2
+ import { PopulatePixelIndicesKernel } from './compute/wavefront/PopulatePixelIndicesKernel.js';
3
+ import { LogicKernel } from './compute/wavefront/LogicKernel.js';
4
+ import { MaterialKernel } from './compute/wavefront/MaterialKernel.js';
5
+ import { TraceRayKernel } from './compute/wavefront/TraceRayKernel.js';
6
+ import { TraceShadowRayKernel } from './compute/wavefront/TraceShadowRayKernel.js';
7
+ import { QueueLengthToDispatchKernel } from './compute/wavefront/QueueLengthToDispatchKernel.js';
8
+ import { ResetSlotsKernel } from './compute/wavefront/ResetSlotsKernel.js';
9
+ import { ZeroOutBufferKernel } from './compute/ZeroOutBufferKernel.js';
10
+ import { CopyBufferKernel } from './compute/CopyBufferKernel.js';
11
+ import { EquirectHdrInfoNode } from './EquirectHdrInfoNode.js';
12
+ import { EquirectBackgroundInfo } from './EquirectBackgroundInfo.js';
13
+ import { LightsInfoNode } from './LightsInfoNode.js';
14
+ import { rayDataStruct, traceQueuedRayStruct, intersectionResultStruct, rayQueueStruct, pixelQueueStruct } from './compute/wavefront/structs.js';
15
+ import { PathTracerBackend } from './PathTracerBackend.js';
16
+ import { FILTER_GLOSSY_DISABLED } from './nodes/material.wgsl.js';
17
+ import {
18
+ PrimeSampleCountersKernel,
19
+ TallySampleCountsKernel,
20
+ SAMPLE_COUNTER_LENGTH,
21
+ SAMPLE_COUNTER_MAX,
22
+ SAMPLE_COUNTER_MIN,
23
+ SAMPLE_COUNTER_PIXEL_COUNT,
24
+ SAMPLE_COUNTER_TOTAL_HI,
25
+ SAMPLE_COUNTER_TOTAL_LO,
26
+ U32_RANGE,
27
+ } from './compute/TallySampleCountsKernel.js';
28
+
29
+ // set the buffers to the max possible size supported by default (128MB)
30
+ // TODO: this can be increased based on platform.
31
+ const MAX_BUFFER_SIZE = 134217728;
32
+
33
+ // the persistent path-slot pool is sized off the ray data struct; every other per-slot buffer
34
+ // ( queues, intersections ) is sized to the same count so a full pool can never overflow them
35
+ const MAX_RAY_DATA_COUNT = Math.floor( MAX_BUFFER_SIZE / ( rayDataStruct.getLength() * 4 ) );
36
+
37
+ const LOW_RES_ITERATIONS = 5;
38
+
39
+ export class WaveFrontPathTracer extends PathTracerBackend {
40
+
41
+ constructor( renderer ) {
42
+
43
+ super( renderer );
44
+
45
+ // options
46
+ this.seed = 0;
47
+ this.envInfo = new EquirectHdrInfoNode();
48
+ this.backgroundInfo = new EquirectBackgroundInfo();
49
+ this.lightsInfo = new LightsInfoNode();
50
+
51
+ // The path slot pool, sized to the frame budget and resized in the render loop as the budget
52
+ // changes. Every per-slot buffer is allocated to the same count so a full pool can never
53
+ // overflow them.
54
+ this.rayCount = 0;
55
+
56
+ // persistent per-path state, one slot per in-flight path
57
+ this.rayDataStorage = null;
58
+
59
+ // append-only trace queues, prefixed by an atomic length header
60
+ this.rayQueue = null;
61
+ this.shadowRayQueue = null;
62
+
63
+ // per-queue-slot trace results, indexed by the ray's position in its queue
64
+ this.rayIntersectionsStorage = null;
65
+ this.shadowRayIntersectionsStorage = null;
66
+
67
+ // pixel indices waiting for a free path slot, sized to the resolution
68
+ this.pixelQueue = null;
69
+
70
+ // reduction target for the per pixel sample counts, read back asynchronously
71
+ this.sampleCountersStorage = new StorageBufferAttribute( new Uint32Array( SAMPLE_COUNTER_LENGTH ), SAMPLE_COUNTER_LENGTH );
72
+ this.sampleCountersStorage.name = 'Sample Counters';
73
+ this._samplesPromise = null;
74
+
75
+ // kernels
76
+ this.populatePixelIndicesKernel = new PopulatePixelIndicesKernel().setWorkgroupSize( 8, 8, 1 );
77
+ this.logicKernel = new LogicKernel().setWorkgroupSize( 64, 1, 1 );
78
+ this.materialKernel = new MaterialKernel().setWorkgroupSize( 64, 1, 1 );
79
+ this.traceRayKernel = new TraceRayKernel().setWorkgroupSize( 64, 1, 1 );
80
+ this.traceShadowRayKernel = new TraceShadowRayKernel().setWorkgroupSize( 64, 1, 1 );
81
+ this.rayDispatchConverter = new QueueLengthToDispatchKernel().setWorkgroupSize( 1, 1, 1 );
82
+ this.shadowDispatchConverter = new QueueLengthToDispatchKernel().setWorkgroupSize( 1, 1, 1 );
83
+ this.primeSampleCountersKernel = new PrimeSampleCountersKernel().setWorkgroupSize( 1, 1, 1 );
84
+ this.tallySampleCountsKernel = new TallySampleCountsKernel().setWorkgroupSize( 8, 8, 1 );
85
+
86
+ // pool resizing: carry live slots into a reallocated pool and idle the rest
87
+ this.resetSlotsKernel = new ResetSlotsKernel().setWorkgroupSize( 64, 1, 1 );
88
+ this.copyRayDataKernel = new CopyBufferKernel( rayDataStruct ).setWorkgroupSize( 64, 1, 1 );
89
+
90
+ // bind the shared env / lights providers so the kernels' proxies resolve even before they're set
91
+ this.logicKernel.envInfo = this.envInfo;
92
+ this.logicKernel.backgroundInfo = this.backgroundInfo;
93
+ this.logicKernel.lightsInfo = this.lightsInfo;
94
+
95
+ // clear kernels
96
+ this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 );
97
+
98
+ // later
99
+ this.volumeKernel = null;
100
+
101
+ }
102
+
103
+ resetSeed() {
104
+
105
+ this.seed = 0;
106
+
107
+ }
108
+
109
+ reset() {
110
+
111
+ super.reset();
112
+ this._samplesPromise = null;
113
+
114
+ }
115
+
116
+ setBVHData( bvhData ) {
117
+
118
+ this.materialKernel.bvhData = bvhData;
119
+ this.materialKernel.needsUpdate = true;
120
+
121
+ this.traceRayKernel.bvhData = bvhData;
122
+ this.traceRayKernel.needsUpdate = true;
123
+
124
+ this.traceShadowRayKernel.bvhData = bvhData;
125
+ this.traceShadowRayKernel.needsUpdate = true;
126
+
127
+ this.reset();
128
+
129
+ }
130
+
131
+ rebuild() {
132
+
133
+ super.rebuild();
134
+
135
+ // MaterialKernel bakes in the camera ray function, so a camera swap needs a recompile
136
+ this.materialKernel.needsUpdate = true;
137
+ this.reset();
138
+
139
+ }
140
+
141
+ setRandom( random ) {
142
+
143
+ this.logicKernel.context.random = random;
144
+ this.logicKernel.needsUpdate = true;
145
+
146
+ this.materialKernel.context.random = random;
147
+ this.materialKernel.needsUpdate = true;
148
+
149
+ this.traceRayKernel.context.random = random;
150
+ this.traceRayKernel.needsUpdate = true;
151
+
152
+ this.traceShadowRayKernel.context.random = random;
153
+ this.traceShadowRayKernel.needsUpdate = true;
154
+
155
+ this.reset();
156
+
157
+ }
158
+
159
+ setMaterial( material ) {
160
+
161
+ this.materialKernel.material = material.getData();
162
+ this.materialKernel.needsUpdate = true;
163
+ this.reset();
164
+
165
+ }
166
+
167
+ setFilterGlossy( value ) {
168
+
169
+ // the kernel takes the inverted threshold, mirroring Cycles
170
+ this.materialKernel.filterGlossy = value === 0 ? FILTER_GLOSSY_DISABLED : 1 / value;
171
+ this.reset();
172
+
173
+ }
174
+
175
+ setClamping( direct, indirect ) {
176
+
177
+ this.logicKernel.clampDirect = direct;
178
+ this.logicKernel.clampIndirect = indirect;
179
+ this.reset();
180
+
181
+ }
182
+
183
+ setTransmissiveBackground( value ) {
184
+
185
+ this.logicKernel.transmissiveBackground = value;
186
+ this.reset();
187
+
188
+ }
189
+
190
+ setEnvironment( envMap ) {
191
+
192
+ this.envInfo.updateFrom( envMap );
193
+
194
+ }
195
+
196
+ setLights( lights ) {
197
+
198
+ this.lightsInfo.updateFrom( this.renderer, lights );
199
+ this.reset();
200
+
201
+ }
202
+
203
+ setEnvironmentParams( envMapIntensity, envMapRotation ) {
204
+
205
+ const { envInfo } = this;
206
+ const rotationMatrix = new Matrix4().makeRotationFromEuler( envMapRotation ).invert();
207
+ envInfo.rotationNode.value.setFromMatrix4( rotationMatrix );
208
+ envInfo.intensityNode.value = envMapIntensity;
209
+
210
+ }
211
+
212
+ setMultipleImportanceSampling( enabled ) {
213
+
214
+ this.logicKernel.misEnabled = enabled ? 1 : 0;
215
+ this.reset();
216
+
217
+ }
218
+
219
+ setBackground( background ) {
220
+
221
+ const { backgroundInfo } = this;
222
+ backgroundInfo.dispose();
223
+ backgroundInfo.map = background;
224
+
225
+ }
226
+
227
+ setBackgroundParams(
228
+ backgroundIntensity,
229
+ backgroundRotation,
230
+ backgroundBlurriness,
231
+ ) {
232
+
233
+ const { backgroundInfo } = this;
234
+ const rotationMatrix = new Matrix4().makeRotationFromEuler( backgroundRotation ).invert();
235
+ backgroundInfo.rotationNode.value.copy( rotationMatrix );
236
+ backgroundInfo.intensity = backgroundIntensity;
237
+ backgroundInfo.blur = backgroundBlurriness;
238
+
239
+ }
240
+
241
+ dispose() {
242
+
243
+ super.dispose();
244
+
245
+ this.envInfo.dispose();
246
+ this.lightsInfo.dispose();
247
+
248
+ this.rayDataStorage?.dispose();
249
+ this.rayQueue?.dispose();
250
+ this.shadowRayQueue?.dispose();
251
+ this.rayIntersectionsStorage?.dispose();
252
+ this.shadowRayIntersectionsStorage?.dispose();
253
+ this.sampleCountersStorage.dispose();
254
+ this.pixelQueue?.dispose();
255
+
256
+ // TODO: the dispatch converters own their indirect dispatch buffers, they should dispose of them themselves
257
+ this.rayDispatchConverter.outputDispatch.dispose();
258
+ this.shadowDispatchConverter.outputDispatch.dispose();
259
+
260
+ }
261
+
262
+ // The queue holds every pixel not owned by a slot. It is sized to the full pixel count so slots
263
+ // retired by a shrink can always return theirs.
264
+ _updatePixelQueue( width, height ) {
265
+
266
+ const size = pixelQueueStruct.getLength() + Math.max( width * height, 1 );
267
+ if ( ! this.pixelQueue || this.pixelQueue.array.length < size ) {
268
+
269
+ this.pixelQueue?.dispose();
270
+ this.pixelQueue = new StorageBufferAttribute( new Float32Array( size ), size );
271
+ this.pixelQueue.name = 'Pixel Queue';
272
+
273
+ // the queue buffer object changed, so kernels bound to it must rebuild
274
+ this.materialKernel.pixelQueue = this.pixelQueue;
275
+ this.materialKernel.needsUpdate = true;
276
+ this.populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
277
+ this.populatePixelIndicesKernel.needsUpdate = true;
278
+ this.resetSlotsKernel.pixelQueue = this.pixelQueue;
279
+ this.resetSlotsKernel.needsUpdate = true;
280
+
281
+ }
282
+
283
+ }
284
+
285
+ // Reallocates the per slot buffers to "count" slots. Slots that survive carry their ray data
286
+ // over, dropped slots return their pixel to the queue, and added slots take one from it. The
287
+ // trace queues and results only live within a frame, so they start empty.
288
+ _resizePool( count ) {
289
+
290
+ const { renderer, resetSlotsKernel, copyRayDataKernel } = this;
291
+ const previousCount = this.rayCount;
292
+
293
+ resetSlotsKernel.pixelQueue = this.pixelQueue;
294
+ if ( count < previousCount ) {
295
+
296
+ resetSlotsKernel.rayDataStorage = this.rayDataStorage;
297
+ resetSlotsKernel.start = count;
298
+ resetSlotsKernel.end = previousCount;
299
+ resetSlotsKernel.addingSlots = false;
300
+ renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( previousCount - count, 1, 1 ) );
301
+
302
+ }
303
+
304
+ const rayDataStorage = new StorageBufferAttribute( count, rayDataStruct.getLength() );
305
+ rayDataStorage.name = 'Ray Data';
306
+
307
+ const queueSize = rayQueueStruct.getLength() + count * traceQueuedRayStruct.getLength();
308
+ const rayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
309
+ rayQueue.name = 'Ray Queue';
310
+
311
+ const shadowRayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
312
+ shadowRayQueue.name = 'Shadow Ray Queue';
313
+
314
+ const rayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
315
+ rayIntersectionsStorage.name = 'Ray Intersections';
316
+
317
+ const shadowRayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
318
+ shadowRayIntersectionsStorage.name = 'Shadow Ray Intersections';
319
+
320
+ const copyCount = Math.min( count, previousCount );
321
+ if ( copyCount > 0 ) {
322
+
323
+ copyRayDataKernel.source = this.rayDataStorage;
324
+ copyRayDataKernel.target = rayDataStorage;
325
+ copyRayDataKernel.count = copyCount;
326
+ renderer.compute( copyRayDataKernel.kernel, copyRayDataKernel.getDispatchSize( copyCount, 1, 1 ) );
327
+
328
+ }
329
+
330
+ this.rayDataStorage?.dispose();
331
+ this.rayQueue?.dispose();
332
+ this.shadowRayQueue?.dispose();
333
+ this.rayIntersectionsStorage?.dispose();
334
+ this.shadowRayIntersectionsStorage?.dispose();
335
+
336
+ this.rayDataStorage = rayDataStorage;
337
+ this.rayQueue = rayQueue;
338
+ this.shadowRayQueue = shadowRayQueue;
339
+ this.rayIntersectionsStorage = rayIntersectionsStorage;
340
+ this.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
341
+ this.rayCount = count;
342
+
343
+ // the buffer objects changed, so kernels bound to them must rebuild
344
+ this.logicKernel.needsUpdate = true;
345
+ this.materialKernel.needsUpdate = true;
346
+ this.traceRayKernel.needsUpdate = true;
347
+ this.traceShadowRayKernel.needsUpdate = true;
348
+ this.populatePixelIndicesKernel.needsUpdate = true;
349
+ this.resetSlotsKernel.needsUpdate = true;
350
+
351
+ if ( count > previousCount ) {
352
+
353
+ resetSlotsKernel.rayDataStorage = rayDataStorage;
354
+ resetSlotsKernel.start = previousCount;
355
+ resetSlotsKernel.end = count;
356
+ resetSlotsKernel.addingSlots = true;
357
+ renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( count - previousCount, 1, 1 ) );
358
+
359
+ }
360
+
361
+ }
362
+
363
+ // The slot count the current budget asks for, capped by the pool limit and the pixel count
364
+ _getRequestedSlotCount( pixelCount ) {
365
+
366
+ return Math.min( MAX_RAY_DATA_COUNT, pixelCount, Math.max( 1, Math.floor( this.frameBudget ) ) );
367
+
368
+ }
369
+
370
+ // Resizes the pool when the budget changes. Paths in flight past a smaller budget are dropped.
371
+ _applyBudget( pixelCount ) {
372
+
373
+ const requested = this._getRequestedSlotCount( pixelCount );
374
+ if ( requested !== this.rayCount ) {
375
+
376
+ this._resizePool( requested );
377
+
378
+ }
379
+
380
+ }
381
+
382
+ *createRenderTask() {
383
+
384
+ const {
385
+ renderer,
386
+ maxTransparentBounces,
387
+
388
+ logicKernel,
389
+ materialKernel,
390
+ traceRayKernel,
391
+ traceShadowRayKernel,
392
+ rayDispatchConverter,
393
+ shadowDispatchConverter,
394
+ zeroDispatchKernel,
395
+ populatePixelIndicesKernel,
396
+ } = this;
397
+
398
+ // a resize resets, recreating this task, so the dimensions and pixel queue hold for its
399
+ // lifetime. The slot pool starts tight, at the budget, and is resized in the loop below.
400
+ const targetDimensions = new Vector2();
401
+ this.getSize( targetDimensions );
402
+
403
+ // referenced via "this" since the buffers may be replaced here and in the loop
404
+ this._updatePixelQueue( targetDimensions.x, targetDimensions.y );
405
+
406
+ // the populate pass below overwrites whatever the resize carried over
407
+ const pixelCount = targetDimensions.x * targetDimensions.y;
408
+ this._applyBudget( pixelCount );
409
+
410
+ // reset the trace queues — only the length header needs zeroing
411
+ zeroDispatchKernel.target = this.rayQueue;
412
+ renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
413
+
414
+ zeroDispatchKernel.target = this.shadowRayQueue;
415
+ renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
416
+
417
+ // assign every path slot a pixel and park the overflow pixels in the pixel queue
418
+ populatePixelIndicesKernel.rayDataStorage = this.rayDataStorage;
419
+ populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
420
+ populatePixelIndicesKernel.frameBudget = this.rayCount;
421
+ populatePixelIndicesKernel.targetDimensions.copy( targetDimensions );
422
+ renderer.compute( populatePixelIndicesKernel.kernel, populatePixelIndicesKernel.getDispatchSize( targetDimensions.x, targetDimensions.y, 1 ) );
423
+
424
+ // advance the sequence once per task so each render pass starts from fresh
425
+ // noise unless "stableNoise" has reset it
426
+ this.seed ++;
427
+
428
+ while ( true ) {
429
+
430
+ // TODO: this only makes sense if we can cap the number rays per iteration to the full
431
+ // frame
432
+ const iter = this.lowResMode ? Math.min( this.maxBounces, LOW_RES_ITERATIONS ) : 1;
433
+
434
+ for ( let i = 0; i < iter; i ++ ) {
435
+
436
+ // Swap targets to support devices without <rgba32float, read_write> textures
437
+ // Copy latest data to a new outputTarget to keep the appearance
438
+ // TODO: this full resolution copy runs every frame - remove it by writing terminated
439
+ // samples to both targets or gating the swap on devices that support read_write
440
+ renderer.copyTextureToTexture( this.outputTarget, this.prevOutputTarget );
441
+ [ this.outputTarget, this.prevOutputTarget ] = [ this.prevOutputTarget, this.outputTarget ];
442
+ logicKernel.prevOutputTarget = this.prevOutputTarget;
443
+ logicKernel.outputTarget = this.outputTarget;
444
+ logicKernel.sampleCountTarget = this.sampleCountTarget;
445
+
446
+ // Step 1: resolve last frame's trace results — accumulate NEE / emission / env, terminate
447
+ // finished paths into the output, and pick the next NEE light for each live path
448
+ logicKernel.rayDataStorage = this.rayDataStorage;
449
+ logicKernel.rayIntersectionsStorage = this.rayIntersectionsStorage;
450
+ logicKernel.shadowRayIntersectionsStorage = this.shadowRayIntersectionsStorage;
451
+ logicKernel.maxBounces = this.maxBounces;
452
+ logicKernel.rayCount = this.rayCount;
453
+ renderer.compute( logicKernel.kernel, logicKernel.getDispatchSize( this.rayCount, 1, 1 ) );
454
+
455
+ // the trace results are consumed, so only the slot state has to survive a resize
456
+ this._applyBudget( pixelCount );
457
+
458
+ const {
459
+ rayDataStorage,
460
+ rayQueue,
461
+ shadowRayQueue,
462
+ rayIntersectionsStorage,
463
+ shadowRayIntersectionsStorage,
464
+ rayCount,
465
+ } = this;
466
+
467
+ // Step 2: reset the trace queues for this frame's population
468
+ zeroDispatchKernel.target = rayQueue;
469
+ renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
470
+
471
+ zeroDispatchKernel.target = shadowRayQueue;
472
+ renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
473
+
474
+ // Step 3: evaluate materials — spawn camera rays for freed slots, sample the bsdf, and
475
+ // enqueue this frame's bounce + shadow rays
476
+ materialKernel.rayDataStorage = rayDataStorage;
477
+ materialKernel.rayQueue = rayQueue;
478
+ materialKernel.shadowRayQueue = shadowRayQueue;
479
+ materialKernel.pixelQueue = this.pixelQueue;
480
+ materialKernel.sampleCountTarget = this.sampleCountTarget;
481
+ materialKernel.seed = this.seed;
482
+ materialKernel.maxSamples = this.maxSamples;
483
+ materialKernel.rayCount = rayCount;
484
+ materialKernel.maxTransparentBounces = maxTransparentBounces;
485
+ materialKernel.maxBounces = this.maxBounces;
486
+ materialKernel.targetDimensions.copy( targetDimensions );
487
+ renderer.compute( materialKernel.kernel, materialKernel.getDispatchSize( rayCount, 1, 1 ) );
488
+
489
+ // Step 4: convert the queue lengths into indirect dispatch sizes and trace
490
+ rayDispatchConverter.queue = rayQueue;
491
+ renderer.compute( rayDispatchConverter.kernel, [ 1, 1, 1 ] );
492
+
493
+ traceRayKernel.rayQueue = rayQueue;
494
+ traceRayKernel.rayIntersectionsStorage = rayIntersectionsStorage;
495
+ renderer.compute( traceRayKernel.kernel, rayDispatchConverter.outputDispatch );
496
+
497
+ shadowDispatchConverter.queue = shadowRayQueue;
498
+ renderer.compute( shadowDispatchConverter.kernel, [ 1, 1, 1 ] );
499
+
500
+ traceShadowRayKernel.shadowRayQueue = shadowRayQueue;
501
+ traceShadowRayKernel.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
502
+ renderer.compute( traceShadowRayKernel.kernel, shadowDispatchConverter.outputDispatch );
503
+
504
+ }
505
+
506
+ yield;
507
+
508
+ }
509
+
510
+ }
511
+
512
+ // Reduces the per pixel sample counts and reads them back. Runs a full resolution pass, so it
513
+ // only happens when asked for.
514
+ getSampleCountsAsync() {
515
+
516
+ // share the in flight measurement rather than dispatching another
517
+ if ( this._samplesPromise === null ) {
518
+
519
+ const promise = this._measureSampleCounts().finally( () => {
520
+
521
+ // a reset may have started a newer measurement in the meantime
522
+ if ( this._samplesPromise === promise ) {
523
+
524
+ this._samplesPromise = null;
525
+
526
+ }
527
+
528
+ } );
529
+
530
+ this._samplesPromise = promise;
531
+
532
+ }
533
+
534
+ return this._samplesPromise;
535
+
536
+ }
537
+
538
+ async _measureSampleCounts() {
539
+
540
+ const {
541
+ renderer,
542
+ sampleCountTarget,
543
+ sampleCountersStorage,
544
+ primeSampleCountersKernel,
545
+ tallySampleCountsKernel,
546
+ } = this;
547
+
548
+ if ( ! renderer.initialized || this.lowResMode ) {
549
+
550
+ return { min: 0, max: 0, avg: 0 };
551
+
552
+ }
553
+
554
+ primeSampleCountersKernel.counters = sampleCountersStorage;
555
+ renderer.compute( primeSampleCountersKernel.kernel, [ 1, 1, 1 ] );
556
+
557
+ tallySampleCountsKernel.counters = sampleCountersStorage;
558
+ tallySampleCountsKernel.sampleCountTarget = sampleCountTarget;
559
+ renderer.compute(
560
+ tallySampleCountsKernel.kernel,
561
+ tallySampleCountsKernel.getDispatchSize( sampleCountTarget.width, sampleCountTarget.height ),
562
+ );
563
+
564
+ const buffer = await renderer.getArrayBufferAsync( sampleCountersStorage );
565
+ const counters = new Uint32Array( buffer );
566
+ const pixelCount = counters[ SAMPLE_COUNTER_PIXEL_COUNT ];
567
+
568
+ // no camera ray has been dispatched yet, so there is nothing to average over
569
+ if ( pixelCount === 0 ) {
570
+
571
+ return { min: 0, max: 0, avg: 0 };
572
+
573
+ }
574
+
575
+ // the total is accumulated as a split 64 bit value to survive high sample counts
576
+ const total = counters[ SAMPLE_COUNTER_TOTAL_HI ] * U32_RANGE + counters[ SAMPLE_COUNTER_TOTAL_LO ];
577
+
578
+ return {
579
+ min: counters[ SAMPLE_COUNTER_MIN ],
580
+ max: counters[ SAMPLE_COUNTER_MAX ],
581
+ avg: total / pixelCount,
582
+ };
583
+
584
+ }
585
+
586
+ }