three-gpu-pathtracer 0.0.27 → 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 (46) hide show
  1. package/README.md +0 -1
  2. package/build/index.module.js +5 -3
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +4 -2
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +7 -7
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  9. package/src/webgpu/API.md +167 -19
  10. package/src/webgpu/AtlasTexture.js +2 -3
  11. package/src/webgpu/EquirectHdrInfoNode.js +89 -48
  12. package/src/webgpu/LightsInfoNode.js +1 -1
  13. package/src/webgpu/MegaKernelPathTracer.js +10 -3
  14. package/src/webgpu/PathTracerBackend.js +172 -21
  15. package/src/webgpu/WaveFrontPathTracer.js +156 -71
  16. package/src/webgpu/WebGPUPathTracer.js +628 -48
  17. package/src/webgpu/compute/ComputeKernel.js +8 -0
  18. package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
  19. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  20. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  21. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  22. package/src/webgpu/compute/targets.js +55 -0
  23. package/src/webgpu/compute/wavefront/LogicKernel.js +67 -20
  24. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  25. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  26. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  27. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  28. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  29. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  30. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  31. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -25
  32. package/src/webgpu/compute/wavefront/structs.js +63 -37
  33. package/src/webgpu/constants.js +2 -2
  34. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  35. package/src/webgpu/index.d.ts +41 -6
  36. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  37. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  38. package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
  39. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  40. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  41. package/src/webgpu/nodes/material.wgsl.js +2 -1
  42. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  43. package/src/webgpu/nodes/random.wgsl.js +2 -0
  44. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  45. package/src/webgpu/nodes/structs.wgsl.js +7 -1
  46. package/src/webgpu/upscale/FSRUpscaler.js +1 -1
@@ -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,22 +270,28 @@ 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();
259
284
 
260
285
  this.populatePixelIndicesKernel.dispose();
261
286
  this.logicKernel.dispose();
262
287
  this.materialKernel.dispose();
263
288
  this.traceRayKernel.dispose();
264
289
  this.traceShadowRayKernel.dispose();
290
+ this.mediumKernel.dispose();
291
+ this.queueMediumWalksKernel.dispose();
265
292
  this.rayDispatchConverter.dispose();
266
293
  this.shadowDispatchConverter.dispose();
294
+ this.mediumDispatchConverter.dispose();
267
295
  this.primeSampleCountersKernel.dispose();
268
296
  this.tallySampleCountsKernel.dispose();
269
297
  this.resetSlotsKernel.dispose();
@@ -276,67 +304,73 @@ export class WaveFrontPathTracer extends PathTracerBackend {
276
304
  // retired by a shrink can always return theirs.
277
305
  _updatePixelQueue( width, height ) {
278
306
 
279
- const size = pixelQueueStruct.getLength() + Math.max( width * height, 1 );
307
+ const size = PIXEL_QUEUE_HEADER + Math.max( width * height, 1 );
280
308
  if ( ! this.pixelQueue || this.pixelQueue.array.length < size ) {
281
309
 
282
310
  this.pixelQueue?.dispose();
283
311
  this.pixelQueue = new StorageBufferAttribute( new Float32Array( size ), size );
284
312
  this.pixelQueue.name = 'Pixel Queue';
285
313
 
286
- // 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
287
315
  this.materialKernel.pixelQueue = this.pixelQueue;
288
- this.materialKernel.needsUpdate = true;
289
316
  this.populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
290
- this.populatePixelIndicesKernel.needsUpdate = true;
291
317
  this.resetSlotsKernel.pixelQueue = this.pixelQueue;
292
- this.resetSlotsKernel.needsUpdate = true;
293
318
 
294
319
  }
295
320
 
296
321
  }
297
322
 
298
323
  // Reallocates the per slot buffers to "count" slots. Slots that survive carry their ray data
299
- // over, dropped slots return their pixel to the queue, and added slots take one from it. The
300
- // trace queues and results only live within a frame, so they start empty.
301
- _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 ) {
302
328
 
303
- const { renderer, resetSlotsKernel, copyRayDataKernel } = this;
329
+ const { resetSlotsKernel, copyRayDataKernel } = this;
304
330
  const previousCount = this.rayCount;
331
+ const previousActive = Math.min( previousCount, pixelCount );
332
+ const active = Math.min( count, pixelCount );
305
333
 
306
334
  resetSlotsKernel.pixelQueue = this.pixelQueue;
307
- if ( count < previousCount ) {
335
+ if ( active < previousActive ) {
308
336
 
309
337
  resetSlotsKernel.rayDataStorage = this.rayDataStorage;
310
- resetSlotsKernel.start = count;
311
- resetSlotsKernel.end = previousCount;
338
+ resetSlotsKernel.start = active;
339
+ resetSlotsKernel.end = previousActive;
312
340
  resetSlotsKernel.addingSlots = false;
313
- renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( previousCount - count, 1, 1 ) );
341
+ this.dispatch( resetSlotsKernel, resetSlotsKernel.getDispatchSize( previousActive - active, 1, 1 ) );
314
342
 
315
343
  }
316
344
 
317
345
  const rayDataStorage = new StorageBufferAttribute( count, rayDataStruct.getLength() );
318
346
  rayDataStorage.name = 'Ray Data';
319
347
 
320
- const queueSize = rayQueueStruct.getLength() + count * traceQueuedRayStruct.getLength();
321
- const rayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
348
+ const rayQueue = new StorageBufferAttribute( count, traceQueuedRayStruct.getLength() );
322
349
  rayQueue.name = 'Ray Queue';
323
350
 
324
- const shadowRayQueue = new StorageBufferAttribute( new Float32Array( queueSize ), queueSize );
351
+ const shadowRayQueue = new StorageBufferAttribute( count, traceQueuedRayStruct.getLength() );
325
352
  shadowRayQueue.name = 'Shadow Ray Queue';
326
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
327
357
  const rayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
328
358
  rayIntersectionsStorage.name = 'Ray Intersections';
329
359
 
330
- const shadowRayIntersectionsStorage = new StorageBufferAttribute( count, intersectionResultStruct.getLength() );
360
+ const shadowRayIntersectionsStorage = new StorageBufferAttribute( count, shadowTraceResultStruct.getLength() );
331
361
  shadowRayIntersectionsStorage.name = 'Shadow Ray Intersections';
332
362
 
363
+ const mediumQueueSize = MEDIUM_QUEUE_HEADER + count;
364
+ const mediumQueue = new StorageBufferAttribute( new Uint32Array( mediumQueueSize ), mediumQueueSize );
365
+ mediumQueue.name = 'Medium Queue';
366
+
333
367
  const copyCount = Math.min( count, previousCount );
334
368
  if ( copyCount > 0 ) {
335
369
 
336
370
  copyRayDataKernel.source = this.rayDataStorage;
337
371
  copyRayDataKernel.target = rayDataStorage;
338
372
  copyRayDataKernel.count = copyCount;
339
- renderer.compute( copyRayDataKernel.kernel, copyRayDataKernel.getDispatchSize( copyCount, 1, 1 ) );
373
+ this.dispatch( copyRayDataKernel, copyRayDataKernel.getDispatchSize( copyCount, 1, 1 ) );
340
374
 
341
375
  }
342
376
 
@@ -345,48 +379,56 @@ export class WaveFrontPathTracer extends PathTracerBackend {
345
379
  this.shadowRayQueue?.dispose();
346
380
  this.rayIntersectionsStorage?.dispose();
347
381
  this.shadowRayIntersectionsStorage?.dispose();
382
+ this.mediumQueue?.dispose();
348
383
 
349
384
  this.rayDataStorage = rayDataStorage;
350
385
  this.rayQueue = rayQueue;
351
386
  this.shadowRayQueue = shadowRayQueue;
352
387
  this.rayIntersectionsStorage = rayIntersectionsStorage;
353
388
  this.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
389
+ this.mediumQueue = mediumQueue;
354
390
  this.rayCount = count;
355
391
 
356
- // the buffer objects changed, so kernels bound to them must rebuild
357
- this.logicKernel.needsUpdate = true;
358
- this.materialKernel.needsUpdate = true;
359
- this.traceRayKernel.needsUpdate = true;
360
- this.traceShadowRayKernel.needsUpdate = true;
361
- this.populatePixelIndicesKernel.needsUpdate = true;
362
- this.resetSlotsKernel.needsUpdate = true;
363
-
364
- if ( count > previousCount ) {
392
+ if ( active > previousActive ) {
365
393
 
366
394
  resetSlotsKernel.rayDataStorage = rayDataStorage;
367
- resetSlotsKernel.start = previousCount;
368
- resetSlotsKernel.end = count;
395
+ resetSlotsKernel.start = previousActive;
396
+ resetSlotsKernel.end = active;
369
397
  resetSlotsKernel.addingSlots = true;
370
- renderer.compute( resetSlotsKernel.kernel, resetSlotsKernel.getDispatchSize( count - previousCount, 1, 1 ) );
398
+ this.dispatch( resetSlotsKernel, resetSlotsKernel.getDispatchSize( active - previousActive, 1, 1 ) );
371
399
 
372
400
  }
373
401
 
374
402
  }
375
403
 
376
- // The slot count the current budget asks for, capped by the pool limit and the pixel count
377
- _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() {
378
420
 
379
- 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 );
380
422
 
381
423
  }
382
424
 
383
425
  // Resizes the pool when the budget changes. Paths in flight past a smaller budget are dropped.
384
426
  _applyBudget( pixelCount ) {
385
427
 
386
- const requested = this._getRequestedSlotCount( pixelCount );
428
+ const requested = this._getRequestedSlotCount();
387
429
  if ( requested !== this.rayCount ) {
388
430
 
389
- this._resizePool( requested );
431
+ this._resizePool( requested, pixelCount );
390
432
 
391
433
  }
392
434
 
@@ -402,8 +444,11 @@ export class WaveFrontPathTracer extends PathTracerBackend {
402
444
  materialKernel,
403
445
  traceRayKernel,
404
446
  traceShadowRayKernel,
447
+ mediumKernel,
448
+ queueMediumWalksKernel,
405
449
  rayDispatchConverter,
406
450
  shadowDispatchConverter,
451
+ mediumDispatchConverter,
407
452
  zeroDispatchKernel,
408
453
  populatePixelIndicesKernel,
409
454
  } = this;
@@ -420,19 +465,31 @@ export class WaveFrontPathTracer extends PathTracerBackend {
420
465
  const pixelCount = targetDimensions.x * targetDimensions.y;
421
466
  this._applyBudget( pixelCount );
422
467
 
423
- // reset the trace queues — only the length header needs zeroing
424
- zeroDispatchKernel.target = this.rayQueue;
425
- 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
+ ] );
426
482
 
427
- zeroDispatchKernel.target = this.shadowRayQueue;
428
- 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 ] );
429
486
 
430
- // 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
431
488
  populatePixelIndicesKernel.rayDataStorage = this.rayDataStorage;
432
489
  populatePixelIndicesKernel.pixelQueue = this.pixelQueue;
433
- populatePixelIndicesKernel.frameBudget = this.rayCount;
490
+ populatePixelIndicesKernel.frameBudget = Math.min( this.rayCount, pixelCount );
434
491
  populatePixelIndicesKernel.targetDimensions.copy( targetDimensions );
435
- renderer.compute( populatePixelIndicesKernel.kernel, populatePixelIndicesKernel.getDispatchSize( targetDimensions.x, targetDimensions.y, 1 ) );
492
+ this.dispatch( populatePixelIndicesKernel, populatePixelIndicesKernel.getDispatchSize( targetDimensions.x, targetDimensions.y, 1 ) );
436
493
 
437
494
  // advance the sequence once per task so each render pass starts from fresh
438
495
  // noise unless "stableNoise" has reset it
@@ -440,9 +497,10 @@ export class WaveFrontPathTracer extends PathTracerBackend {
440
497
 
441
498
  while ( true ) {
442
499
 
443
- // TODO: this only makes sense if we can cap the number rays per iteration to the full
444
- // frame
445
- 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 ) );
446
504
 
447
505
  for ( let i = 0; i < iter; i ++ ) {
448
506
 
@@ -455,6 +513,8 @@ export class WaveFrontPathTracer extends PathTracerBackend {
455
513
  logicKernel.prevOutputTarget = this.prevOutputTarget;
456
514
  logicKernel.outputTarget = this.outputTarget;
457
515
  logicKernel.sampleCountTarget = this.sampleCountTarget;
516
+ logicKernel.auxTarget = this.auxTarget ?? this._auxPlaceholder;
517
+ logicKernel.accumulateAux = this.auxTarget ? 1 : 0;
458
518
 
459
519
  // Step 1: resolve last frame's trace results — accumulate NEE / emission / env, terminate
460
520
  // finished paths into the output, and pick the next NEE light for each live path
@@ -462,8 +522,8 @@ export class WaveFrontPathTracer extends PathTracerBackend {
462
522
  logicKernel.rayIntersectionsStorage = this.rayIntersectionsStorage;
463
523
  logicKernel.shadowRayIntersectionsStorage = this.shadowRayIntersectionsStorage;
464
524
  logicKernel.maxBounces = this.maxBounces;
465
- logicKernel.rayCount = this.rayCount;
466
- 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 ) );
467
527
 
468
528
  // the trace results are consumed, so only the slot state has to survive a resize
469
529
  this._applyBudget( pixelCount );
@@ -472,23 +532,28 @@ export class WaveFrontPathTracer extends PathTracerBackend {
472
532
  rayDataStorage,
473
533
  rayQueue,
474
534
  shadowRayQueue,
535
+ queueCounters,
475
536
  rayIntersectionsStorage,
476
537
  shadowRayIntersectionsStorage,
477
- rayCount,
538
+ mediumQueue,
478
539
  } = this;
479
540
 
541
+ // slots past the pixel count never run
542
+ const rayCount = Math.min( this.rayCount, pixelCount );
543
+
480
544
  // Step 2: reset the trace queues for this frame's population
481
- zeroDispatchKernel.target = rayQueue;
482
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
545
+ zeroDispatchKernel.target = queueCounters;
546
+ this.dispatch( zeroDispatchKernel, [ QUEUE_COUNTER_COUNT ] );
483
547
 
484
- zeroDispatchKernel.target = shadowRayQueue;
485
- renderer.compute( zeroDispatchKernel.kernel, [ 1 ] );
548
+ zeroDispatchKernel.target = mediumQueue;
549
+ this.dispatch( zeroDispatchKernel, [ MEDIUM_QUEUE_HEADER ] );
486
550
 
487
551
  // Step 3: evaluate materials — spawn camera rays for freed slots, sample the bsdf, and
488
552
  // enqueue this frame's bounce + shadow rays
489
553
  materialKernel.rayDataStorage = rayDataStorage;
490
554
  materialKernel.rayQueue = rayQueue;
491
555
  materialKernel.shadowRayQueue = shadowRayQueue;
556
+ materialKernel.queueCounters = queueCounters;
492
557
  materialKernel.pixelQueue = this.pixelQueue;
493
558
  materialKernel.sampleCountTarget = this.sampleCountTarget;
494
559
  materialKernel.seed = this.seed;
@@ -497,22 +562,42 @@ export class WaveFrontPathTracer extends PathTracerBackend {
497
562
  materialKernel.maxTransparentBounces = maxTransparentBounces;
498
563
  materialKernel.maxBounces = this.maxBounces;
499
564
  materialKernel.targetDimensions.copy( targetDimensions );
500
- renderer.compute( materialKernel.kernel, materialKernel.getDispatchSize( rayCount, 1, 1 ) );
565
+ this.dispatch( materialKernel, materialKernel.getDispatchSize( rayCount, 1, 1 ) );
501
566
 
502
567
  // Step 4: convert the queue lengths into indirect dispatch sizes and trace
503
- rayDispatchConverter.queue = rayQueue;
504
- renderer.compute( rayDispatchConverter.kernel, [ 1, 1, 1 ] );
568
+ rayDispatchConverter.queueCounters = queueCounters;
569
+ this.dispatch( rayDispatchConverter, [ 1, 1, 1 ] );
505
570
 
506
571
  traceRayKernel.rayQueue = rayQueue;
572
+ traceRayKernel.queueCounters = queueCounters;
507
573
  traceRayKernel.rayIntersectionsStorage = rayIntersectionsStorage;
508
- renderer.compute( traceRayKernel.kernel, rayDispatchConverter.outputDispatch );
574
+ this.dispatch( traceRayKernel, rayDispatchConverter.outputDispatch );
509
575
 
510
- shadowDispatchConverter.queue = shadowRayQueue;
511
- renderer.compute( shadowDispatchConverter.kernel, [ 1, 1, 1 ] );
576
+ shadowDispatchConverter.queueCounters = queueCounters;
577
+ this.dispatch( shadowDispatchConverter, [ 1, 1, 1 ] );
512
578
 
513
579
  traceShadowRayKernel.shadowRayQueue = shadowRayQueue;
580
+ traceShadowRayKernel.queueCounters = queueCounters;
514
581
  traceShadowRayKernel.shadowRayIntersectionsStorage = shadowRayIntersectionsStorage;
515
- 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 );
516
601
 
517
602
  }
518
603