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.
- package/README.md +4 -2
- package/build/index.module.js +23 -5
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +22 -4
- package/build/index.umd.cjs.map +1 -1
- package/package.json +8 -8
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/core/utils/sceneUpdateUtils.js +2 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/uniforms/LightsInfoUniformStruct.js +16 -1
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +32 -12
- package/src/webgpu/EquirectHdrInfoNode.js +97 -46
- package/src/webgpu/LightsInfoNode.js +142 -4
- package/src/webgpu/MegaKernelPathTracer.js +11 -3
- package/src/webgpu/PathTracerBackend.js +174 -21
- package/src/webgpu/TurquinTexture.js +2 -0
- package/src/webgpu/WaveFrontPathTracer.js +169 -71
- package/src/webgpu/WebGPUPathTracer.js +637 -51
- package/src/webgpu/compute/ComputeKernel.js +18 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
- package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
- package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
- package/src/webgpu/compute/ZeroOutKernel.js +3 -1
- package/src/webgpu/compute/targets.js +55 -0
- package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
- package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
- package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
- package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
- package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
- package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
- package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
- package/src/webgpu/compute/wavefront/structs.js +63 -35
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +79 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -1
- package/src/webgpu/nodes/lights.wgsl.js +21 -1
- package/src/webgpu/nodes/material.wgsl.js +2 -1
- package/src/webgpu/nodes/medium.wgsl.js +165 -0
- package/src/webgpu/nodes/random.wgsl.js +2 -0
- package/src/webgpu/nodes/sampling.wgsl.js +44 -3
- package/src/webgpu/nodes/structs.wgsl.js +8 -1
- package/src/webgpu/upscale/FSRUpscaler.js +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box3, DataTexture, LinearFilter, Vector2, Scene, PerspectiveCamera, Color, NoToneMapping, FloatType, Timer, StorageTexture, MeshBasicNodeMaterial, Matrix4, WebGPUCoordinateSystem } from 'three/webgpu';
|
|
1
|
+
import { Box3, DataTexture, EventDispatcher, LinearFilter, MathUtils, Vector2, Scene, PerspectiveCamera, Color, NoToneMapping, FloatType, Timer, StorageTexture, MeshBasicNodeMaterial, Matrix4, WebGPUCoordinateSystem } from 'three/webgpu';
|
|
2
2
|
import { uv, uniform, varying } from 'three/tsl';
|
|
3
3
|
import { SkinnedMeshBVH, MeshBVH, SAH } from 'three-mesh-bvh';
|
|
4
4
|
import { ndcToCameraRay, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
@@ -11,11 +11,11 @@ import { CubeToEquirectGenerator } from '../utils/CubeToEquirectGenerator.js';
|
|
|
11
11
|
import { PathtracerBVHComputeData } from './nodes/PathtracerBVHComputeData.js';
|
|
12
12
|
import { AtlasDebugMaterial } from './materials/debug/AtlasDebugMaterial.js';
|
|
13
13
|
import { SampleDensityMaterial } from './materials/debug/SampleDensityMaterial.js';
|
|
14
|
+
import { AuxDebugMaterial } from './materials/debug/AuxDebugMaterial.js';
|
|
14
15
|
import { setCommonAttributes } from '../core/utils/GeometryPreparationUtils.js';
|
|
15
16
|
import { getLights } from '../core/utils/sceneUpdateUtils.js';
|
|
16
17
|
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';
|
|
18
|
+
import { TRANSMISSIVE_BACKGROUND_OVERLAY, RANDOM_SOBOL } from './constants.js';
|
|
19
19
|
/** @import { Camera, Scene, Texture, WebGPURenderer } from 'three/webgpu' */
|
|
20
20
|
/** @import { AtlasTexture } from './AtlasTexture.js' */
|
|
21
21
|
/** @import { OIDNDenoiser } from './denoise/OIDNDenoiser.js' */
|
|
@@ -33,6 +33,71 @@ import * as RANDOM_BLUE_DITHER from './nodes/rand/bluedither.wgsl.js';
|
|
|
33
33
|
const _resolution = new Vector2();
|
|
34
34
|
const _color = new Color();
|
|
35
35
|
|
|
36
|
+
// milliseconds between sample count readbacks while waiting for the render to complete
|
|
37
|
+
const COMPLETE_CHECK_INTERVAL = 250;
|
|
38
|
+
|
|
39
|
+
// How the adaptive frame budget follows the measured frame time. The budget is only reconsidered once
|
|
40
|
+
// a window of measurements has been taken at it, and only changes when their average is off the target
|
|
41
|
+
// by the threshold ratio, since a wavefront budget change reallocates the path buffers. It moves at most
|
|
42
|
+
// the step ratio per window.
|
|
43
|
+
// TODO: adapt more often, with a window of around 4, once the wavefront path buffers are grow-only so a budget
|
|
44
|
+
// change only changes the active slot count rather than reallocating them
|
|
45
|
+
const BUDGET_WINDOW = 16;
|
|
46
|
+
const BUDGET_CHANGE_THRESHOLD = 1.15;
|
|
47
|
+
const BUDGET_MAX_STEP = 2;
|
|
48
|
+
const MIN_FRAME_BUDGET = 4096;
|
|
49
|
+
|
|
50
|
+
// the options the mesh geometry bvhs are built with and how many workers build them at once
|
|
51
|
+
const MESH_BVH_OPTIONS = { strategy: SAH, targetLeafSize: 5 };
|
|
52
|
+
const MAX_BVH_WORKERS = 4;
|
|
53
|
+
|
|
54
|
+
// the stages before a scene renders, in order
|
|
55
|
+
const STAGE_BUILDING = 0;
|
|
56
|
+
const STAGE_COMPILING = 1;
|
|
57
|
+
const STAGE_RENDERING = 2;
|
|
58
|
+
|
|
59
|
+
// Builds a geometry's BVH in a worker without giving up its buffers. The workers transfer the position and
|
|
60
|
+
// index buffers, which neuters them for every other array viewing them, such as the attributes of other
|
|
61
|
+
// meshes in a glTF. So the worker gets copies, the original arrays are restored, and the reordered index
|
|
62
|
+
// is written back into the original. Shared array buffers aren't transferred, so they're sent as is.
|
|
63
|
+
// TODO: this is imperfect, and depends on how the worker transfers. Remove once three-mesh-bvh's worker
|
|
64
|
+
// has an option not to transfer
|
|
65
|
+
async function generateBVHInWorker( worker, geometry, options ) {
|
|
66
|
+
|
|
67
|
+
const getCopy = array => {
|
|
68
|
+
|
|
69
|
+
const isShared = typeof SharedArrayBuffer !== 'undefined' && array.buffer instanceof SharedArrayBuffer;
|
|
70
|
+
return isShared ? array : array.slice();
|
|
71
|
+
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const { position } = geometry.attributes;
|
|
75
|
+
const { index } = geometry;
|
|
76
|
+
const positionArray = position.array;
|
|
77
|
+
const indexArray = index ? index.array : null;
|
|
78
|
+
|
|
79
|
+
position.array = getCopy( positionArray );
|
|
80
|
+
if ( index ) {
|
|
81
|
+
|
|
82
|
+
index.array = getCopy( indexArray );
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bvh = await worker.generate( geometry, options );
|
|
87
|
+
|
|
88
|
+
position.array = positionArray;
|
|
89
|
+
if ( index ) {
|
|
90
|
+
|
|
91
|
+
indexArray.set( index.array );
|
|
92
|
+
index.array = indexArray;
|
|
93
|
+
index.needsUpdate = true;
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return bvh;
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
36
101
|
class TextureCache {
|
|
37
102
|
|
|
38
103
|
constructor( renderer, key ) {
|
|
@@ -94,8 +159,18 @@ class TextureCache {
|
|
|
94
159
|
|
|
95
160
|
} else {
|
|
96
161
|
|
|
97
|
-
|
|
162
|
+
// hash the scene's texture rather than the clone, whose uuid is new on every call, so an
|
|
163
|
+
// unchanged texture isn't cloned and re-processed each time the environment is updated
|
|
98
164
|
hash = value.uuid + '_' + value.version;
|
|
165
|
+
if ( hash !== this.hash ) {
|
|
166
|
+
|
|
167
|
+
// a nearest filtered texture gets no sampler from TSL, which the equirect sampling
|
|
168
|
+
// needs, so the copy is always filtered
|
|
169
|
+
value = value.clone();
|
|
170
|
+
value.minFilter = LinearFilter;
|
|
171
|
+
value.magFilter = LinearFilter;
|
|
172
|
+
|
|
173
|
+
}
|
|
99
174
|
|
|
100
175
|
}
|
|
101
176
|
|
|
@@ -126,6 +201,21 @@ class TextureCache {
|
|
|
126
201
|
|
|
127
202
|
}
|
|
128
203
|
|
|
204
|
+
// WebGPUPathTracer Event Definitions
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fired once every pixel has reached `maxSamples` and an attached denoiser has finished. Fires
|
|
208
|
+
* again after a reset, or after `maxSamples` is raised, once the render has caught up.
|
|
209
|
+
* @event WebGPUPathTracer#complete
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fired as the render advances. `stage` is "building", "compiling", "rendering" or "denoising", and
|
|
214
|
+
* `progress` is the fraction of it done, or null when unknown. "rendering" also gives the sample
|
|
215
|
+
* counts as `samples`, reported a few times a second.
|
|
216
|
+
* @event WebGPUPathTracer#progress
|
|
217
|
+
*/
|
|
218
|
+
|
|
129
219
|
/**
|
|
130
220
|
* Progressive path tracer running on WebGPU. Call {@link WebGPUPathTracer#setScene} once,
|
|
131
221
|
* then {@link WebGPUPathTracer#renderSample} every frame to accumulate samples into the canvas.
|
|
@@ -134,7 +224,7 @@ class TextureCache {
|
|
|
134
224
|
* the environment afterward require the matching `update*` function. Any change that invalidates
|
|
135
225
|
* the accumulated image restarts it.
|
|
136
226
|
*/
|
|
137
|
-
export class WebGPUPathTracer {
|
|
227
|
+
export class WebGPUPathTracer extends EventDispatcher {
|
|
138
228
|
|
|
139
229
|
/**
|
|
140
230
|
* Maximum number of times a ray can scatter before the path is terminated. Higher values
|
|
@@ -151,12 +241,12 @@ export class WebGPUPathTracer {
|
|
|
151
241
|
set maxBounces( v ) {
|
|
152
242
|
|
|
153
243
|
this._pathTracer.maxBounces = v;
|
|
154
|
-
this.
|
|
244
|
+
this.reset();
|
|
155
245
|
|
|
156
246
|
}
|
|
157
247
|
|
|
158
248
|
/**
|
|
159
|
-
* Number of path
|
|
249
|
+
* Number of path steps dispatched per {@link WebGPUPathTracer#renderSample} call,
|
|
160
250
|
* independent of resolution. Raising it trades frame rate for convergence speed.
|
|
161
251
|
* @type {number}
|
|
162
252
|
* @default 250000
|
|
@@ -173,11 +263,23 @@ export class WebGPUPathTracer {
|
|
|
173
263
|
|
|
174
264
|
}
|
|
175
265
|
|
|
266
|
+
/**
|
|
267
|
+
* GPU time in milliseconds of a recent {@link WebGPUPathTracer#renderSample} call. Only measured
|
|
268
|
+
* when the renderer is created with `trackTimestamp: true`.
|
|
269
|
+
* @type {number}
|
|
270
|
+
* @readonly
|
|
271
|
+
*/
|
|
272
|
+
get frameTime() {
|
|
273
|
+
|
|
274
|
+
return this._pathTracer.timing.frameTime;
|
|
275
|
+
|
|
276
|
+
}
|
|
277
|
+
|
|
176
278
|
/**
|
|
177
279
|
* Maximum number of alpha tested surfaces a ray can pass through. Counted separately from
|
|
178
280
|
* {@link WebGPUPathTracer#maxBounces} so foliage and cutouts cannot exhaust the bounce budget.
|
|
179
281
|
* @type {number}
|
|
180
|
-
* @default
|
|
282
|
+
* @default 15
|
|
181
283
|
*/
|
|
182
284
|
get maxTransparentBounces() {
|
|
183
285
|
|
|
@@ -188,7 +290,26 @@ export class WebGPUPathTracer {
|
|
|
188
290
|
set maxTransparentBounces( v ) {
|
|
189
291
|
|
|
190
292
|
this._pathTracer.maxTransparentBounces = v;
|
|
191
|
-
this.
|
|
293
|
+
this.reset();
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Maximum number of scatter events a path can take inside a subsurface scattering volume. Counted
|
|
299
|
+
* separately from {@link WebGPUPathTracer#maxBounces}. Paths that reach it end, darkening dense volumes.
|
|
300
|
+
* @type {number}
|
|
301
|
+
* @default 256
|
|
302
|
+
*/
|
|
303
|
+
get maxSubsurfaceBounces() {
|
|
304
|
+
|
|
305
|
+
return this._pathTracer.maxSubsurfaceBounces;
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
set maxSubsurfaceBounces( v ) {
|
|
310
|
+
|
|
311
|
+
this._pathTracer.maxSubsurfaceBounces = v;
|
|
312
|
+
this.reset();
|
|
192
313
|
|
|
193
314
|
}
|
|
194
315
|
|
|
@@ -215,6 +336,21 @@ export class WebGPUPathTracer {
|
|
|
215
336
|
|
|
216
337
|
}
|
|
217
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Whether every pixel has reached {@link WebGPUPathTracer#maxSamples} and an attached denoiser
|
|
341
|
+
* has finished, as of the last measurement. Always false while rendering indefinitely.
|
|
342
|
+
* @type {boolean}
|
|
343
|
+
*/
|
|
344
|
+
get complete() {
|
|
345
|
+
|
|
346
|
+
const maxSamples = this.maxSamples;
|
|
347
|
+
const denoiser = this._denoiser;
|
|
348
|
+
|
|
349
|
+
// "min" so every pixel has stopped, not just the average
|
|
350
|
+
return maxSamples > 0 && this._lastSampleCounts.min >= maxSamples && ( ! denoiser || denoiser.complete );
|
|
351
|
+
|
|
352
|
+
}
|
|
353
|
+
|
|
218
354
|
/**
|
|
219
355
|
* Measures the per pixel sample counts. Use `min` for convergence checks and `avg` to display.
|
|
220
356
|
* @returns {Promise<SampleCounts>}
|
|
@@ -404,6 +540,19 @@ export class WebGPUPathTracer {
|
|
|
404
540
|
|
|
405
541
|
}
|
|
406
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Whether the kernels are compiling, such as after the scene or material changes. Nothing new
|
|
545
|
+
* renders until they finish and the canvas keeps the last frame, so an app may draw its own
|
|
546
|
+
* preview in the meantime.
|
|
547
|
+
* @type {boolean}
|
|
548
|
+
* @readonly
|
|
549
|
+
*/
|
|
550
|
+
get compiling() {
|
|
551
|
+
|
|
552
|
+
return this._pathTracer.compiling;
|
|
553
|
+
|
|
554
|
+
}
|
|
555
|
+
|
|
407
556
|
/**
|
|
408
557
|
* The atlas every scene texture is packed into for the kernels to sample.
|
|
409
558
|
* @type {AtlasTexture}
|
|
@@ -429,6 +578,7 @@ export class WebGPUPathTracer {
|
|
|
429
578
|
this._pathTracer.dispose();
|
|
430
579
|
this._pathTracer = value ? new MegaKernelPathTracer( this._renderer ) : new WaveFrontPathTracer( this._renderer );
|
|
431
580
|
this._pathTracer.maxSamples = maxSamples;
|
|
581
|
+
this._timingCount = 0;
|
|
432
582
|
this._pathTracer.setBVHData( this._bvhData );
|
|
433
583
|
this._pathTracer.setMaterial( this.material );
|
|
434
584
|
this._pathTracer.setRandom( this.random );
|
|
@@ -436,6 +586,7 @@ export class WebGPUPathTracer {
|
|
|
436
586
|
this._pathTracer.setTransmissiveBackground( this._transmissiveBackground );
|
|
437
587
|
this._pathTracer.setFilterGlossy( this._filterGlossyFactor );
|
|
438
588
|
this._pathTracer.setClamping( this._clampDirect, this._clampIndirect );
|
|
589
|
+
this._pathTracer.setAccumulateAux( Boolean( this._denoiser && this._denoiser.useAuxiliaryBuffers ) );
|
|
439
590
|
this.setCamera( this.camera );
|
|
440
591
|
|
|
441
592
|
// the new tracer has default environment and background maps so apply the cached scene textures
|
|
@@ -451,6 +602,8 @@ export class WebGPUPathTracer {
|
|
|
451
602
|
*/
|
|
452
603
|
constructor( renderer ) {
|
|
453
604
|
|
|
605
|
+
super();
|
|
606
|
+
|
|
454
607
|
// members
|
|
455
608
|
this._renderer = renderer;
|
|
456
609
|
this._timer = new Timer();
|
|
@@ -464,6 +617,16 @@ export class WebGPUPathTracer {
|
|
|
464
617
|
this._size = new Vector2();
|
|
465
618
|
this._blitQuad = new FullScreenQuad( new RenderToScreenNodeMaterial() );
|
|
466
619
|
|
|
620
|
+
// adaptive frame budget and preview state
|
|
621
|
+
this._timingCount = 0;
|
|
622
|
+
this._frameTimeTotal = 0;
|
|
623
|
+
this._frameTimeCount = 0;
|
|
624
|
+
this._warnedTimestamps = false;
|
|
625
|
+
|
|
626
|
+
// the budget from before the first pixels finished, restored on reset. The last few pixels render
|
|
627
|
+
// quickly and inflate the budget
|
|
628
|
+
this._fullFrameBudget = null;
|
|
629
|
+
|
|
467
630
|
// avoid mipmap gen on copy, not always supported with float type
|
|
468
631
|
this._lowResTarget = new StorageTexture( 1, 1 );
|
|
469
632
|
this._lowResTarget.type = FloatType;
|
|
@@ -475,10 +638,19 @@ export class WebGPUPathTracer {
|
|
|
475
638
|
this._denoiser = null;
|
|
476
639
|
this._upscaler = null;
|
|
477
640
|
|
|
641
|
+
// the attached denoiser's progress events are forwarded
|
|
642
|
+
this._onDenoiserProgress = e => this.dispatchEvent( { type: 'progress', stage: 'denoising', progress: e.progress } );
|
|
643
|
+
|
|
644
|
+
// the current stage. Only the latest "setSceneAsync" call sets its scene, and a geometry already building
|
|
645
|
+
// for an earlier call is awaited rather than built again
|
|
646
|
+
this._stage = STAGE_RENDERING;
|
|
647
|
+
this._sceneRequest = 0;
|
|
648
|
+
this._pendingBVHs = new WeakMap();
|
|
649
|
+
|
|
478
650
|
// options
|
|
479
651
|
|
|
480
652
|
/**
|
|
481
|
-
*
|
|
653
|
+
* Average samples per pixel the full resolution render must reach before it is faded in.
|
|
482
654
|
* @type {number}
|
|
483
655
|
* @default 1
|
|
484
656
|
*/
|
|
@@ -487,16 +659,16 @@ export class WebGPUPathTracer {
|
|
|
487
659
|
/**
|
|
488
660
|
* Milliseconds to show the low resolution preview for after a reset.
|
|
489
661
|
* @type {number}
|
|
490
|
-
* @default
|
|
662
|
+
* @default 16
|
|
491
663
|
*/
|
|
492
|
-
this.renderDelay =
|
|
664
|
+
this.renderDelay = 16;
|
|
493
665
|
|
|
494
666
|
/**
|
|
495
667
|
* Milliseconds taken to cross fade from the preview to the full render.
|
|
496
668
|
* @type {number}
|
|
497
|
-
* @default
|
|
669
|
+
* @default 250
|
|
498
670
|
*/
|
|
499
|
-
this.fadeDuration =
|
|
671
|
+
this.fadeDuration = 250;
|
|
500
672
|
|
|
501
673
|
/**
|
|
502
674
|
* Whether to render a low resolution preview while the camera is moving.
|
|
@@ -506,12 +678,38 @@ export class WebGPUPathTracer {
|
|
|
506
678
|
this.dynamicLowRes = true;
|
|
507
679
|
|
|
508
680
|
/**
|
|
509
|
-
* Resolution of the low resolution preview, as a fraction of the render size.
|
|
681
|
+
* Resolution of the low resolution preview, as a fraction of the render size. Adjusted when
|
|
682
|
+
* {@link WebGPUPathTracer#adaptiveDynamicRes} is set. The preview never has more pixels than a
|
|
683
|
+
* frame of {@link WebGPUPathTracer#frameBudget} can render.
|
|
510
684
|
* @type {number}
|
|
511
685
|
* @default 0.1
|
|
512
686
|
*/
|
|
513
687
|
this.lowResScale = 0.1;
|
|
514
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Whether {@link WebGPUPathTracer#lowResScale} is adjusted to the pixels a frame of
|
|
691
|
+
* {@link WebGPUPathTracer#frameBudget} can render.
|
|
692
|
+
* @type {boolean}
|
|
693
|
+
* @default true
|
|
694
|
+
*/
|
|
695
|
+
this.adaptiveDynamicRes = true;
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Whether {@link WebGPUPathTracer#frameBudget} is adjusted to keep each
|
|
699
|
+
* {@link WebGPUPathTracer#renderSample} call near {@link WebGPUPathTracer#targetFrameTime} of
|
|
700
|
+
* GPU time. Requires a renderer created with `trackTimestamp: true`.
|
|
701
|
+
* @type {boolean}
|
|
702
|
+
* @default true
|
|
703
|
+
*/
|
|
704
|
+
this.adaptiveFrameBudget = true;
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* GPU time in milliseconds per frame targeted by {@link WebGPUPathTracer#adaptiveFrameBudget}.
|
|
708
|
+
* @type {number}
|
|
709
|
+
* @default 16
|
|
710
|
+
*/
|
|
711
|
+
this.targetFrameTime = 16;
|
|
712
|
+
|
|
515
713
|
/**
|
|
516
714
|
* Resolution the path tracer renders at, as a fraction of the canvas size. Lowering it
|
|
517
715
|
* converges faster at the cost of detail, and pairs with an upscaler.
|
|
@@ -528,6 +726,23 @@ export class WebGPUPathTracer {
|
|
|
528
726
|
*/
|
|
529
727
|
this.synchronizeRenderSize = true;
|
|
530
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Whether {@link WebGPUPathTracer#renderSample} draws the result to the current render
|
|
731
|
+
* target, the canvas by default. Set to `false` to only accumulate, as when baking to a
|
|
732
|
+
* texture read through {@link WebGPUPathTracer#target}.
|
|
733
|
+
* @type {boolean}
|
|
734
|
+
* @default true
|
|
735
|
+
*/
|
|
736
|
+
this.renderToCanvas = true;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Whether to compile the kernels asynchronously so a compile doesn't stall the page. Nothing
|
|
740
|
+
* new renders while {@link WebGPUPathTracer#compiling} is true.
|
|
741
|
+
* @type {boolean}
|
|
742
|
+
* @default false
|
|
743
|
+
*/
|
|
744
|
+
this.asyncCompile = false;
|
|
745
|
+
|
|
531
746
|
/**
|
|
532
747
|
* Whether to generate the attributes in {@link WebGPUPathTracer#commonAttributes} on
|
|
533
748
|
* geometry that is missing them when the scene is set.
|
|
@@ -569,7 +784,7 @@ export class WebGPUPathTracer {
|
|
|
569
784
|
* @type {Object}
|
|
570
785
|
* @private
|
|
571
786
|
*/
|
|
572
|
-
this.random =
|
|
787
|
+
this.random = RANDOM_SOBOL;
|
|
573
788
|
|
|
574
789
|
/**
|
|
575
790
|
* Material model the kernels evaluate surfaces with.
|
|
@@ -583,6 +798,11 @@ export class WebGPUPathTracer {
|
|
|
583
798
|
// ( world * inverseProjection ), premultiplied on the CPU so no matrix multiply runs per ray.
|
|
584
799
|
this._invViewProjectionMatrix = uniform( new Matrix4() );
|
|
585
800
|
this._cameraRayFnHandle = null;
|
|
801
|
+
this._BVHWorker = null;
|
|
802
|
+
|
|
803
|
+
// the render time of the last check for the render having stopped
|
|
804
|
+
this._lastCompleteCheck = 0;
|
|
805
|
+
this._wasComplete = false;
|
|
586
806
|
|
|
587
807
|
// initialize the scene so it doesn't fail
|
|
588
808
|
this.setMaterial( this.material );
|
|
@@ -604,7 +824,8 @@ export class WebGPUPathTracer {
|
|
|
604
824
|
|
|
605
825
|
/**
|
|
606
826
|
* 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.
|
|
827
|
+
* Settings live on the instance. Pass null to remove it. The render restarts when the auxiliary
|
|
828
|
+
* buffers it needs are added or dropped.
|
|
608
829
|
*
|
|
609
830
|
* @param {OIDNDenoiser|null} denoiser
|
|
610
831
|
*/
|
|
@@ -613,15 +834,19 @@ export class WebGPUPathTracer {
|
|
|
613
834
|
// a detached instance stops receiving resets, so clear it rather than let it hold a
|
|
614
835
|
// result from a camera position that has since moved
|
|
615
836
|
this._denoiser?.reset();
|
|
837
|
+
this._denoiser?.removeEventListener( 'progress', this._onDenoiserProgress );
|
|
616
838
|
this._denoiser = denoiser;
|
|
617
839
|
|
|
618
840
|
if ( denoiser ) {
|
|
619
841
|
|
|
620
842
|
denoiser.init( this._renderer );
|
|
621
|
-
denoiser.
|
|
843
|
+
denoiser.addEventListener( 'progress', this._onDenoiserProgress );
|
|
622
844
|
|
|
623
845
|
}
|
|
624
846
|
|
|
847
|
+
this._pathTracer.setAccumulateAux( Boolean( denoiser && denoiser.useAuxiliaryBuffers ) );
|
|
848
|
+
this.reset();
|
|
849
|
+
|
|
625
850
|
}
|
|
626
851
|
|
|
627
852
|
/**
|
|
@@ -688,7 +913,7 @@ export class WebGPUPathTracer {
|
|
|
688
913
|
|
|
689
914
|
if ( ! child.geometry.boundsTree ) {
|
|
690
915
|
|
|
691
|
-
child.geometry.boundsTree = new MeshBVH( child.geometry,
|
|
916
|
+
child.geometry.boundsTree = new MeshBVH( child.geometry, MESH_BVH_OPTIONS );
|
|
692
917
|
|
|
693
918
|
}
|
|
694
919
|
|
|
@@ -715,8 +940,150 @@ export class WebGPUPathTracer {
|
|
|
715
940
|
this.updateEnvironment();
|
|
716
941
|
this.updateLights();
|
|
717
942
|
|
|
718
|
-
|
|
719
|
-
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Sets the worker class {@link WebGPUPathTracer#setSceneAsync} builds the mesh geometry BVHs
|
|
947
|
+
* with, such as `GenerateMeshBVHWorker` from three-mesh-bvh. Several are created and disposed
|
|
948
|
+
* per call so geometries build in parallel.
|
|
949
|
+
*
|
|
950
|
+
* @param {Function} WorkerConstructor
|
|
951
|
+
*/
|
|
952
|
+
setBVHWorker( WorkerConstructor ) {
|
|
953
|
+
|
|
954
|
+
this._BVHWorker = WorkerConstructor;
|
|
955
|
+
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Like {@link WebGPUPathTracer#setScene} but builds the mesh geometry BVHs in the workers set
|
|
960
|
+
* with {@link WebGPUPathTracer#setBVHWorker} so the page is not blocked. Skinned meshes are
|
|
961
|
+
* still built synchronously.
|
|
962
|
+
*
|
|
963
|
+
* @param {Scene} scene
|
|
964
|
+
* @param {Camera} camera
|
|
965
|
+
* @returns {Promise<void>}
|
|
966
|
+
*/
|
|
967
|
+
async setSceneAsync( scene, camera ) {
|
|
968
|
+
|
|
969
|
+
const WorkerConstructor = this._BVHWorker;
|
|
970
|
+
if ( ! WorkerConstructor ) {
|
|
971
|
+
|
|
972
|
+
throw new Error( 'WebGPUPathTracer: "setBVHWorker" must be called before "setSceneAsync" can be called.' );
|
|
973
|
+
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
this._sceneRequest ++;
|
|
977
|
+
|
|
978
|
+
const request = this._sceneRequest;
|
|
979
|
+
const pendingBVHs = this._pendingBVHs;
|
|
980
|
+
|
|
981
|
+
// the worker doesn't support interleaved attributes, so those geometries are built in "setScene" instead
|
|
982
|
+
const geometries = new Set();
|
|
983
|
+
const pending = new Set();
|
|
984
|
+
scene.traverse( child => {
|
|
985
|
+
|
|
986
|
+
if ( child.isMesh && ! child.isSkinnedMesh && ! child.geometry.boundsTree ) {
|
|
987
|
+
|
|
988
|
+
const { geometry } = child;
|
|
989
|
+
if ( geometry.attributes.position.isInterleavedBufferAttribute || geometry.index && geometry.index.isInterleavedBufferAttribute ) {
|
|
990
|
+
|
|
991
|
+
return;
|
|
992
|
+
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if ( pendingBVHs.has( geometry ) ) {
|
|
996
|
+
|
|
997
|
+
pending.add( pendingBVHs.get( geometry ) );
|
|
998
|
+
|
|
999
|
+
} else {
|
|
1000
|
+
|
|
1001
|
+
geometries.add( geometry );
|
|
1002
|
+
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
} );
|
|
1008
|
+
|
|
1009
|
+
// each worker runs one job at a time so they pull from a shared queue
|
|
1010
|
+
const queue = Array.from( geometries );
|
|
1011
|
+
|
|
1012
|
+
// build progress, weighted by triangle count
|
|
1013
|
+
const getTriangleCount = geometry => ( geometry.index ? geometry.index.count : geometry.attributes.position.count ) / 3;
|
|
1014
|
+
const totalTriangles = queue.reduce( ( sum, geometry ) => sum + getTriangleCount( geometry ), 0 );
|
|
1015
|
+
let builtTriangles = 0;
|
|
1016
|
+
|
|
1017
|
+
const workers = [];
|
|
1018
|
+
for ( let i = 0, l = Math.min( MAX_BVH_WORKERS, queue.length ); i < l; i ++ ) {
|
|
1019
|
+
|
|
1020
|
+
workers.push( new WorkerConstructor() );
|
|
1021
|
+
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// stop rendering the previous scene while the next one builds, keeping the last frame. The denoiser runs
|
|
1025
|
+
// its tiles on its own schedule, so it's reset separately
|
|
1026
|
+
this._setStage( STAGE_BUILDING );
|
|
1027
|
+
this._denoiser?.reset();
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
|
|
1031
|
+
await Promise.all( [ ...pending, ...workers.map( async worker => {
|
|
1032
|
+
|
|
1033
|
+
while ( queue.length > 0 ) {
|
|
1034
|
+
|
|
1035
|
+
const geometry = queue.shift();
|
|
1036
|
+
const triangles = getTriangleCount( geometry );
|
|
1037
|
+
let reported = 0;
|
|
1038
|
+
const onProgress = progress => {
|
|
1039
|
+
|
|
1040
|
+
builtTriangles += ( progress - reported ) * triangles;
|
|
1041
|
+
reported = progress;
|
|
1042
|
+
|
|
1043
|
+
// skip if a later call replaced this scene
|
|
1044
|
+
if ( request === this._sceneRequest ) {
|
|
1045
|
+
|
|
1046
|
+
this.dispatchEvent( { type: 'progress', stage: 'building', progress: builtTriangles / totalTriangles } );
|
|
1047
|
+
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const promise = generateBVHInWorker( worker, geometry, { ...MESH_BVH_OPTIONS, onProgress } );
|
|
1053
|
+
pendingBVHs.set( geometry, promise );
|
|
1054
|
+
|
|
1055
|
+
try {
|
|
1056
|
+
|
|
1057
|
+
geometry.boundsTree = await promise;
|
|
1058
|
+
onProgress( 1 );
|
|
1059
|
+
|
|
1060
|
+
} finally {
|
|
1061
|
+
|
|
1062
|
+
pendingBVHs.delete( geometry );
|
|
1063
|
+
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
} ) ] );
|
|
1069
|
+
|
|
1070
|
+
} finally {
|
|
1071
|
+
|
|
1072
|
+
workers.forEach( worker => worker.dispose() );
|
|
1073
|
+
if ( request === this._sceneRequest ) {
|
|
1074
|
+
|
|
1075
|
+
this._setStage( STAGE_COMPILING );
|
|
1076
|
+
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// skip the scene if a later call replaced it while it built
|
|
1082
|
+
if ( request === this._sceneRequest ) {
|
|
1083
|
+
|
|
1084
|
+
this.setScene( scene, camera );
|
|
1085
|
+
|
|
1086
|
+
}
|
|
720
1087
|
|
|
721
1088
|
}
|
|
722
1089
|
|
|
@@ -942,6 +1309,8 @@ export class WebGPUPathTracer {
|
|
|
942
1309
|
|
|
943
1310
|
// a fresh object rather than a clear, so measurements still in flight detach
|
|
944
1311
|
this._lastSampleCounts = { min: 0, max: 0, avg: 0, samplesPerSecond: 0 };
|
|
1312
|
+
this._lastCompleteCheck = 0;
|
|
1313
|
+
this._wasComplete = false;
|
|
945
1314
|
|
|
946
1315
|
if ( this.stableNoise ) {
|
|
947
1316
|
|
|
@@ -949,6 +1318,130 @@ export class WebGPUPathTracer {
|
|
|
949
1318
|
|
|
950
1319
|
}
|
|
951
1320
|
|
|
1321
|
+
if ( this._fullFrameBudget !== null ) {
|
|
1322
|
+
|
|
1323
|
+
this.frameBudget = this._fullFrameBudget;
|
|
1324
|
+
this._fullFrameBudget = null;
|
|
1325
|
+
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Sets the stage, reporting when each one starts
|
|
1331
|
+
_setStage( stage ) {
|
|
1332
|
+
|
|
1333
|
+
if ( this._stage === stage ) {
|
|
1334
|
+
|
|
1335
|
+
return;
|
|
1336
|
+
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
this._stage = stage;
|
|
1340
|
+
if ( stage === STAGE_BUILDING ) {
|
|
1341
|
+
|
|
1342
|
+
this.dispatchEvent( { type: 'progress', stage: 'building', progress: 0 } );
|
|
1343
|
+
|
|
1344
|
+
} else if ( stage === STAGE_COMPILING ) {
|
|
1345
|
+
|
|
1346
|
+
this.dispatchEvent( { type: 'progress', stage: 'compiling', progress: null } );
|
|
1347
|
+
|
|
1348
|
+
} else {
|
|
1349
|
+
|
|
1350
|
+
this._dispatchRenderProgress( { min: 0, max: 0, avg: 0, samplesPerSecond: 0 } );
|
|
1351
|
+
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Measures the sample counts and reports them
|
|
1357
|
+
_measureSamples() {
|
|
1358
|
+
|
|
1359
|
+
const counts = this._lastSampleCounts;
|
|
1360
|
+
this.getSampleCountsAsync().then( samples => {
|
|
1361
|
+
|
|
1362
|
+
// skip if the render was reset or a new scene started building meanwhile
|
|
1363
|
+
if ( counts === this._lastSampleCounts && this._stage === STAGE_RENDERING ) {
|
|
1364
|
+
|
|
1365
|
+
// save the budget once the first pixels finish
|
|
1366
|
+
const { maxSamples } = this;
|
|
1367
|
+
if ( this._fullFrameBudget === null && maxSamples > 0 && samples.max >= maxSamples ) {
|
|
1368
|
+
|
|
1369
|
+
this._fullFrameBudget = this.frameBudget;
|
|
1370
|
+
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
this._dispatchRenderProgress( samples );
|
|
1374
|
+
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
} );
|
|
1378
|
+
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
_dispatchRenderProgress( samples ) {
|
|
1382
|
+
|
|
1383
|
+
const { maxSamples } = this;
|
|
1384
|
+
const progress = maxSamples > 0 ? Math.min( samples.avg / maxSamples, 1 ) : null;
|
|
1385
|
+
this.dispatchEvent( { type: 'progress', stage: 'rendering', progress, samples } );
|
|
1386
|
+
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Adapts the frame budget to the tracer's latest gpu time measurement
|
|
1390
|
+
_readTiming() {
|
|
1391
|
+
|
|
1392
|
+
const { timing } = this._pathTracer;
|
|
1393
|
+
const { trackTimestamp } = this._renderer.backend;
|
|
1394
|
+
if ( this.adaptiveFrameBudget && ! trackTimestamp && ! this._warnedTimestamps ) {
|
|
1395
|
+
|
|
1396
|
+
console.warn( 'WebGPUPathTracer: "adaptiveFrameBudget" needs timestamp queries. Create the renderer with "trackTimestamp: true" on a device that supports them.' );
|
|
1397
|
+
this._warnedTimestamps = true;
|
|
1398
|
+
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if ( timing.count === this._timingCount ) {
|
|
1402
|
+
|
|
1403
|
+
return;
|
|
1404
|
+
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
this._timingCount = timing.count;
|
|
1408
|
+
|
|
1409
|
+
// a measurement from before the budget changed doesn't describe the current one
|
|
1410
|
+
const { frameBudget } = timing;
|
|
1411
|
+
if ( ! this.adaptiveFrameBudget || frameBudget !== this.frameBudget ) {
|
|
1412
|
+
|
|
1413
|
+
this._frameTimeTotal = 0;
|
|
1414
|
+
this._frameTimeCount = 0;
|
|
1415
|
+
return;
|
|
1416
|
+
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
this._frameTimeTotal += timing.frameTime;
|
|
1420
|
+
this._frameTimeCount ++;
|
|
1421
|
+
if ( this._frameTimeCount < BUDGET_WINDOW ) {
|
|
1422
|
+
|
|
1423
|
+
return;
|
|
1424
|
+
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const { targetFrameTime } = this;
|
|
1428
|
+
const average = this._frameTimeTotal / this._frameTimeCount;
|
|
1429
|
+
this._frameTimeTotal = 0;
|
|
1430
|
+
this._frameTimeCount = 0;
|
|
1431
|
+
|
|
1432
|
+
// leave the budget alone while it's close enough to the target
|
|
1433
|
+
const ratio = average / targetFrameTime;
|
|
1434
|
+
if ( ratio < BUDGET_CHANGE_THRESHOLD && ratio > 1 / BUDGET_CHANGE_THRESHOLD ) {
|
|
1435
|
+
|
|
1436
|
+
return;
|
|
1437
|
+
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// scale the budget to land the average on the target
|
|
1441
|
+
const next = frameBudget * targetFrameTime / average;
|
|
1442
|
+
const stepped = MathUtils.clamp( next, frameBudget / BUDGET_MAX_STEP, frameBudget * BUDGET_MAX_STEP );
|
|
1443
|
+
this.frameBudget = Math.max( Math.round( stepped ), MIN_FRAME_BUDGET );
|
|
1444
|
+
|
|
952
1445
|
}
|
|
953
1446
|
|
|
954
1447
|
/**
|
|
@@ -967,7 +1460,7 @@ export class WebGPUPathTracer {
|
|
|
967
1460
|
dynamicLowRes,
|
|
968
1461
|
synchronizeRenderSize,
|
|
969
1462
|
renderScale,
|
|
970
|
-
|
|
1463
|
+
adaptiveDynamicRes,
|
|
971
1464
|
minSamples,
|
|
972
1465
|
maxSamples,
|
|
973
1466
|
} = this;
|
|
@@ -1016,35 +1509,61 @@ export class WebGPUPathTracer {
|
|
|
1016
1509
|
const lowResMode = this._resetTime < renderDelay || ( firstFrame && dynamicLowRes && minSamples !== 0 );
|
|
1017
1510
|
if ( lowResMode ) {
|
|
1018
1511
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1512
|
+
// the most pixels a preview frame can render within the frame budget. Pixels past it are never
|
|
1513
|
+
// reached by the wavefront in a preview
|
|
1514
|
+
const budgetScale = Math.min( Math.sqrt( pathTracer.getPreviewPixelBudget() / ( width * height ) ), 1 );
|
|
1515
|
+
|
|
1516
|
+
if ( adaptiveDynamicRes ) {
|
|
1517
|
+
|
|
1518
|
+
this.lowResScale = budgetScale;
|
|
1519
|
+
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// checked every frame since the budget adapts separately
|
|
1523
|
+
const scale = Math.min( this.lowResScale, budgetScale );
|
|
1524
|
+
width = Math.max( 1, Math.floor( scale * width ) );
|
|
1525
|
+
height = Math.max( 1, Math.floor( scale * height ) );
|
|
1021
1526
|
|
|
1022
1527
|
}
|
|
1023
1528
|
|
|
1024
1529
|
// set the size if necessary
|
|
1025
1530
|
pathTracer.getSize( _resolution );
|
|
1026
1531
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1532
|
+
// copy the low res content when transitioning to the full resolution view so we can fade from
|
|
1533
|
+
// it, including when the preview was already full size and nothing is resized
|
|
1534
|
+
if ( ! lowResMode && pathTracer.lowResMode && dynamicLowRes ) {
|
|
1029
1535
|
|
|
1030
|
-
|
|
1536
|
+
lowResTarget.setSize( _resolution.x, _resolution.y );
|
|
1537
|
+
renderer.copyTextureToTexture( pathTracer.outputTarget, lowResTarget );
|
|
1031
1538
|
|
|
1032
|
-
|
|
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 );
|
|
1539
|
+
}
|
|
1036
1540
|
|
|
1037
|
-
|
|
1541
|
+
if ( _resolution.x !== width || _resolution.y !== height ) {
|
|
1038
1542
|
|
|
1039
1543
|
pathTracer.setSize( width, height );
|
|
1040
1544
|
|
|
1041
1545
|
}
|
|
1042
1546
|
|
|
1043
|
-
// update the samples
|
|
1044
|
-
|
|
1547
|
+
// update the samples, stopping once every pixel has reached "maxSamples"
|
|
1548
|
+
const samplesComplete = maxSamples > 0 && this._lastSampleCounts.min >= maxSamples;
|
|
1549
|
+
if ( ! this.pause && ! samplesComplete && this._stage > STAGE_BUILDING && ( ! lowResMode || ( lowResMode && dynamicLowRes ) ) ) {
|
|
1045
1550
|
|
|
1046
1551
|
pathTracer.lowResMode = lowResMode;
|
|
1552
|
+
pathTracer.asyncCompile = this.asyncCompile;
|
|
1047
1553
|
pathTracer.update();
|
|
1554
|
+
this._readTiming();
|
|
1555
|
+
this._setStage( pathTracer.compiling ? STAGE_COMPILING : STAGE_RENDERING );
|
|
1556
|
+
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// nothing renders while the next scene's BVHs build or the kernels compile, so restart the preview timer and
|
|
1560
|
+
// keep the last frame
|
|
1561
|
+
// TODO: decide what to display meanwhile - the last frame goes stale when the camera moves, and a
|
|
1562
|
+
// rasterized preview compiles its own materials
|
|
1563
|
+
if ( this._stage < STAGE_RENDERING ) {
|
|
1564
|
+
|
|
1565
|
+
this._resetTime = - 1;
|
|
1566
|
+
return;
|
|
1048
1567
|
|
|
1049
1568
|
}
|
|
1050
1569
|
|
|
@@ -1056,17 +1575,25 @@ export class WebGPUPathTracer {
|
|
|
1056
1575
|
// counts every frame. A count of the camera rays dispatched would answer it in one value
|
|
1057
1576
|
const awaitingDenoise = Boolean( denoiser ) && ! denoiser.complete && ! denoiser.running && maxSamples > 0;
|
|
1058
1577
|
|
|
1059
|
-
// Gate on the
|
|
1578
|
+
// Gate on the average pixel. Measuring is expensive so it stops once faded in, and
|
|
1060
1579
|
// the check reads the last measurement rather than waiting on this one.
|
|
1061
1580
|
if ( ! lowResMode ) {
|
|
1062
1581
|
|
|
1063
|
-
|
|
1582
|
+
// the counts can't change once every pixel has finished, so measuring stops
|
|
1583
|
+
if ( ! samplesComplete && this._fadeState < 1 && minSamples > 0 ) {
|
|
1064
1584
|
|
|
1065
|
-
this.
|
|
1585
|
+
this._measureSamples();
|
|
1586
|
+
|
|
1587
|
+
} else if ( ! samplesComplete && this._resetTime - this._lastCompleteCheck >= COMPLETE_CHECK_INTERVAL ) {
|
|
1588
|
+
|
|
1589
|
+
// the readback stalls the gpu pipeline, so the render only measures a few times a second.
|
|
1590
|
+
// The "progress" and "complete" events and the denoiser key off it
|
|
1591
|
+
this._lastCompleteCheck = this._resetTime;
|
|
1592
|
+
this._measureSamples();
|
|
1066
1593
|
|
|
1067
1594
|
}
|
|
1068
1595
|
|
|
1069
|
-
if ( this._lastSampleCounts.
|
|
1596
|
+
if ( this._lastSampleCounts.avg >= minSamples ) {
|
|
1070
1597
|
|
|
1071
1598
|
this._fadeState += delta / this.fadeDuration;
|
|
1072
1599
|
this._fadeState = Math.min( 1.0, this._fadeState ) || 1.0;
|
|
@@ -1086,7 +1613,7 @@ export class WebGPUPathTracer {
|
|
|
1086
1613
|
// "min" so every pixel has stopped, not just the average
|
|
1087
1614
|
if ( awaitingDenoise && this._lastSampleCounts.min >= maxSamples ) {
|
|
1088
1615
|
|
|
1089
|
-
denoiser.update( pathTracer.outputTarget );
|
|
1616
|
+
denoiser.update( pathTracer.outputTarget, pathTracer.auxTarget );
|
|
1090
1617
|
|
|
1091
1618
|
}
|
|
1092
1619
|
|
|
@@ -1094,26 +1621,41 @@ export class WebGPUPathTracer {
|
|
|
1094
1621
|
|
|
1095
1622
|
}
|
|
1096
1623
|
|
|
1097
|
-
|
|
1624
|
+
// the upscale and the draw only serve the display
|
|
1625
|
+
if ( this.renderToCanvas ) {
|
|
1098
1626
|
|
|
1099
|
-
|
|
1627
|
+
if ( upscaler && ! lowResMode ) {
|
|
1100
1628
|
|
|
1101
|
-
|
|
1102
|
-
renderer.setRenderTarget( originalTarget );
|
|
1629
|
+
texture = upscaler.upscale( texture, this.camera );
|
|
1103
1630
|
|
|
1104
|
-
|
|
1631
|
+
// the pass above binds its own targets
|
|
1632
|
+
renderer.setRenderTarget( originalTarget );
|
|
1633
|
+
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
renderer.autoClear = dynamicLowRes ? true : opacity === 1.0;
|
|
1637
|
+
blitQuad.material.transition = dynamicLowRes ? opacity : 1.0;
|
|
1638
|
+
blitQuad.material.opacity = dynamicLowRes ? 1.0 : opacity;
|
|
1639
|
+
blitQuad.material.fromTexture = lowResTarget;
|
|
1640
|
+
blitQuad.material.texture = texture;
|
|
1641
|
+
blitQuad.render( renderer );
|
|
1105
1642
|
|
|
1106
|
-
|
|
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 );
|
|
1643
|
+
}
|
|
1112
1644
|
|
|
1113
1645
|
// reset the renderer
|
|
1114
1646
|
renderer.autoClear = originalAutoClear;
|
|
1115
1647
|
renderer.setRenderTarget( originalTarget );
|
|
1116
1648
|
|
|
1649
|
+
// fires once the render, and the denoiser if one is attached, has finished and the result is drawn
|
|
1650
|
+
const complete = this.complete;
|
|
1651
|
+
if ( complete && ! this._wasComplete ) {
|
|
1652
|
+
|
|
1653
|
+
this.dispatchEvent( { type: 'complete' } );
|
|
1654
|
+
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
this._wasComplete = complete;
|
|
1658
|
+
|
|
1117
1659
|
}
|
|
1118
1660
|
|
|
1119
1661
|
/**
|
|
@@ -1263,6 +1805,42 @@ export class WebGPUPathTracer {
|
|
|
1263
1805
|
|
|
1264
1806
|
}
|
|
1265
1807
|
|
|
1808
|
+
/**
|
|
1809
|
+
* Draws the albedo or normal auxiliary buffer accumulated for the denoiser, for checking what
|
|
1810
|
+
* it is given. Nothing is drawn until a denoiser that uses them is attached.
|
|
1811
|
+
*
|
|
1812
|
+
* @param {'albedo'|'normal'} [buffer='albedo']
|
|
1813
|
+
*/
|
|
1814
|
+
renderAuxBuffer( buffer = 'albedo' ) {
|
|
1815
|
+
|
|
1816
|
+
const renderer = this._renderer;
|
|
1817
|
+
const pathTracer = this._pathTracer;
|
|
1818
|
+
|
|
1819
|
+
if ( ! renderer._initialized || ! pathTracer.auxTarget ) {
|
|
1820
|
+
|
|
1821
|
+
return;
|
|
1822
|
+
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if ( ! this._auxDebugQuad ) {
|
|
1826
|
+
|
|
1827
|
+
this._auxDebugQuad = new FullScreenQuad( new AuxDebugMaterial() );
|
|
1828
|
+
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const originalToneMapping = renderer.toneMapping;
|
|
1832
|
+
renderer.toneMapping = NoToneMapping;
|
|
1833
|
+
|
|
1834
|
+
const quad = this._auxDebugQuad;
|
|
1835
|
+
quad.material.texture = pathTracer.auxTarget;
|
|
1836
|
+
quad.material.showNormal = buffer === 'normal';
|
|
1837
|
+
renderer.setRenderTarget( null );
|
|
1838
|
+
quad.render( renderer );
|
|
1839
|
+
|
|
1840
|
+
renderer.toneMapping = originalToneMapping;
|
|
1841
|
+
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1266
1844
|
/**
|
|
1267
1845
|
* Frees every GPU resource held by the tracer, including any attached denoiser and upscaler.
|
|
1268
1846
|
*/
|
|
@@ -1275,14 +1853,22 @@ export class WebGPUPathTracer {
|
|
|
1275
1853
|
this._bvhData.textureAtlas.dispose();
|
|
1276
1854
|
this._environmentCache.dispose();
|
|
1277
1855
|
this._backgroundCache.dispose();
|
|
1278
|
-
this._blitQuad.dispose();
|
|
1279
1856
|
this._lowResTarget.dispose();
|
|
1857
|
+
|
|
1858
|
+
// FullScreenQuad.dispose only disposes the shared geometry, not the material
|
|
1859
|
+
this._blitQuad.dispose();
|
|
1860
|
+
this._blitQuad.material.dispose();
|
|
1280
1861
|
this._atlasDebugQuad?.dispose();
|
|
1862
|
+
this._atlasDebugQuad?.material.dispose();
|
|
1281
1863
|
this._sampleDensityQuad?.dispose();
|
|
1864
|
+
this._sampleDensityQuad?.material.dispose();
|
|
1865
|
+
this._auxDebugQuad?.dispose();
|
|
1866
|
+
this._auxDebugQuad?.material.dispose();
|
|
1282
1867
|
|
|
1283
1868
|
if ( this._debugBoundsQuad !== undefined ) {
|
|
1284
1869
|
|
|
1285
1870
|
this._debugBoundsQuad.dispose();
|
|
1871
|
+
this._debugBoundsQuad.material.dispose();
|
|
1286
1872
|
|
|
1287
1873
|
}
|
|
1288
1874
|
|