three-gpu-pathtracer 0.0.26 → 0.0.28

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 (50) hide show
  1. package/README.md +4 -2
  2. package/build/index.module.js +23 -5
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +22 -4
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +8 -8
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/core/utils/sceneUpdateUtils.js +2 -1
  9. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  10. package/src/uniforms/LightsInfoUniformStruct.js +16 -1
  11. package/src/webgpu/API.md +167 -19
  12. package/src/webgpu/AtlasTexture.js +32 -12
  13. package/src/webgpu/EquirectHdrInfoNode.js +97 -46
  14. package/src/webgpu/LightsInfoNode.js +142 -4
  15. package/src/webgpu/MegaKernelPathTracer.js +11 -3
  16. package/src/webgpu/PathTracerBackend.js +174 -21
  17. package/src/webgpu/TurquinTexture.js +2 -0
  18. package/src/webgpu/WaveFrontPathTracer.js +169 -71
  19. package/src/webgpu/WebGPUPathTracer.js +637 -51
  20. package/src/webgpu/compute/ComputeKernel.js +18 -0
  21. package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
  22. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  23. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  24. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  25. package/src/webgpu/compute/targets.js +55 -0
  26. package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
  27. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  28. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  29. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  30. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  31. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  32. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  33. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  34. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
  35. package/src/webgpu/compute/wavefront/structs.js +63 -35
  36. package/src/webgpu/constants.js +2 -2
  37. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  38. package/src/webgpu/index.d.ts +79 -6
  39. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  40. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  41. package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
  42. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  43. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  44. package/src/webgpu/nodes/lights.wgsl.js +21 -1
  45. package/src/webgpu/nodes/material.wgsl.js +2 -1
  46. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  47. package/src/webgpu/nodes/random.wgsl.js +2 -0
  48. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  49. package/src/webgpu/nodes/structs.wgsl.js +8 -1
  50. package/src/webgpu/upscale/FSRUpscaler.js +3 -2
@@ -4,6 +4,8 @@ import { LogicKernel } from './compute/wavefront/LogicKernel.js';
4
4
  import { MaterialKernel } from './compute/wavefront/MaterialKernel.js';
5
5
  import { TraceRayKernel } from './compute/wavefront/TraceRayKernel.js';
6
6
  import { TraceShadowRayKernel } from './compute/wavefront/TraceShadowRayKernel.js';
7
+ import { MediumKernel } from './compute/wavefront/MediumKernel.js';
8
+ import { QueueMediumWalksKernel } from './compute/wavefront/QueueMediumWalksKernel.js';
7
9
  import { QueueLengthToDispatchKernel } from './compute/wavefront/QueueLengthToDispatchKernel.js';
8
10
  import { ResetSlotsKernel } from './compute/wavefront/ResetSlotsKernel.js';
9
11
  import { ZeroOutBufferKernel } from './compute/ZeroOutBufferKernel.js';
@@ -11,7 +13,11 @@ import { CopyBufferKernel } from './compute/CopyBufferKernel.js';
11
13
  import { EquirectHdrInfoNode } from './EquirectHdrInfoNode.js';
12
14
  import { EquirectBackgroundInfo } from './EquirectBackgroundInfo.js';
13
15
  import { LightsInfoNode } from './LightsInfoNode.js';
14
- import { rayDataStruct, traceQueuedRayStruct, intersectionResultStruct, rayQueueStruct, pixelQueueStruct } from './compute/wavefront/structs.js';
16
+ import {
17
+ rayDataStruct, traceQueuedRayStruct, intersectionResultStruct, shadowTraceResultStruct,
18
+ RAY_QUEUE_LENGTH, SHADOW_RAY_QUEUE_LENGTH, QUEUE_COUNTER_COUNT, PIXEL_QUEUE_HEADER,
19
+ MEDIUM_QUEUE_LENGTH, MEDIUM_QUEUE_HEADER,
20
+ } from './compute/wavefront/structs.js';
15
21
  import { PathTracerBackend } from './PathTracerBackend.js';
16
22
  import { FILTER_GLOSSY_DISABLED } from './nodes/material.wgsl.js';
17
23
  import {
@@ -44,6 +50,10 @@ export class WaveFrontPathTracer extends PathTracerBackend {
44
50
 
45
51
  // options
46
52
  this.seed = 0;
53
+
54
+ // scatter events a medium walk takes per pass
55
+ this.mediumStepsPerPass = 16;
56
+
47
57
  this.envInfo = new EquirectHdrInfoNode();
48
58
  this.backgroundInfo = new EquirectBackgroundInfo();
49
59
  this.lightsInfo = new LightsInfoNode();
@@ -56,14 +66,20 @@ export class WaveFrontPathTracer extends PathTracerBackend {
56
66
  // persistent per-path state, one slot per in-flight path
57
67
  this.rayDataStorage = null;
58
68
 
59
- // append-only trace queues, prefixed by an atomic length header
69
+ // append-only trace queues, with their lengths in the counters buffer
60
70
  this.rayQueue = null;
61
71
  this.shadowRayQueue = null;
72
+ this.queueCounters = new StorageBufferAttribute( new Uint32Array( QUEUE_COUNTER_COUNT ), 1 );
73
+ this.queueCounters.name = 'Queue Counters';
62
74
 
63
- // per-queue-slot trace results, indexed by the ray's position in its queue
75
+ // per-queue-slot trace results, indexed by the ray's position in its queue. The ray results also
76
+ // hold the hits of medium walks, filled from the back
64
77
  this.rayIntersectionsStorage = null;
65
78
  this.shadowRayIntersectionsStorage = null;
66
79
 
80
+ // the slots walking a medium this pass
81
+ this.mediumQueue = null;
82
+
67
83
  // pixel indices waiting for a free path slot, sized to the resolution
68
84
  this.pixelQueue = null;
69
85
 
@@ -78,8 +94,11 @@ export class WaveFrontPathTracer extends PathTracerBackend {
78
94
  this.materialKernel = new MaterialKernel().setWorkgroupSize( 64, 1, 1 );
79
95
  this.traceRayKernel = new TraceRayKernel().setWorkgroupSize( 64, 1, 1 );
80
96
  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 );
97
+ this.mediumKernel = new MediumKernel().setWorkgroupSize( 64, 1, 1 );
98
+ this.queueMediumWalksKernel = new QueueMediumWalksKernel().setWorkgroupSize( 64, 1, 1 );
99
+ this.rayDispatchConverter = new QueueLengthToDispatchKernel( RAY_QUEUE_LENGTH ).setWorkgroupSize( 1, 1, 1 );
100
+ this.shadowDispatchConverter = new QueueLengthToDispatchKernel( SHADOW_RAY_QUEUE_LENGTH ).setWorkgroupSize( 1, 1, 1 );
101
+ this.mediumDispatchConverter = new QueueLengthToDispatchKernel( MEDIUM_QUEUE_LENGTH ).setWorkgroupSize( 1, 1, 1 );
83
102
  this.primeSampleCountersKernel = new PrimeSampleCountersKernel().setWorkgroupSize( 1, 1, 1 );
84
103
  this.tallySampleCountsKernel = new TallySampleCountsKernel().setWorkgroupSize( 8, 8, 1 );
85
104
 
@@ -95,9 +114,6 @@ export class WaveFrontPathTracer extends PathTracerBackend {
95
114
  // clear kernels
96
115
  this.zeroDispatchKernel = new ZeroOutBufferKernel().setWorkgroupSize( 1, 1, 1 );
97
116
 
98
- // later
99
- this.volumeKernel = null;
100
-
101
117
  }
102
118
 
103
119
  resetSeed() {
@@ -115,15 +131,18 @@ export class WaveFrontPathTracer extends PathTracerBackend {
115
131
 
116
132
  setBVHData( bvhData ) {
117
133
 
118
- this.materialKernel.bvhData = bvhData;
134
+ this.materialKernel.context.bvhData = bvhData;
119
135
  this.materialKernel.needsUpdate = true;
120
136
 
121
- this.traceRayKernel.bvhData = bvhData;
137
+ this.traceRayKernel.context.bvhData = bvhData;
122
138
  this.traceRayKernel.needsUpdate = true;
123
139
 
124
- this.traceShadowRayKernel.bvhData = bvhData;
140
+ this.traceShadowRayKernel.context.bvhData = bvhData;
125
141
  this.traceShadowRayKernel.needsUpdate = true;
126
142
 
143
+ this.mediumKernel.context.bvhData = bvhData;
144
+ this.mediumKernel.needsUpdate = true;
145
+
127
146
  this.reset();
128
147
 
129
148
  }
@@ -152,6 +171,9 @@ export class WaveFrontPathTracer extends PathTracerBackend {
152
171
  this.traceShadowRayKernel.context.random = random;
153
172
  this.traceShadowRayKernel.needsUpdate = true;
154
173
 
174
+ this.mediumKernel.context.random = random;
175
+ this.mediumKernel.needsUpdate = true;
176
+
155
177
  this.reset();
156
178
 
157
179
  }
@@ -248,14 +270,33 @@ export class WaveFrontPathTracer extends PathTracerBackend {
248
270
  this.rayDataStorage?.dispose();
249
271
  this.rayQueue?.dispose();
250
272
  this.shadowRayQueue?.dispose();
273
+ this.queueCounters.dispose();
251
274
  this.rayIntersectionsStorage?.dispose();
252
275
  this.shadowRayIntersectionsStorage?.dispose();
276
+ this.mediumQueue?.dispose();
253
277
  this.sampleCountersStorage.dispose();
254
278
  this.pixelQueue?.dispose();
255
279
 
256
280
  // TODO: the dispatch converters own their indirect dispatch buffers, they should dispose of them themselves
257
281
  this.rayDispatchConverter.outputDispatch.dispose();
258
282
  this.shadowDispatchConverter.outputDispatch.dispose();
283
+ this.mediumDispatchConverter.outputDispatch.dispose();
284
+
285
+ this.populatePixelIndicesKernel.dispose();
286
+ this.logicKernel.dispose();
287
+ this.materialKernel.dispose();
288
+ this.traceRayKernel.dispose();
289
+ this.traceShadowRayKernel.dispose();
290
+ this.mediumKernel.dispose();
291
+ this.queueMediumWalksKernel.dispose();
292
+ this.rayDispatchConverter.dispose();
293
+ this.shadowDispatchConverter.dispose();
294
+ this.mediumDispatchConverter.dispose();
295
+ this.primeSampleCountersKernel.dispose();
296
+ this.tallySampleCountsKernel.dispose();
297
+ this.resetSlotsKernel.dispose();
298
+ this.copyRayDataKernel.dispose();
299
+ this.zeroDispatchKernel.dispose();
259
300
 
260
301
  }
261
302
 
@@ -263,67 +304,73 @@ export class WaveFrontPathTracer extends PathTracerBackend {
263
304
  // retired by a shrink can always return theirs.
264
305
  _updatePixelQueue( width, height ) {
265
306
 
266
- const size = pixelQueueStruct.getLength() + Math.max( width * height, 1 );
307
+ const size = PIXEL_QUEUE_HEADER + Math.max( width * height, 1 );
267
308
  if ( ! this.pixelQueue || this.pixelQueue.array.length < size ) {
268
309
 
269
310
  this.pixelQueue?.dispose();
270
311
  this.pixelQueue = new StorageBufferAttribute( new Float32Array( size ), size );
271
312
  this.pixelQueue.name = 'Pixel Queue';
272
313
 
273
- // the queue buffer object changed, so kernels bound to it must rebuild
314
+ // three rebinds a swapped buffer, so the kernels keep their compiled pipelines
274
315
  this.materialKernel.pixelQueue = this.pixelQueue;
275
- this.materialKernel.needsUpdate = true;
276
316
  this.populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
277
- this.populatePixelIndicesKernel.needsUpdate = true;
278
317
  this.resetSlotsKernel.pixelQueue = this.pixelQueue;
279
- this.resetSlotsKernel.needsUpdate = true;
280
318
 
281
319
  }
282
320
 
283
321
  }
284
322
 
285
323
  // 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 ) {
324
+ // over, dropped slots return their pixel to the queue, and added slots take one from it. Only
325
+ // the slots in use, up to the pixel count, hold pixels. The trace queues and results only live
326
+ // within a frame, so they start empty.
327
+ _resizePool( count, pixelCount ) {
289
328
 
290
- const { renderer, resetSlotsKernel, copyRayDataKernel } = this;
329
+ const { resetSlotsKernel, copyRayDataKernel } = this;
291
330
  const previousCount = this.rayCount;
331
+ const previousActive = Math.min( previousCount, pixelCount );
332
+ const active = Math.min( count, pixelCount );
292
333
 
293
334
  resetSlotsKernel.pixelQueue = this.pixelQueue;
294
- if ( count < previousCount ) {
335
+ if ( active < previousActive ) {
295
336
 
296
337
  resetSlotsKernel.rayDataStorage = this.rayDataStorage;
297
- resetSlotsKernel.start = count;
298
- resetSlotsKernel.end = previousCount;
338
+ resetSlotsKernel.start = active;
339
+ resetSlotsKernel.end = previousActive;
299
340
  resetSlotsKernel.addingSlots = false;
300
- renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( previousCount - count, 1, 1 ) );
341
+ this.dispatch( resetSlotsKernel, resetSlotsKernel.getDispatchSize( previousActive - active, 1, 1 ) );
301
342
 
302
343
  }
303
344
 
304
345
  const rayDataStorage = new StorageBufferAttribute( count, rayDataStruct.getLength() );
305
346
  rayDataStorage.name = 'Ray Data';
306
347
 
307
- const queueSize = rayQueueStruct.getLength() + count * traceQueuedRayStruct.getLength();
308
- const rayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
348
+ const rayQueue = new StorageBufferAttribute( count, traceQueuedRayStruct.getLength() );
309
349
  rayQueue.name = 'Ray Queue';
310
350
 
311
- const shadowRayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
351
+ const shadowRayQueue = new StorageBufferAttribute( count, traceQueuedRayStruct.getLength() );
312
352
  shadowRayQueue.name = 'Shadow Ray Queue';
313
353
 
354
+ // Traced rays fill the results from the front at their queue index, and MediumKernel fills them from the
355
+ // back with the hits of medium walks. In a round each path slot either queues one ray or walks a medium,
356
+ // never both, so the two ranges together never exceed the slot count and can't overlap
314
357
  const rayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
315
358
  rayIntersectionsStorage.name = 'Ray Intersections';
316
359
 
317
- const shadowRayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
360
+ const shadowRayIntersectionsStorage = new StorageBufferAttribute( count, shadowTraceResultStruct.getLength() );
318
361
  shadowRayIntersectionsStorage.name = 'Shadow Ray Intersections';
319
362
 
363
+ const mediumQueueSize = MEDIUM_QUEUE_HEADER + count;
364
+ const mediumQueue = new StorageBufferAttribute( new Uint32Array( mediumQueueSize ), mediumQueueSize );
365
+ mediumQueue.name = 'Medium Queue';
366
+
320
367
  const copyCount = Math.min( count, previousCount );
321
368
  if ( copyCount > 0 ) {
322
369
 
323
370
  copyRayDataKernel.source = this.rayDataStorage;
324
371
  copyRayDataKernel.target = rayDataStorage;
325
372
  copyRayDataKernel.count = copyCount;
326
- renderer.compute( copyRayDataKernel.kernel, copyRayDataKernel.getDispatchSize( copyCount, 1, 1 ) );
373
+ this.dispatch( copyRayDataKernel, copyRayDataKernel.getDispatchSize( copyCount, 1, 1 ) );
327
374
 
328
375
  }
329
376
 
@@ -332,48 +379,56 @@ export class WaveFrontPathTracer extends PathTracerBackend {
332
379
  this.shadowRayQueue?.dispose();
333
380
  this.rayIntersectionsStorage?.dispose();
334
381
  this.shadowRayIntersectionsStorage?.dispose();
382
+ this.mediumQueue?.dispose();
335
383
 
336
384
  this.rayDataStorage = rayDataStorage;
337
385
  this.rayQueue = rayQueue;
338
386
  this.shadowRayQueue = shadowRayQueue;
339
387
  this.rayIntersectionsStorage = rayIntersectionsStorage;
340
388
  this.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
389
+ this.mediumQueue = mediumQueue;
341
390
  this.rayCount = count;
342
391
 
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 ) {
392
+ if ( active > previousActive ) {
352
393
 
353
394
  resetSlotsKernel.rayDataStorage = rayDataStorage;
354
- resetSlotsKernel.start = previousCount;
355
- resetSlotsKernel.end = count;
395
+ resetSlotsKernel.start = previousActive;
396
+ resetSlotsKernel.end = active;
356
397
  resetSlotsKernel.addingSlots = true;
357
- renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( count - previousCount, 1, 1 ) );
398
+ this.dispatch( resetSlotsKernel, resetSlotsKernel.getDispatchSize( active - previousActive, 1, 1 ) );
358
399
 
359
400
  }
360
401
 
361
402
  }
362
403
 
363
- // The slot count the current budget asks for, capped by the pool limit and the pixel count
364
- _getRequestedSlotCount( pixelCount ) {
404
+ // The slot count the current budget asks for, capped by the pool limit
405
+ _getRequestedSlotCount() {
406
+
407
+ return Math.min( MAX_RAY_DATA_COUNT, Math.max( 1, Math.floor( this.frameBudget ) ) );
408
+
409
+ }
410
+
411
+ // a preview runs at least this many iterations so its samples finish within the frame
412
+ _getPreviewIterations() {
413
+
414
+ return Math.max( 1, Math.min( this.maxBounces, LOW_RES_ITERATIONS ) );
415
+
416
+ }
417
+
418
+ // a preview steps every path several times, so it covers that many fewer pixels than the budget
419
+ getPreviewPixelBudget() {
365
420
 
366
- return Math.min( MAX_RAY_DATA_COUNT, pixelCount, Math.max( 1, Math.floor( this.frameBudget ) ) );
421
+ return Math.min( this.frameBudget / this._getPreviewIterations(), MAX_RAY_DATA_COUNT );
367
422
 
368
423
  }
369
424
 
370
425
  // Resizes the pool when the budget changes. Paths in flight past a smaller budget are dropped.
371
426
  _applyBudget( pixelCount ) {
372
427
 
373
- const requested = this._getRequestedSlotCount( pixelCount );
428
+ const requested = this._getRequestedSlotCount();
374
429
  if ( requested !== this.rayCount ) {
375
430
 
376
- this._resizePool( requested );
431
+ this._resizePool( requested, pixelCount );
377
432
 
378
433
  }
379
434
 
@@ -389,8 +444,11 @@ export class WaveFrontPathTracer extends PathTracerBackend {
389
444
  materialKernel,
390
445
  traceRayKernel,
391
446
  traceShadowRayKernel,
447
+ mediumKernel,
448
+ queueMediumWalksKernel,
392
449
  rayDispatchConverter,
393
450
  shadowDispatchConverter,
451
+ mediumDispatchConverter,
394
452
  zeroDispatchKernel,
395
453
  populatePixelIndicesKernel,
396
454
  } = this;
@@ -407,19 +465,31 @@ export class WaveFrontPathTracer extends PathTracerBackend {
407
465
  const pixelCount = targetDimensions.x * targetDimensions.y;
408
466
  this._applyBudget( pixelCount );
409
467
 
410
- // reset the trace queues — only the length header needs zeroing
411
- zeroDispatchKernel.target = this.rayQueue;
412
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
468
+ // compile the kernels before their first dispatch
469
+ yield* this.compileKernels( [
470
+ logicKernel,
471
+ materialKernel,
472
+ traceRayKernel,
473
+ traceShadowRayKernel,
474
+ mediumKernel,
475
+ queueMediumWalksKernel,
476
+ rayDispatchConverter,
477
+ shadowDispatchConverter,
478
+ mediumDispatchConverter,
479
+ zeroDispatchKernel,
480
+ populatePixelIndicesKernel,
481
+ ] );
413
482
 
414
- zeroDispatchKernel.target = this.shadowRayQueue;
415
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
483
+ // reset the trace queues — only their lengths need zeroing
484
+ zeroDispatchKernel.target = this.queueCounters;
485
+ this.dispatch( zeroDispatchKernel, [ QUEUE_COUNTER_COUNT ] );
416
486
 
417
- // assign every path slot a pixel and park the overflow pixels in the pixel queue
487
+ // assign every path slot in use a pixel and park the overflow pixels in the pixel queue
418
488
  populatePixelIndicesKernel.rayDataStorage = this.rayDataStorage;
419
489
  populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
420
- populatePixelIndicesKernel.frameBudget = this.rayCount;
490
+ populatePixelIndicesKernel.frameBudget = Math.min( this.rayCount, pixelCount );
421
491
  populatePixelIndicesKernel.targetDimensions.copy( targetDimensions );
422
- renderer.compute( populatePixelIndicesKernel.kernel, populatePixelIndicesKernel.getDispatchSize( targetDimensions.x, targetDimensions.y, 1 ) );
492
+ this.dispatch( populatePixelIndicesKernel, populatePixelIndicesKernel.getDispatchSize( targetDimensions.x, targetDimensions.y, 1 ) );
423
493
 
424
494
  // advance the sequence once per task so each render pass starts from fresh
425
495
  // noise unless "stableNoise" has reset it
@@ -427,9 +497,10 @@ export class WaveFrontPathTracer extends PathTracerBackend {
427
497
 
428
498
  while ( true ) {
429
499
 
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;
500
+ // the iterations that spend the budget over the slots in use
501
+ const activeSlots = Math.min( this._getRequestedSlotCount(), pixelCount );
502
+ const minIterations = this.lowResMode ? this._getPreviewIterations() : 1;
503
+ const iter = Math.max( minIterations, Math.round( this.frameBudget / activeSlots ) );
433
504
 
434
505
  for ( let i = 0; i < iter; i ++ ) {
435
506
 
@@ -442,6 +513,8 @@ export class WaveFrontPathTracer extends PathTracerBackend {
442
513
  logicKernel.prevOutputTarget = this.prevOutputTarget;
443
514
  logicKernel.outputTarget = this.outputTarget;
444
515
  logicKernel.sampleCountTarget = this.sampleCountTarget;
516
+ logicKernel.auxTarget = this.auxTarget ?? this._auxPlaceholder;
517
+ logicKernel.accumulateAux = this.auxTarget ? 1 : 0;
445
518
 
446
519
  // Step 1: resolve last frame's trace results — accumulate NEE / emission / env, terminate
447
520
  // finished paths into the output, and pick the next NEE light for each live path
@@ -449,8 +522,8 @@ export class WaveFrontPathTracer extends PathTracerBackend {
449
522
  logicKernel.rayIntersectionsStorage = this.rayIntersectionsStorage;
450
523
  logicKernel.shadowRayIntersectionsStorage = this.shadowRayIntersectionsStorage;
451
524
  logicKernel.maxBounces = this.maxBounces;
452
- logicKernel.rayCount = this.rayCount;
453
- renderer.compute( logicKernel.kernel, logicKernel.getDispatchSize( this.rayCount, 1, 1 ) );
525
+ logicKernel.rayCount = Math.min( this.rayCount, pixelCount );
526
+ this.dispatch( logicKernel, logicKernel.getDispatchSize( logicKernel.rayCount, 1, 1 ) );
454
527
 
455
528
  // the trace results are consumed, so only the slot state has to survive a resize
456
529
  this._applyBudget( pixelCount );
@@ -459,23 +532,28 @@ export class WaveFrontPathTracer extends PathTracerBackend {
459
532
  rayDataStorage,
460
533
  rayQueue,
461
534
  shadowRayQueue,
535
+ queueCounters,
462
536
  rayIntersectionsStorage,
463
537
  shadowRayIntersectionsStorage,
464
- rayCount,
538
+ mediumQueue,
465
539
  } = this;
466
540
 
541
+ // slots past the pixel count never run
542
+ const rayCount = Math.min( this.rayCount, pixelCount );
543
+
467
544
  // Step 2: reset the trace queues for this frame's population
468
- zeroDispatchKernel.target = rayQueue;
469
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
545
+ zeroDispatchKernel.target = queueCounters;
546
+ this.dispatch( zeroDispatchKernel, [ QUEUE_COUNTER_COUNT ] );
470
547
 
471
- zeroDispatchKernel.target = shadowRayQueue;
472
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
548
+ zeroDispatchKernel.target = mediumQueue;
549
+ this.dispatch( zeroDispatchKernel, [ MEDIUM_QUEUE_HEADER ] );
473
550
 
474
551
  // Step 3: evaluate materials — spawn camera rays for freed slots, sample the bsdf, and
475
552
  // enqueue this frame's bounce + shadow rays
476
553
  materialKernel.rayDataStorage = rayDataStorage;
477
554
  materialKernel.rayQueue = rayQueue;
478
555
  materialKernel.shadowRayQueue = shadowRayQueue;
556
+ materialKernel.queueCounters = queueCounters;
479
557
  materialKernel.pixelQueue = this.pixelQueue;
480
558
  materialKernel.sampleCountTarget = this.sampleCountTarget;
481
559
  materialKernel.seed = this.seed;
@@ -484,22 +562,42 @@ export class WaveFrontPathTracer extends PathTracerBackend {
484
562
  materialKernel.maxTransparentBounces = maxTransparentBounces;
485
563
  materialKernel.maxBounces = this.maxBounces;
486
564
  materialKernel.targetDimensions.copy( targetDimensions );
487
- renderer.compute( materialKernel.kernel, materialKernel.getDispatchSize( rayCount, 1, 1 ) );
565
+ this.dispatch( materialKernel, materialKernel.getDispatchSize( rayCount, 1, 1 ) );
488
566
 
489
567
  // Step 4: convert the queue lengths into indirect dispatch sizes and trace
490
- rayDispatchConverter.queue = rayQueue;
491
- renderer.compute( rayDispatchConverter.kernel, [ 1, 1, 1 ] );
568
+ rayDispatchConverter.queueCounters = queueCounters;
569
+ this.dispatch( rayDispatchConverter, [ 1, 1, 1 ] );
492
570
 
493
571
  traceRayKernel.rayQueue = rayQueue;
572
+ traceRayKernel.queueCounters = queueCounters;
494
573
  traceRayKernel.rayIntersectionsStorage = rayIntersectionsStorage;
495
- renderer.compute( traceRayKernel.kernel, rayDispatchConverter.outputDispatch );
574
+ this.dispatch( traceRayKernel, rayDispatchConverter.outputDispatch );
496
575
 
497
- shadowDispatchConverter.queue = shadowRayQueue;
498
- renderer.compute( shadowDispatchConverter.kernel, [ 1, 1, 1 ] );
576
+ shadowDispatchConverter.queueCounters = queueCounters;
577
+ this.dispatch( shadowDispatchConverter, [ 1, 1, 1 ] );
499
578
 
500
579
  traceShadowRayKernel.shadowRayQueue = shadowRayQueue;
580
+ traceShadowRayKernel.queueCounters = queueCounters;
501
581
  traceShadowRayKernel.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
502
- renderer.compute( traceShadowRayKernel.kernel, shadowDispatchConverter.outputDispatch );
582
+ this.dispatch( traceShadowRayKernel, shadowDispatchConverter.outputDispatch );
583
+
584
+ // Step 5: walk the paths inside media for a capped number of steps
585
+ queueMediumWalksKernel.rayDataStorage = rayDataStorage;
586
+ queueMediumWalksKernel.mediumQueue = mediumQueue;
587
+ queueMediumWalksKernel.rayCount = rayCount;
588
+ this.dispatch( queueMediumWalksKernel, queueMediumWalksKernel.getDispatchSize( rayCount, 1, 1 ) );
589
+
590
+ // the medium queue's length leads it, where the converter reads it as a counter
591
+ mediumDispatchConverter.queueCounters = mediumQueue;
592
+ this.dispatch( mediumDispatchConverter, [ 1, 1, 1 ] );
593
+
594
+ mediumKernel.rayDataStorage = rayDataStorage;
595
+ mediumKernel.mediumQueue = mediumQueue;
596
+ mediumKernel.rayIntersectionsStorage = rayIntersectionsStorage;
597
+ mediumKernel.rayCount = rayCount;
598
+ mediumKernel.maxSubsurfaceBounces = this.maxSubsurfaceBounces;
599
+ mediumKernel.stepsPerPass = this.mediumStepsPerPass;
600
+ this.dispatch( mediumKernel, mediumDispatchConverter.outputDispatch );
503
601
 
504
602
  }
505
603