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,1302 @@
1
+ import { Box3, DataTexture, LinearFilter, Vector2, Scene, PerspectiveCamera, Color, NoToneMapping, FloatType, Timer, StorageTexture, MeshBasicNodeMaterial, Matrix4, WebGPUCoordinateSystem } from 'three/webgpu';
2
+ import { uv, uniform, varying } from 'three/tsl';
3
+ import { SkinnedMeshBVH, MeshBVH, SAH } from 'three-mesh-bvh';
4
+ import { ndcToCameraRay, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+ import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
6
+ import { RenderToScreenNodeMaterial } from './materials/RenderToScreenMaterial.js';
7
+ import { getDebugBoundsFunction } from './nodes/debugBounds.wgsl.js';
8
+ import { MegaKernelPathTracer } from './MegaKernelPathTracer.js';
9
+ import { WaveFrontPathTracer } from './WaveFrontPathTracer.js';
10
+ import { CubeToEquirectGenerator } from '../utils/CubeToEquirectGenerator.js';
11
+ import { PathtracerBVHComputeData } from './nodes/PathtracerBVHComputeData.js';
12
+ import { AtlasDebugMaterial } from './materials/debug/AtlasDebugMaterial.js';
13
+ import { SampleDensityMaterial } from './materials/debug/SampleDensityMaterial.js';
14
+ import { setCommonAttributes } from '../core/utils/GeometryPreparationUtils.js';
15
+ import { getLights } from '../core/utils/sceneUpdateUtils.js';
16
+ import { GltfCompliantMaterial } from './materials/GltfCompliantMaterial.js';
17
+ import { TRANSMISSIVE_BACKGROUND_OVERLAY } from './constants.js';
18
+ import * as RANDOM_BLUE_DITHER from './nodes/rand/bluedither.wgsl.js';
19
+ /** @import { Camera, Scene, Texture, WebGPURenderer } from 'three/webgpu' */
20
+ /** @import { AtlasTexture } from './AtlasTexture.js' */
21
+ /** @import { OIDNDenoiser } from './denoise/OIDNDenoiser.js' */
22
+ /** @import { FSRUpscaler } from './upscale/FSRUpscaler.js' */
23
+ /** @import { PathtracingMaterial } from './materials/PathtracingMaterial.js' */
24
+
25
+ /**
26
+ * @typedef {Object} SampleCounts
27
+ * @property {number} min - Lowest sample count of any pixel.
28
+ * @property {number} max - Highest sample count of any pixel.
29
+ * @property {number} avg - Mean sample count across the image.
30
+ * @property {number} samplesPerSecond - Mean samples accumulated per second since the last reset.
31
+ */
32
+
33
+ const _resolution = new Vector2();
34
+ const _color = new Color();
35
+
36
+ class TextureCache {
37
+
38
+ constructor( renderer, key ) {
39
+
40
+ this.key = key;
41
+ this.renderer = renderer;
42
+ this.texture = null;
43
+ this.hash = null;
44
+
45
+ const colorTex = new DataTexture( new Uint8Array( [ 255, 255, 255, 255 ] ), 1, 1 );
46
+ colorTex.minFilter = LinearFilter;
47
+ colorTex.magFilter = LinearFilter;
48
+ this.colorTex = colorTex;
49
+
50
+ }
51
+
52
+ dispose() {
53
+
54
+ this.colorTex.dispose();
55
+ this.texture?.dispose();
56
+
57
+ }
58
+
59
+ setFromScene( scene ) {
60
+
61
+ const { renderer, key, colorTex } = this;
62
+ let value = scene[ key ];
63
+ let hash = '';
64
+
65
+ if ( ! value ) {
66
+
67
+ const clearAlpha = renderer.getClearAlpha();
68
+ renderer.getClearColor( _color );
69
+
70
+ colorTex.image.data[ 0 ] = _color.r * 255;
71
+ colorTex.image.data[ 1 ] = _color.g * 255;
72
+ colorTex.image.data[ 2 ] = _color.b * 255;
73
+ colorTex.image.data[ 3 ] = clearAlpha * 255;
74
+
75
+ value = colorTex;
76
+ hash = colorTex.image.data.join();
77
+ colorTex.needsUpdate = hash !== this.hash;
78
+
79
+ } else if ( value.isColor ) {
80
+
81
+ colorTex.image.data[ 0 ] = value.r * 255;
82
+ colorTex.image.data[ 1 ] = value.g * 255;
83
+ colorTex.image.data[ 2 ] = value.b * 255;
84
+ colorTex.image.data[ 3 ] = 255;
85
+
86
+ value = colorTex;
87
+ hash = colorTex.image.data.join();
88
+ colorTex.needsUpdate = hash !== this.hash;
89
+
90
+ } else if ( value.isCubeTexture ) {
91
+
92
+ hash = null;
93
+ value = new CubeToEquirectGenerator( renderer ).generate( value );
94
+
95
+ } else {
96
+
97
+ value = value.clone();
98
+ hash = value.uuid + '_' + value.version;
99
+
100
+ }
101
+
102
+ if ( hash === null ) {
103
+
104
+ this.texture?.dispose();
105
+
106
+ this.texture = value;
107
+ this.hash = null;
108
+ return true;
109
+
110
+ } else {
111
+
112
+ const needsUpdate = hash !== this.hash;
113
+ if ( needsUpdate ) {
114
+
115
+ this.texture?.dispose();
116
+ this.texture = value;
117
+ this.hash = hash;
118
+
119
+ }
120
+
121
+ return needsUpdate;
122
+
123
+ }
124
+
125
+ }
126
+
127
+ }
128
+
129
+ /**
130
+ * Progressive path tracer running on WebGPU. Call {@link WebGPUPathTracer#setScene} once,
131
+ * then {@link WebGPUPathTracer#renderSample} every frame to accumulate samples into the canvas.
132
+ *
133
+ * The scene is captured when `setScene` is called, so changes to geometry, materials, lights, or
134
+ * the environment afterward require the matching `update*` function. Any change that invalidates
135
+ * the accumulated image restarts it.
136
+ */
137
+ export class WebGPUPathTracer {
138
+
139
+ /**
140
+ * Maximum number of times a ray can scatter before the path is terminated. Higher values
141
+ * resolve more indirect light at the cost of speed.
142
+ * @type {number}
143
+ * @default 15
144
+ */
145
+ get maxBounces() {
146
+
147
+ return this._pathTracer.maxBounces;
148
+
149
+ }
150
+
151
+ set maxBounces( v ) {
152
+
153
+ this._pathTracer.maxBounces = v;
154
+ this._pathTracer.reset();
155
+
156
+ }
157
+
158
+ /**
159
+ * Number of path slots dispatched per {@link WebGPUPathTracer#renderSample} call,
160
+ * independent of resolution. Raising it trades frame rate for convergence speed.
161
+ * @type {number}
162
+ * @default 250000
163
+ */
164
+ get frameBudget() {
165
+
166
+ return this._pathTracer.frameBudget;
167
+
168
+ }
169
+
170
+ set frameBudget( v ) {
171
+
172
+ this._pathTracer.frameBudget = v;
173
+
174
+ }
175
+
176
+ /**
177
+ * Maximum number of alpha tested surfaces a ray can pass through. Counted separately from
178
+ * {@link WebGPUPathTracer#maxBounces} so foliage and cutouts cannot exhaust the bounce budget.
179
+ * @type {number}
180
+ * @default 5
181
+ */
182
+ get maxTransparentBounces() {
183
+
184
+ return this._pathTracer.maxTransparentBounces;
185
+
186
+ }
187
+
188
+ set maxTransparentBounces( v ) {
189
+
190
+ this._pathTracer.maxTransparentBounces = v;
191
+ this._pathTracer.reset();
192
+
193
+ }
194
+
195
+ /**
196
+ * Stops accumulating once every pixel reaches this many samples. A denoiser only runs once the
197
+ * render has stopped, so it has no effect while this is `0`.
198
+ * @type {number}
199
+ * @default 0
200
+ * @note `0` renders indefinitely.
201
+ */
202
+ get maxSamples() {
203
+
204
+ return this._pathTracer.maxSamples;
205
+
206
+ }
207
+
208
+ set maxSamples( v ) {
209
+
210
+ this._pathTracer.maxSamples = v;
211
+
212
+ // the settle point moved, so a finished pass may need to run again against a longer
213
+ // render. The accumulated samples are still valid
214
+ this._denoiser?.reset();
215
+
216
+ }
217
+
218
+ /**
219
+ * Measures the per pixel sample counts. Use `min` for convergence checks and `avg` to display.
220
+ * @returns {Promise<SampleCounts>}
221
+ * @note The wavefront backend reduces the counts on the GPU and reads them back, so only call
222
+ * this when the numbers are needed.
223
+ */
224
+ async getSampleCountsAsync() {
225
+
226
+ const pathTracer = this._pathTracer;
227
+
228
+ // written in place. reset swaps in a new object so a late measurement can't overwrite it.
229
+ const counts = this._lastSampleCounts;
230
+ const measured = await pathTracer.getSampleCountsAsync();
231
+
232
+ counts.min = measured.min;
233
+ counts.max = measured.max;
234
+ counts.avg = measured.avg;
235
+
236
+ // averaged over the whole render, and idle time counts against it
237
+ const elapsed = this.getRenderTime() / 1000;
238
+ counts.samplesPerSecond = elapsed > 0 ? counts.avg / elapsed : 0;
239
+
240
+ return { ...counts };
241
+
242
+ }
243
+
244
+ /**
245
+ * How the background is treated behind transmissive surfaces. One of
246
+ * `TRANSMISSIVE_BACKGROUND_OVERLAY`, `TRANSMISSIVE_BACKGROUND_ENVIRONMENT`, or
247
+ * `TRANSMISSIVE_BACKGROUND_TRANSPARENT`.
248
+ * @type {number}
249
+ * @default TRANSMISSIVE_BACKGROUND_OVERLAY
250
+ */
251
+ get transmissiveBackground() {
252
+
253
+ return this._transmissiveBackground;
254
+
255
+ }
256
+
257
+ set transmissiveBackground( v ) {
258
+
259
+ if ( this._transmissiveBackground !== v ) {
260
+
261
+ this._transmissiveBackground = v;
262
+ this._pathTracer.setTransmissiveBackground( v );
263
+
264
+ }
265
+
266
+ }
267
+
268
+ /**
269
+ * Blurs sharp reflections seen through rough surfaces to suppress fireflies. Higher values
270
+ * remove more noise and more detail.
271
+ * @type {number}
272
+ * @default 1
273
+ * @note `0` disables the filter.
274
+ */
275
+ get filterGlossyFactor() {
276
+
277
+ return this._filterGlossyFactor;
278
+
279
+ }
280
+
281
+ set filterGlossyFactor( v ) {
282
+
283
+ if ( this._filterGlossyFactor !== v ) {
284
+
285
+ this._filterGlossyFactor = v;
286
+ this._pathTracer.setFilterGlossy( v );
287
+
288
+ }
289
+
290
+ }
291
+
292
+ /**
293
+ * Whether to combine light sampling and bsdf sampling with MIS. Disabling it makes lights
294
+ * noticeably noisier.
295
+ * @type {boolean}
296
+ * @default true
297
+ */
298
+ get multipleImportanceSampling() {
299
+
300
+ return this._multipleImportanceSampling;
301
+
302
+ }
303
+
304
+ set multipleImportanceSampling( v ) {
305
+
306
+ if ( this._multipleImportanceSampling !== v ) {
307
+
308
+ this.setMultipleImportanceSampling( v );
309
+
310
+ }
311
+
312
+ }
313
+
314
+ /**
315
+ * Upper bound on the contribution of a directly lit path segment, for suppressing fireflies.
316
+ * @type {number}
317
+ * @default 0
318
+ * @note `0` disables the clamp. Clamping darkens the image and biases the result.
319
+ */
320
+ get clampDirect() {
321
+
322
+ return this._clampDirect;
323
+
324
+ }
325
+
326
+ set clampDirect( v ) {
327
+
328
+ v = Math.max( 0, v );
329
+ if ( this._clampDirect !== v ) {
330
+
331
+ this._clampDirect = v;
332
+ this._pathTracer.setClamping( v, this._clampIndirect );
333
+
334
+ }
335
+
336
+ }
337
+
338
+ /**
339
+ * Upper bound on the contribution of an indirectly lit path segment, for suppressing
340
+ * fireflies.
341
+ * @type {number}
342
+ * @default 10
343
+ * @note `0` disables the clamp. Clamping darkens the image and biases the result.
344
+ */
345
+ get clampIndirect() {
346
+
347
+ return this._clampIndirect;
348
+
349
+ }
350
+
351
+ set clampIndirect( v ) {
352
+
353
+ v = Math.max( 0, v );
354
+ if ( this._clampIndirect !== v ) {
355
+
356
+ this._clampIndirect = v;
357
+ this._pathTracer.setClamping( this._clampDirect, v );
358
+
359
+ }
360
+
361
+ }
362
+
363
+ /**
364
+ * The render target the accumulated samples are written into.
365
+ * @type {Texture|null}
366
+ * @readonly
367
+ */
368
+ get target() {
369
+
370
+ return this._pathTracer.outputTarget ?? null;
371
+
372
+ }
373
+
374
+ /**
375
+ * Progress of the fade from the low resolution preview to the full render, from 0 to 1.
376
+ * @type {number}
377
+ * @readonly
378
+ */
379
+ get fadeState() {
380
+
381
+ return this._fadeState;
382
+
383
+ }
384
+
385
+ /**
386
+ * The low resolution preview rendered while {@link WebGPUPathTracer#renderDelay} elapses.
387
+ * @type {Texture}
388
+ * @readonly
389
+ */
390
+ get lowResTarget() {
391
+
392
+ return this._lowResTarget;
393
+
394
+ }
395
+
396
+ /**
397
+ * Whether the tracer is currently rendering the low resolution preview.
398
+ * @type {boolean}
399
+ * @readonly
400
+ */
401
+ get lowResMode() {
402
+
403
+ return this._pathTracer.lowResMode;
404
+
405
+ }
406
+
407
+ /**
408
+ * The atlas every scene texture is packed into for the kernels to sample.
409
+ * @type {AtlasTexture}
410
+ * @readonly
411
+ */
412
+ get textureAtlas() {
413
+
414
+ return this._bvhData.textureAtlas;
415
+
416
+ }
417
+
418
+ /**
419
+ * Switches between the two tracing backends and rebuilds the kernels. The wavefront backend is
420
+ * used by default and is faster on most scenes.
421
+ *
422
+ * @param {boolean} value - `true` for the megakernel, `false` for the wavefront tracer.
423
+ * @private
424
+ */
425
+ useMegakernel( value ) {
426
+
427
+ const maxSamples = this._pathTracer.maxSamples;
428
+
429
+ this._pathTracer.dispose();
430
+ this._pathTracer = value ? new MegaKernelPathTracer( this._renderer ) : new WaveFrontPathTracer( this._renderer );
431
+ this._pathTracer.maxSamples = maxSamples;
432
+ this._pathTracer.setBVHData( this._bvhData );
433
+ this._pathTracer.setMaterial( this.material );
434
+ this._pathTracer.setRandom( this.random );
435
+ this._pathTracer.setMultipleImportanceSampling( this.multipleImportanceSampling );
436
+ this._pathTracer.setTransmissiveBackground( this._transmissiveBackground );
437
+ this._pathTracer.setFilterGlossy( this._filterGlossyFactor );
438
+ this._pathTracer.setClamping( this._clampDirect, this._clampIndirect );
439
+ this.setCamera( this.camera );
440
+
441
+ // the new tracer has default environment and background maps so apply the cached scene textures
442
+ this._pathTracer.setEnvironment( this._environmentCache.texture );
443
+ this._pathTracer.setBackground( this._backgroundCache.texture );
444
+ this.updateEnvironment();
445
+ this.updateLights();
446
+
447
+ }
448
+
449
+ /**
450
+ * @param {WebGPURenderer} renderer
451
+ */
452
+ constructor( renderer ) {
453
+
454
+ // members
455
+ this._renderer = renderer;
456
+ this._timer = new Timer();
457
+
458
+ this._environmentCache = new TextureCache( renderer, 'environment' );
459
+ this._backgroundCache = new TextureCache( renderer, 'background' );
460
+
461
+ this._resetTime = - 1;
462
+ this._fadeState = 0;
463
+ this._lastSampleCounts = { min: 0, max: 0, avg: 0, samplesPerSecond: 0 };
464
+ this._size = new Vector2();
465
+ this._blitQuad = new FullScreenQuad( new RenderToScreenNodeMaterial() );
466
+
467
+ // avoid mipmap gen on copy, not always supported with float type
468
+ this._lowResTarget = new StorageTexture( 1, 1 );
469
+ this._lowResTarget.type = FloatType;
470
+ this._lowResTarget.generateMipmaps = false;
471
+
472
+ this._pathTracer = new WaveFrontPathTracer( renderer );
473
+
474
+ // optional post passes, attached with "setDenoiser" / "setUpscaler"
475
+ this._denoiser = null;
476
+ this._upscaler = null;
477
+
478
+ // options
479
+
480
+ /**
481
+ * Samples every pixel must reach before the full resolution render is faded in.
482
+ * @type {number}
483
+ * @default 1
484
+ */
485
+ this.minSamples = 1;
486
+
487
+ /**
488
+ * Milliseconds to show the low resolution preview for after a reset.
489
+ * @type {number}
490
+ * @default 500
491
+ */
492
+ this.renderDelay = 500;
493
+
494
+ /**
495
+ * Milliseconds taken to cross fade from the preview to the full render.
496
+ * @type {number}
497
+ * @default 500
498
+ */
499
+ this.fadeDuration = 500;
500
+
501
+ /**
502
+ * Whether to render a low resolution preview while the camera is moving.
503
+ * @type {boolean}
504
+ * @default true
505
+ */
506
+ this.dynamicLowRes = true;
507
+
508
+ /**
509
+ * Resolution of the low resolution preview, as a fraction of the render size.
510
+ * @type {number}
511
+ * @default 0.1
512
+ */
513
+ this.lowResScale = 0.1;
514
+
515
+ /**
516
+ * Resolution the path tracer renders at, as a fraction of the canvas size. Lowering it
517
+ * converges faster at the cost of detail, and pairs with an upscaler.
518
+ * @type {number}
519
+ * @default 1
520
+ */
521
+ this.renderScale = 1;
522
+
523
+ /**
524
+ * Whether to track the canvas size automatically. Set to `false` to drive the render
525
+ * size with {@link WebGPUPathTracer#setSize}.
526
+ * @type {boolean}
527
+ * @default true
528
+ */
529
+ this.synchronizeRenderSize = true;
530
+
531
+ /**
532
+ * Whether to generate the attributes in {@link WebGPUPathTracer#commonAttributes} on
533
+ * geometry that is missing them when the scene is set.
534
+ * @type {boolean}
535
+ * @default true
536
+ */
537
+ this.generateMissingAttributes = true;
538
+
539
+ /**
540
+ * Vertex attributes every geometry is expected to provide.
541
+ * @type {Array<string>}
542
+ * @default [ 'normal', 'tangent' ]
543
+ */
544
+ this.commonAttributes = [ 'normal', 'tangent' ];
545
+
546
+ /**
547
+ * Whether to restart the random sequence on reset so a given camera always produces the
548
+ * same image.
549
+ * @type {boolean}
550
+ * @default true
551
+ */
552
+ this.stableNoise = true;
553
+
554
+ /**
555
+ * Whether to stop accumulating samples. The last image keeps being presented.
556
+ * @type {boolean}
557
+ * @default false
558
+ */
559
+ this.pause = false;
560
+
561
+ this.filterGlossyFactor = 1;
562
+ this._clampDirect = 0;
563
+ this._clampIndirect = 10;
564
+ this.multipleImportanceSampling = true;
565
+ this.transmissiveBackground = TRANSMISSIVE_BACKGROUND_OVERLAY;
566
+
567
+ /**
568
+ * Random number generator the kernels sample with.
569
+ * @type {Object}
570
+ * @private
571
+ */
572
+ this.random = RANDOM_BLUE_DITHER;
573
+
574
+ /**
575
+ * Material model the kernels evaluate surfaces with.
576
+ * @type {PathtracingMaterial}
577
+ * @private
578
+ */
579
+ this.material = new GltfCompliantMaterial();
580
+
581
+ // default camera ray generation ( perspective / orthographic ), assigned onto each bvh compute
582
+ // data's fns so the kernels can proxy it. The uniform is the inverse view-projection
583
+ // ( world * inverseProjection ), premultiplied on the CPU so no matrix multiply runs per ray.
584
+ this._invViewProjectionMatrix = uniform( new Matrix4() );
585
+ this._cameraRayFnHandle = null;
586
+
587
+ // initialize the scene so it doesn't fail
588
+ this.setMaterial( this.material );
589
+ this.setRandom( this.random );
590
+ this.setScene( new Scene(), new PerspectiveCamera() );
591
+
592
+ }
593
+
594
+ /**
595
+ * @param {boolean} value
596
+ */
597
+ setMultipleImportanceSampling( value ) {
598
+
599
+ this._multipleImportanceSampling = value;
600
+ this._pathTracer.setMultipleImportanceSampling( value );
601
+ this.reset();
602
+
603
+ }
604
+
605
+ /**
606
+ * Attaches a denoiser, run once the render settles and displayed in place of the raw image.
607
+ * Settings live on the instance. Pass null to remove it.
608
+ *
609
+ * @param {OIDNDenoiser|null} denoiser
610
+ */
611
+ setDenoiser( denoiser ) {
612
+
613
+ // a detached instance stops receiving resets, so clear it rather than let it hold a
614
+ // result from a camera position that has since moved
615
+ this._denoiser?.reset();
616
+ this._denoiser = denoiser;
617
+
618
+ if ( denoiser ) {
619
+
620
+ denoiser.init( this._renderer );
621
+ denoiser.setScene( this.scene, this.camera );
622
+
623
+ }
624
+
625
+ }
626
+
627
+ /**
628
+ * Attaches an upscaler, run before the image is presented so the render can happen below the
629
+ * canvas resolution. Settings live on the instance. Pass null to remove it.
630
+ *
631
+ * @param {FSRUpscaler|null} upscaler
632
+ */
633
+ setUpscaler( upscaler ) {
634
+
635
+ this._upscaler = upscaler;
636
+
637
+ if ( upscaler ) {
638
+
639
+ upscaler.init( this._renderer );
640
+
641
+ }
642
+
643
+ }
644
+
645
+ /**
646
+ * Captures the scene and camera and builds the acceleration structures the kernels trace
647
+ * against. Call this once, then use the `update*` functions for later changes.
648
+ *
649
+ * @param {Scene} scene
650
+ * @param {Camera} camera
651
+ * @note Geometry BVHs are built synchronously, so this blocks for large scenes.
652
+ */
653
+ setScene( scene, camera ) {
654
+
655
+ scene.updateMatrixWorld( true );
656
+ camera.updateMatrixWorld();
657
+
658
+ // Build BVH for each mesh geometry
659
+ scene.traverse( child => {
660
+
661
+ if ( this.generateMissingAttributes && child.geometry?.isBufferGeometry ) {
662
+
663
+ setCommonAttributes( child.geometry, this.commonAttributes );
664
+
665
+ }
666
+
667
+ if ( child.isSkinnedMesh ) {
668
+
669
+ if ( ! child.boundsTree ) {
670
+
671
+ child.boundsTree = new SkinnedMeshBVH( child, { strategy: SAH, targetLeafSize: 5, indirect: true } );
672
+
673
+ } else {
674
+
675
+ child.boundsTree.refit();
676
+
677
+ }
678
+
679
+ if ( child.boundingBox === null ) {
680
+
681
+ child.boundingBox = new Box3();
682
+
683
+ }
684
+
685
+ child.boundsTree.getBoundingBox( child.boundingBox );
686
+
687
+ } else if ( child.isMesh ) {
688
+
689
+ if ( ! child.geometry.boundsTree ) {
690
+
691
+ child.geometry.boundsTree = new MeshBVH( child.geometry, { strategy: SAH, targetLeafSize: 5 } );
692
+
693
+ }
694
+
695
+ }
696
+
697
+ } );
698
+
699
+ // Build TLAS and compute functions
700
+ const bvhData = new PathtracerBVHComputeData( scene );
701
+ bvhData.update();
702
+ bvhData.textureAtlas.setTextures( this._renderer, bvhData.textures );
703
+
704
+ if ( this._bvhData ) {
705
+
706
+ this._bvhData.dispose();
707
+ this._bvhData.textureAtlas.dispose();
708
+
709
+ }
710
+
711
+ this.scene = scene;
712
+ this._bvhData = bvhData;
713
+ this._pathTracer.setBVHData( bvhData );
714
+ this.setCamera( camera );
715
+ this.updateEnvironment();
716
+ this.updateLights();
717
+
718
+ // the denoiser rasterizes its own guide buffers, so it needs the scene too
719
+ this._denoiser?.setScene( scene, camera );
720
+
721
+ }
722
+
723
+ /**
724
+ * @returns {PathtracingMaterial}
725
+ * @private
726
+ */
727
+ // TODO: consider renaming these functions or removing them
728
+ getMaterial() {
729
+
730
+ return this.material;
731
+
732
+ }
733
+
734
+ /**
735
+ * Replaces the material model and recompiles the kernels.
736
+ *
737
+ * @param {PathtracingMaterial} material
738
+ * @private
739
+ */
740
+ setMaterial( material ) {
741
+
742
+ this.material = material;
743
+ this._pathTracer.setMaterial( material );
744
+ this.reset();
745
+
746
+ }
747
+
748
+ /**
749
+ * Replaces the random number generator and recompiles the kernels.
750
+ *
751
+ * @param {Object} random - One of the Random Strategies constants, such as `RANDOM_BLUE_DITHER`.
752
+ */
753
+ setRandom( random ) {
754
+
755
+ this.random = random;
756
+ this._pathTracer.setRandom( random );
757
+ this.reset();
758
+
759
+ }
760
+
761
+ /**
762
+ * Replaces the camera the rays are generated from. Cameras with a `getCameraRayFn`, such as
763
+ * `PhysicalCamera` and `EquirectCamera`, provide their own ray generation.
764
+ *
765
+ * @param {Camera} camera
766
+ */
767
+ setCamera( camera ) {
768
+
769
+ this.camera = camera;
770
+
771
+ if ( camera.getCameraRayFn ) {
772
+
773
+ this._cameraRayFnHandle = camera.getCameraRayFn();
774
+
775
+ } else {
776
+
777
+ // add a default camera ray getter. the update function is called when the camera is
778
+ // updated to trigger any necessary uniform updates.
779
+ const invViewProjectionMatrix = this._invViewProjectionMatrix;
780
+ this._cameraRayFnHandle = {
781
+ update: () => {
782
+
783
+ camera.coordinateSystem = WebGPUCoordinateSystem;
784
+ camera.updateMatrixWorld();
785
+
786
+ if ( camera.isOrthographicCamera || camera.isPerspectiveCamera ) {
787
+
788
+ camera.updateProjectionMatrix();
789
+
790
+ }
791
+
792
+ invViewProjectionMatrix.value.multiplyMatrices( camera.matrixWorld, camera.projectionMatrixInverse );
793
+ return false;
794
+
795
+ },
796
+ fn: wgslTagFn/* wgsl */`
797
+ fn getCameraRay( uv: vec2f, resolution: vec2f, ray: ptr<function, ${ rayStruct }> ) -> bool {
798
+
799
+ let ndc = uv * 2.0 - vec2f( 1.0 );
800
+ *ray = ${ ndcToCameraRay }( ndc, ${ invViewProjectionMatrix } );
801
+ return true;
802
+
803
+ }
804
+ `,
805
+ };
806
+
807
+ }
808
+
809
+ this._bvhData.fns.getCameraRay = this._cameraRayFnHandle.fn;
810
+ this._cameraRayFnHandle.update();
811
+ this._pathTracer.rebuild();
812
+ this.reset();
813
+
814
+ }
815
+
816
+ /**
817
+ * Re-reads the material properties and textures from the scene. Call after changing any
818
+ * material.
819
+ */
820
+ updateMaterials() {
821
+
822
+ const { _bvhData, _renderer } = this;
823
+ _bvhData.updateMaterials();
824
+ _bvhData.textureAtlas.setTextures( _renderer, _bvhData.textures );
825
+ this.reset();
826
+
827
+ }
828
+
829
+ /**
830
+ * Re-reads the world matrices from the scene. Call after moving any object.
831
+ * @note Changing geometry requires {@link WebGPUPathTracer#setScene} instead, since the BVH
832
+ * must be rebuilt.
833
+ */
834
+ updateTransforms() {
835
+
836
+ this.scene.updateMatrixWorld( true );
837
+ this._bvhData.updateTransforms();
838
+ this.reset();
839
+
840
+ }
841
+
842
+ /**
843
+ * Re-reads the camera transform and projection. Call after moving the camera.
844
+ */
845
+ updateCamera() {
846
+
847
+ const { _cameraRayFnHandle, _pathTracer } = this;
848
+ if ( _cameraRayFnHandle.update() ) {
849
+
850
+ _pathTracer.rebuild();
851
+
852
+ }
853
+
854
+ this.reset();
855
+
856
+ }
857
+
858
+ /**
859
+ * Re-reads `scene.environment` and `scene.background`, along with their intensity, rotation,
860
+ * and blur.
861
+ */
862
+ updateEnvironment() {
863
+
864
+ const {
865
+ _pathTracer,
866
+ scene,
867
+
868
+ _environmentCache,
869
+ _backgroundCache,
870
+ } = this;
871
+
872
+ // update the texture if they've changed
873
+ if ( _environmentCache.setFromScene( scene ) ) {
874
+
875
+ _pathTracer.setEnvironment( _environmentCache.texture );
876
+
877
+ }
878
+
879
+ if ( _backgroundCache.setFromScene( scene ) ) {
880
+
881
+ _pathTracer.setBackground( _backgroundCache.texture );
882
+
883
+ }
884
+
885
+ // update the params always since they're cheap
886
+ _pathTracer.setEnvironmentParams(
887
+ scene.environmentIntensity,
888
+ scene.environmentRotation,
889
+ );
890
+
891
+ _pathTracer.setBackgroundParams(
892
+ scene.backgroundIntensity,
893
+ scene.backgroundRotation,
894
+ scene.backgroundBlurriness,
895
+ );
896
+
897
+ this.reset();
898
+
899
+ }
900
+
901
+ /**
902
+ * Re-collects the lights in the scene. Call after adding, removing, or changing one.
903
+ */
904
+ updateLights() {
905
+
906
+ const { _pathTracer, scene } = this;
907
+
908
+ const lights = getLights( scene );
909
+ _pathTracer.setLights( lights );
910
+ this.reset();
911
+
912
+ }
913
+
914
+ /**
915
+ * Sets the resolution the path tracer renders at. Only used when
916
+ * {@link WebGPUPathTracer#synchronizeRenderSize} is `false`.
917
+ *
918
+ * @param {number} x
919
+ * @param {number} y
920
+ */
921
+ setSize( x, y ) {
922
+
923
+ if ( this._size.x !== x || this._size.y !== y ) {
924
+
925
+ this._size.set( x, y );
926
+ this.reset();
927
+
928
+ }
929
+
930
+ }
931
+
932
+ /**
933
+ * Discards the accumulated image and starts over. The `update*` functions call this for you.
934
+ */
935
+ reset() {
936
+
937
+ this._pathTracer.reset();
938
+ this._denoiser?.reset();
939
+ this._resetTime = - 1;
940
+ this._fadeState = 0;
941
+ this._timer.update();
942
+
943
+ // a fresh object rather than a clear, so measurements still in flight detach
944
+ this._lastSampleCounts = { min: 0, max: 0, avg: 0, samplesPerSecond: 0 };
945
+
946
+ if ( this.stableNoise ) {
947
+
948
+ this._pathTracer.resetSeed();
949
+
950
+ }
951
+
952
+ }
953
+
954
+ /**
955
+ * Accumulates one round of samples and presents the result to the canvas. Call once per frame.
956
+ */
957
+ renderSample() {
958
+
959
+ const renderer = this._renderer;
960
+ const size = this._size;
961
+ const blitQuad = this._blitQuad;
962
+ const pathTracer = this._pathTracer;
963
+ const lowResTarget = this._lowResTarget;
964
+ const timer = this._timer;
965
+ const {
966
+ renderDelay,
967
+ dynamicLowRes,
968
+ synchronizeRenderSize,
969
+ renderScale,
970
+ lowResScale,
971
+ minSamples,
972
+ maxSamples,
973
+ } = this;
974
+
975
+ timer.update();
976
+
977
+ if ( ! this._renderer._initialized ) {
978
+
979
+ return;
980
+
981
+ }
982
+
983
+ if ( ! this.material.initialized ) {
984
+
985
+ this.material.init( renderer );
986
+ this.material.initialized = true;
987
+
988
+ }
989
+
990
+ let delta = 1000 * timer.getDelta();
991
+ const firstFrame = this._resetTime === - 1;
992
+ if ( firstFrame ) {
993
+
994
+ this._resetTime = 0;
995
+ delta = 0.0;
996
+
997
+ }
998
+
999
+ this._resetTime += delta;
1000
+
1001
+ const originalTarget = renderer.getRenderTarget();
1002
+ const originalAutoClear = renderer.autoClear;
1003
+
1004
+ // handle canvas-size auto synchronization
1005
+ if ( synchronizeRenderSize ) {
1006
+
1007
+ renderer.getDrawingBufferSize( _resolution );
1008
+ const w = Math.floor( renderScale * _resolution.x );
1009
+ const h = Math.floor( renderScale * _resolution.y );
1010
+ this.setSize( w, h );
1011
+
1012
+ }
1013
+
1014
+ // check if we should be in low res mode and calculate the target size
1015
+ let { width, height } = size;
1016
+ const lowResMode = this._resetTime < renderDelay || ( firstFrame && dynamicLowRes && minSamples !== 0 );
1017
+ if ( lowResMode ) {
1018
+
1019
+ width = Math.ceil( lowResScale * width );
1020
+ height = Math.ceil( lowResScale * height );
1021
+
1022
+ }
1023
+
1024
+ // set the size if necessary
1025
+ pathTracer.getSize( _resolution );
1026
+
1027
+ const resized = _resolution.x !== width || _resolution.y !== height;
1028
+ if ( resized ) {
1029
+
1030
+ if ( ! lowResMode && dynamicLowRes ) {
1031
+
1032
+ // copy the low reset content if we're transitioning to the full
1033
+ // resolution view so we can fade to it
1034
+ lowResTarget.setSize( Math.ceil( lowResScale * width ), Math.ceil( lowResScale * height ) );
1035
+ renderer.copyTextureToTexture( pathTracer.outputTarget, lowResTarget );
1036
+
1037
+ }
1038
+
1039
+ pathTracer.setSize( width, height );
1040
+
1041
+ }
1042
+
1043
+ // update the samples
1044
+ if ( ! this.pause && ( ! lowResMode || ( lowResMode && dynamicLowRes ) ) ) {
1045
+
1046
+ pathTracer.lowResMode = lowResMode;
1047
+ pathTracer.update();
1048
+
1049
+ }
1050
+
1051
+ const denoiser = this._denoiser;
1052
+ const upscaler = this._upscaler;
1053
+
1054
+ // the denoiser runs once the render stops, so an uncapped render never denoises
1055
+ // TODO: this only needs to know whether the render stopped, but measures the per pixel
1056
+ // counts every frame. A count of the camera rays dispatched would answer it in one value
1057
+ const awaitingDenoise = Boolean( denoiser ) && ! denoiser.complete && ! denoiser.running && maxSamples > 0;
1058
+
1059
+ // Gate on the least converged pixel. Measuring is expensive so it stops once faded in, and
1060
+ // the check reads the last measurement rather than waiting on this one.
1061
+ if ( ! lowResMode ) {
1062
+
1063
+ if ( ( this._fadeState < 1 && minSamples > 0 ) || awaitingDenoise ) {
1064
+
1065
+ this.getSampleCountsAsync();
1066
+
1067
+ }
1068
+
1069
+ if ( this._lastSampleCounts.min >= minSamples ) {
1070
+
1071
+ this._fadeState += delta / this.fadeDuration;
1072
+ this._fadeState = Math.min( 1.0, this._fadeState ) || 1.0;
1073
+
1074
+ }
1075
+
1076
+ }
1077
+
1078
+
1079
+ // render the content to the canvas
1080
+ const opacity = ( lowResMode && dynamicLowRes ? 1.0 : this._fadeState );
1081
+
1082
+ // the low res preview is replaced moments later, so it is neither denoised nor upscaled
1083
+ let texture = pathTracer.outputTarget;
1084
+ if ( denoiser && ! lowResMode ) {
1085
+
1086
+ // "min" so every pixel has stopped, not just the average
1087
+ if ( awaitingDenoise && this._lastSampleCounts.min >= maxSamples ) {
1088
+
1089
+ denoiser.update( pathTracer.outputTarget );
1090
+
1091
+ }
1092
+
1093
+ texture = denoiser.texture ?? texture;
1094
+
1095
+ }
1096
+
1097
+ if ( upscaler && ! lowResMode ) {
1098
+
1099
+ texture = upscaler.upscale( texture, this.camera );
1100
+
1101
+ // the pass above binds its own targets
1102
+ renderer.setRenderTarget( originalTarget );
1103
+
1104
+ }
1105
+
1106
+ renderer.autoClear = dynamicLowRes ? true : opacity === 1.0;
1107
+ blitQuad.material.transition = dynamicLowRes ? opacity : 1.0;
1108
+ blitQuad.material.opacity = dynamicLowRes ? 1.0 : opacity;
1109
+ blitQuad.material.fromTexture = lowResTarget;
1110
+ blitQuad.material.texture = texture;
1111
+ blitQuad.render( renderer );
1112
+
1113
+ // reset the renderer
1114
+ renderer.autoClear = originalAutoClear;
1115
+ renderer.setRenderTarget( originalTarget );
1116
+
1117
+ }
1118
+
1119
+ /**
1120
+ * Draws a heatmap of how many BVH bounding boxes each camera ray intersects, for diagnosing
1121
+ * box overlap and traversal cost. Hotter pixels traverse more nodes.
1122
+ *
1123
+ * @param {Object} [options]
1124
+ * @param {boolean} [options.displayTLAS=true] - Count the top level object bounding boxes.
1125
+ * @param {boolean} [options.displayBLAS=true] - Count the per object geometry bounding boxes.
1126
+ * @param {boolean} [options.stopAtSurface=false] - Only count boxes in front of the nearest
1127
+ * hit, so occluded boxes do not contribute. Culling and material transparency are honored.
1128
+ * @param {number} [options.saturationCount=64] - Node count that saturates to full heat.
1129
+ */
1130
+ renderDebugBounds( options = {} ) {
1131
+
1132
+ const {
1133
+ displayTLAS = true,
1134
+ displayBLAS = true,
1135
+ stopAtSurface = false,
1136
+ saturationCount = 64,
1137
+ } = options;
1138
+
1139
+ const renderer = this._renderer;
1140
+ const camera = this.camera;
1141
+
1142
+ if ( ! renderer._initialized ) {
1143
+
1144
+ return;
1145
+
1146
+ }
1147
+
1148
+ camera.updateMatrixWorld();
1149
+
1150
+ // (re)build the quad if it hasn't been built or the bvh data has changed
1151
+ if ( this._debugBoundsQuad === undefined || this._debugBoundsData !== this._bvhData ) {
1152
+
1153
+ this._buildDebugBoundsQuad();
1154
+
1155
+ }
1156
+
1157
+ const uniforms = this._debugBoundsUniforms;
1158
+ uniforms.cameraToModelMatrix.value.copy( camera.matrixWorld );
1159
+ uniforms.inverseProjectionMatrix.value.copy( camera.projectionMatrixInverse );
1160
+ uniforms.displayTLAS.value = displayTLAS ? 1 : 0;
1161
+ uniforms.displayBLAS.value = displayBLAS ? 1 : 0;
1162
+ uniforms.stopAtSurface.value = stopAtSurface ? 1 : 0;
1163
+ uniforms.saturationCount.value = saturationCount;
1164
+
1165
+ const originalTarget = renderer.getRenderTarget();
1166
+ const originalAutoClear = renderer.autoClear;
1167
+ const originalToneMapping = renderer.toneMapping;
1168
+
1169
+ renderer.setRenderTarget( null );
1170
+ renderer.autoClear = true;
1171
+ renderer.toneMapping = NoToneMapping;
1172
+
1173
+ this._debugBoundsQuad.render( renderer );
1174
+
1175
+ renderer.setRenderTarget( originalTarget );
1176
+ renderer.autoClear = originalAutoClear;
1177
+ renderer.toneMapping = originalToneMapping;
1178
+
1179
+ }
1180
+
1181
+ _buildDebugBoundsQuad() {
1182
+
1183
+ const bvhData = this._bvhData;
1184
+ const debugBounds = getDebugBoundsFunction( bvhData );
1185
+
1186
+ const material = new MeshBasicNodeMaterial();
1187
+ material.colorNode = debugBounds( varying( uv() ) );
1188
+
1189
+ if ( this._debugBoundsQuad === undefined ) {
1190
+
1191
+ this._debugBoundsQuad = new FullScreenQuad( material );
1192
+
1193
+ } else {
1194
+
1195
+ this._debugBoundsQuad.material.dispose();
1196
+ this._debugBoundsQuad.material = material;
1197
+
1198
+ }
1199
+
1200
+ this._debugBoundsUniforms = debugBounds.uniforms;
1201
+ this._debugBoundsData = bvhData;
1202
+
1203
+ }
1204
+
1205
+ /**
1206
+ * Draws one layer of the texture atlas to the canvas, for debugging texture packing.
1207
+ *
1208
+ * @param {number} [layer=0]
1209
+ */
1210
+ renderTextureAtlas( layer = 0 ) {
1211
+
1212
+ const renderer = this._renderer;
1213
+
1214
+ if ( ! this._atlasDebugQuad ) {
1215
+
1216
+ this._atlasDebugQuad = new FullScreenQuad( new AtlasDebugMaterial() );
1217
+
1218
+ }
1219
+
1220
+ const quad = this._atlasDebugQuad;
1221
+ quad.material.texture = this.textureAtlas.texture;
1222
+ quad.material.layer = layer;
1223
+ renderer.setRenderTarget( null );
1224
+ quad.render( renderer );
1225
+
1226
+ }
1227
+
1228
+ /**
1229
+ * Draws a heatmap of the per pixel sample counts, for spotting pixels that converge slowly.
1230
+ * @note Measures on every call and draws with the previous result, since the readback lands a
1231
+ * frame later.
1232
+ */
1233
+ // TODO: bind the counters buffer in the shader instead to avoid the readback.
1234
+ renderSampleDensity() {
1235
+
1236
+ const renderer = this._renderer;
1237
+
1238
+ if ( ! renderer._initialized ) {
1239
+
1240
+ return;
1241
+
1242
+ }
1243
+
1244
+ this.getSampleCountsAsync();
1245
+
1246
+ if ( ! this._sampleDensityQuad ) {
1247
+
1248
+ this._sampleDensityQuad = new FullScreenQuad( new SampleDensityMaterial() );
1249
+
1250
+ }
1251
+
1252
+ const originalToneMapping = renderer.toneMapping;
1253
+ renderer.toneMapping = NoToneMapping;
1254
+
1255
+ const quad = this._sampleDensityQuad;
1256
+ quad.material.texture = this._pathTracer.sampleCountTarget;
1257
+ quad.material.minCount = this._lastSampleCounts.min;
1258
+ quad.material.maxCount = this._lastSampleCounts.max;
1259
+ renderer.setRenderTarget( null );
1260
+ quad.render( renderer );
1261
+
1262
+ renderer.toneMapping = originalToneMapping;
1263
+
1264
+ }
1265
+
1266
+ /**
1267
+ * Frees every GPU resource held by the tracer, including any attached denoiser and upscaler.
1268
+ */
1269
+ dispose() {
1270
+
1271
+ this._pathTracer.dispose();
1272
+ this._denoiser?.dispose();
1273
+ this._upscaler?.dispose();
1274
+ this._bvhData.dispose();
1275
+ this._bvhData.textureAtlas.dispose();
1276
+ this._environmentCache.dispose();
1277
+ this._backgroundCache.dispose();
1278
+ this._blitQuad.dispose();
1279
+ this._lowResTarget.dispose();
1280
+ this._atlasDebugQuad?.dispose();
1281
+ this._sampleDensityQuad?.dispose();
1282
+
1283
+ if ( this._debugBoundsQuad !== undefined ) {
1284
+
1285
+ this._debugBoundsQuad.dispose();
1286
+
1287
+ }
1288
+
1289
+ }
1290
+
1291
+ /**
1292
+ * Milliseconds elapsed since the last reset.
1293
+ * @returns {number}
1294
+ */
1295
+ // TODO: this is not completely accurate if the user has not called "renderSample" continuously
1296
+ getRenderTime() {
1297
+
1298
+ return this._resetTime;
1299
+
1300
+ }
1301
+
1302
+ }