three-gpu-pathtracer 0.0.23 → 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.
- package/README.md +53 -528
- package/build/index.module.js +319 -66
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +318 -64
- package/build/index.umd.cjs.map +1 -1
- package/package.json +35 -18
- package/src/core/PathTracingSceneGenerator.js +21 -2
- package/src/core/WebGLPathTracer.js +16 -7
- package/src/core/utils/mergeGeometries.js +14 -1
- package/src/core/utils/sceneUpdateUtils.js +8 -12
- package/src/detectors/PrecisionMaterial.js +9 -9
- package/src/index.d.ts +2 -1
- package/src/index.js +1 -0
- package/src/materials/pathtracing/PhysicalPathTracingMaterial.js +2 -1
- package/src/materials/pathtracing/glsl/attenuate_hit_function.glsl.js +2 -1
- package/src/materials/pathtracing/glsl/camera_util_functions.glsl.js +1 -1
- package/src/materials/pathtracing/glsl/get_surface_record_function.glsl.js +2 -1
- package/src/materials/surface/AmbientOcclusionMaterial.js +2 -0
- package/src/objects/PhysicalSpotLight.js +2 -0
- package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
- package/src/shader/bvh/inside_fog_volume_function.glsl.js +1 -1
- package/src/shader/structs/material_struct.glsl.js +3 -1
- package/src/textures/BlueNoiseTexture.js +6 -6
- package/src/textures/ProceduralEquirectTexture.js +8 -7
- package/src/textures/turquinMetal.png +0 -0
- package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
- package/src/uniforms/FloatAttributeTextureArray.js +11 -11
- package/src/uniforms/MaterialsTexture.js +13 -10
- package/src/webgpu/API.md +762 -0
- package/src/webgpu/AtlasTexture.js +471 -0
- package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
- package/src/webgpu/EquirectBackgroundInfo.js +97 -0
- package/src/webgpu/EquirectHdrInfoNode.js +151 -0
- package/src/webgpu/LightsInfoNode.js +224 -0
- package/src/webgpu/MegaKernelPathTracer.js +277 -0
- package/src/webgpu/PathTracerBackend.js +216 -0
- package/src/webgpu/TurquinTexture.js +169 -0
- package/src/webgpu/WaveFrontPathTracer.js +586 -0
- package/src/webgpu/WebGPUPathTracer.js +1302 -0
- package/src/webgpu/compute/ComputeKernel.js +92 -0
- package/src/webgpu/compute/CopyBufferKernel.js +40 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
- package/src/webgpu/compute/SampleDebugKernel.js +51 -0
- package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
- package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
- package/src/webgpu/compute/ZeroOutKernel.js +31 -0
- package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
- package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
- package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
- package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
- package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
- package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
- package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
- package/src/webgpu/compute/wavefront/structs.js +155 -0
- package/src/webgpu/constants.js +62 -0
- package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
- package/src/webgpu/index.d.ts +176 -0
- package/src/webgpu/index.js +11 -0
- package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
- package/src/webgpu/materials/GraphMaterial.js +331 -0
- package/src/webgpu/materials/PathtracingMaterial.js +89 -0
- package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
- package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
- package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
- package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
- package/src/webgpu/nodes/eon.wgsl.js +262 -0
- package/src/webgpu/nodes/ggx.wgsl.js +190 -0
- package/src/webgpu/nodes/lights.wgsl.js +203 -0
- package/src/webgpu/nodes/material.wgsl.js +990 -0
- package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
- package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
- package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
- package/src/webgpu/nodes/random.wgsl.js +20 -0
- package/src/webgpu/nodes/reset.wgsl.js +21 -0
- package/src/webgpu/nodes/sampling.wgsl.js +193 -0
- package/src/webgpu/nodes/sheen.wgsl.js +148 -0
- package/src/webgpu/nodes/structs.wgsl.js +272 -0
- package/src/webgpu/nodes/utils.wgsl.js +366 -0
- package/src/webgpu/shims/ArrayCameraShim.js +74 -0
- package/src/webgpu/shims/EquirectCameraShim.js +37 -0
- package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
- 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
|
+
}
|