three-gpu-pathtracer 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/build/index.module.js +5 -3
- package/build/index.module.js.map +1 -1
- package/build/index.umd.cjs +4 -2
- package/build/index.umd.cjs.map +1 -1
- package/package.json +7 -7
- package/src/core/PathTracingSceneGenerator.js +1 -1
- package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
- package/src/webgpu/API.md +167 -19
- package/src/webgpu/AtlasTexture.js +2 -3
- package/src/webgpu/EquirectHdrInfoNode.js +89 -48
- package/src/webgpu/LightsInfoNode.js +1 -1
- package/src/webgpu/MegaKernelPathTracer.js +10 -3
- package/src/webgpu/PathTracerBackend.js +172 -21
- package/src/webgpu/WaveFrontPathTracer.js +156 -71
- package/src/webgpu/WebGPUPathTracer.js +628 -48
- package/src/webgpu/compute/ComputeKernel.js +8 -0
- package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
- 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 +67 -20
- 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 -25
- package/src/webgpu/compute/wavefront/structs.js +63 -37
- package/src/webgpu/constants.js +2 -2
- package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
- package/src/webgpu/index.d.ts +41 -6
- package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
- package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
- package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
- package/src/webgpu/nodes/aux.wgsl.js +61 -0
- package/src/webgpu/nodes/eon.wgsl.js +1 -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 +7 -1
- package/src/webgpu/upscale/FSRUpscaler.js +1 -1
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { EventDispatcher, ExternalTexture } from 'three/webgpu';
|
|
2
|
+
import { UnpackAuxKernel } from '../compute/UnpackAuxKernel.js';
|
|
3
|
+
import { createAuxImageTarget } from '../compute/targets.js';
|
|
3
4
|
|
|
4
|
-
/** @import {
|
|
5
|
-
/** @import { DynamicTileSetting } from 'oidn-
|
|
5
|
+
/** @import { StorageArrayTexture, Texture, WebGPURenderer } from 'three/webgpu' */
|
|
6
|
+
/** @import { DynamicTileSetting } from 'oidn-webgpu' */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fired when a pass started by `update` fails, such as the weights failing to download, with the
|
|
10
|
+
* failure as the event's `error`. No other pass starts until a reset.
|
|
11
|
+
* @event OIDNDenoiser#error
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fired as each tile of a pass finishes, with the fraction of the pass done as `progress`.
|
|
16
|
+
* @event OIDNDenoiser#progress
|
|
17
|
+
*/
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
20
|
* Runs Open Image Denoise over a path traced image. Pass one to
|
|
@@ -12,14 +24,14 @@ import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
|
|
|
12
24
|
* nor the network files become a dependency.
|
|
13
25
|
*
|
|
14
26
|
* ```js
|
|
15
|
-
* import { initUNetFromURL } from 'oidn-
|
|
27
|
+
* import { initUNetFromURL } from 'oidn-webgpu';
|
|
16
28
|
* pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
|
|
17
29
|
* ```
|
|
18
30
|
*
|
|
19
31
|
* Weights come from the oidn-weights repository, where the "_small" and "_large" variants trade
|
|
20
32
|
* quality against download size and per tile cost.
|
|
21
33
|
*/
|
|
22
|
-
export class OIDNDenoiser {
|
|
34
|
+
export class OIDNDenoiser extends EventDispatcher {
|
|
23
35
|
|
|
24
36
|
/**
|
|
25
37
|
* The denoised result, or null until the first tile has been produced.
|
|
@@ -69,6 +81,8 @@ export class OIDNDenoiser {
|
|
|
69
81
|
*/
|
|
70
82
|
constructor( options = {} ) {
|
|
71
83
|
|
|
84
|
+
super();
|
|
85
|
+
|
|
72
86
|
const {
|
|
73
87
|
initUNetFromURL,
|
|
74
88
|
auxWeightsUrl,
|
|
@@ -80,7 +94,7 @@ export class OIDNDenoiser {
|
|
|
80
94
|
|
|
81
95
|
if ( ! initUNetFromURL ) {
|
|
82
96
|
|
|
83
|
-
throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-
|
|
97
|
+
throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-webgpu" must be provided.' );
|
|
84
98
|
|
|
85
99
|
}
|
|
86
100
|
|
|
@@ -88,27 +102,32 @@ export class OIDNDenoiser {
|
|
|
88
102
|
this.auxWeightsUrl = auxWeightsUrl;
|
|
89
103
|
this.colorWeightsUrl = colorWeightsUrl;
|
|
90
104
|
|
|
91
|
-
// `false` falls back to the color only model, which is blurrier but
|
|
105
|
+
// `false` falls back to the color only model, which is blurrier but needs no auxiliary buffers
|
|
92
106
|
this.useAuxiliaryBuffers = useAuxiliaryBuffers;
|
|
93
107
|
|
|
94
|
-
// tiling, passed through to oidn-
|
|
108
|
+
// tiling, passed through to oidn-webgpu. Null keeps its defaults
|
|
95
109
|
this.maxTileSize = maxTileSize;
|
|
96
110
|
this.dynamicTile = dynamicTile;
|
|
97
111
|
|
|
98
112
|
this.renderer = null;
|
|
99
|
-
this.scene = null;
|
|
100
|
-
this.camera = null;
|
|
101
113
|
|
|
102
114
|
this._texture = null;
|
|
103
115
|
this._running = false;
|
|
104
116
|
this._complete = false;
|
|
105
117
|
this._abort = null;
|
|
106
118
|
this._requestId = 0;
|
|
119
|
+
|
|
120
|
+
// set when a pass started by "update" fails, so the render loop doesn't start another until a reset
|
|
121
|
+
this._failed = false;
|
|
107
122
|
this._unets = { aux: null, color: null };
|
|
108
123
|
|
|
109
124
|
this._resultPipeline = null;
|
|
110
125
|
this._rawTexture = null;
|
|
111
|
-
|
|
126
|
+
|
|
127
|
+
// the albedo and normal unpacked from the path tracer's auxiliary target for a pass
|
|
128
|
+
this._unpackAuxKernel = new UnpackAuxKernel().setWorkgroupSize( 8, 8, 1 );
|
|
129
|
+
this._albedoTarget = createAuxImageTarget( 'Denoiser Albedo' );
|
|
130
|
+
this._normalTarget = createAuxImageTarget( 'Denoiser Normal' );
|
|
112
131
|
|
|
113
132
|
}
|
|
114
133
|
|
|
@@ -122,41 +141,40 @@ export class OIDNDenoiser {
|
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*/
|
|
128
|
-
setScene( scene, camera ) {
|
|
129
|
-
|
|
130
|
-
this.scene = scene;
|
|
131
|
-
this.camera = camera;
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Renders the auxiliary buffers and starts a pass. Safe to call every frame.
|
|
144
|
+
* Unpacks the auxiliary buffers the path tracer accumulated and starts a pass. Safe to call
|
|
145
|
+
* every frame.
|
|
137
146
|
*
|
|
138
147
|
* @param {Texture} target - The path traced result, in linear HDR.
|
|
148
|
+
* @param {StorageArrayTexture|null} aux - The packed auxiliary buffers, see "aux.wgsl.js".
|
|
139
149
|
* @returns {Texture|null}
|
|
140
150
|
*/
|
|
141
|
-
update( target ) {
|
|
151
|
+
update( target, aux = null ) {
|
|
142
152
|
|
|
143
|
-
if ( this._complete || this._running ) {
|
|
153
|
+
if ( this._complete || this._running || this._failed ) {
|
|
144
154
|
|
|
145
155
|
return this._texture;
|
|
146
156
|
|
|
147
157
|
}
|
|
148
158
|
|
|
159
|
+
// TODO: denoise the alpha for transparent backgrounds. oidn-webgpu's "denoiseAlpha" runs as a
|
|
160
|
+
// second pass, but its "_updatePasses" doesn't rebuild its shaders when the option changes
|
|
149
161
|
let albedo = null;
|
|
150
162
|
let normal = null;
|
|
151
|
-
if ( this.useAuxiliaryBuffers ) {
|
|
163
|
+
if ( this.useAuxiliaryBuffers && aux ) {
|
|
152
164
|
|
|
153
|
-
|
|
154
|
-
albedo =
|
|
155
|
-
normal =
|
|
165
|
+
this._unpackAux( aux );
|
|
166
|
+
albedo = this._albedoTarget;
|
|
167
|
+
normal = this._normalTarget;
|
|
156
168
|
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
// a failure, such as the weights failing to download, is reported once rather than every frame
|
|
172
|
+
this.denoise( target, albedo, normal ).catch( error => {
|
|
173
|
+
|
|
174
|
+
this._failed = true;
|
|
175
|
+
this.dispatchEvent( { type: 'error', error } );
|
|
176
|
+
|
|
177
|
+
} );
|
|
160
178
|
|
|
161
179
|
return this._texture;
|
|
162
180
|
|
|
@@ -213,14 +231,22 @@ export class OIDNDenoiser {
|
|
|
213
231
|
const inputs = {
|
|
214
232
|
color: { data: backend.get( color ).texture, width, height },
|
|
215
233
|
|
|
234
|
+
// pace the tiles to display frames so the path tracer keeps rendering between them
|
|
235
|
+
scheduling: 'animation-frame',
|
|
236
|
+
|
|
216
237
|
// the output is seeded with the raw color, so copying per tile shows a progressive wipe
|
|
217
|
-
progress:
|
|
218
|
-
done: output => {
|
|
238
|
+
progress: ( output, tileData, tile, tileIndex, tileCount ) => {
|
|
219
239
|
|
|
220
240
|
this._copyOutputToTexture( output, width, height );
|
|
241
|
+
this.dispatchEvent( { type: 'progress', progress: ( tileIndex + 1 ) / tileCount } );
|
|
242
|
+
|
|
243
|
+
},
|
|
244
|
+
done: output => {
|
|
245
|
+
|
|
221
246
|
this._running = false;
|
|
222
247
|
this._complete = true;
|
|
223
248
|
this._abort = null;
|
|
249
|
+
this._disposeAuxImages();
|
|
224
250
|
|
|
225
251
|
},
|
|
226
252
|
};
|
|
@@ -254,17 +280,18 @@ export class OIDNDenoiser {
|
|
|
254
280
|
this._rawTexture?.destroy();
|
|
255
281
|
this._rawTexture = null;
|
|
256
282
|
|
|
283
|
+
this._disposeAuxImages();
|
|
284
|
+
|
|
257
285
|
this._running = false;
|
|
258
286
|
this._complete = false;
|
|
287
|
+
this._failed = false;
|
|
259
288
|
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
dispose() {
|
|
263
292
|
|
|
264
293
|
this.reset();
|
|
265
|
-
|
|
266
|
-
this._auxTarget?.dispose();
|
|
267
|
-
this._auxTarget = null;
|
|
294
|
+
this._unpackAuxKernel.dispose();
|
|
268
295
|
|
|
269
296
|
// the networks hold the weights on the GPU, and a load may still be in flight
|
|
270
297
|
for ( const key in this._unets ) {
|
|
@@ -276,52 +303,28 @@ export class OIDNDenoiser {
|
|
|
276
303
|
|
|
277
304
|
}
|
|
278
305
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
306
|
+
// Unpacks the path tracer's auxiliary target into albedo and normal textures, since oidn-webgpu
|
|
307
|
+
// reads every input as the same kind as the color texture
|
|
308
|
+
_unpackAux( aux ) {
|
|
282
309
|
|
|
283
|
-
const { renderer,
|
|
310
|
+
const { renderer, _unpackAuxKernel, _albedoTarget, _normalTarget } = this;
|
|
311
|
+
const { width, height } = aux.image;
|
|
284
312
|
|
|
285
|
-
|
|
313
|
+
_albedoTarget.setSize( width, height );
|
|
314
|
+
_normalTarget.setSize( width, height );
|
|
286
315
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
type: UnsignedByteType,
|
|
292
|
-
minFilter: NearestFilter,
|
|
293
|
-
magFilter: NearestFilter,
|
|
294
|
-
samples: 4,
|
|
295
|
-
} );
|
|
296
|
-
|
|
297
|
-
// the MRT keys are matched against these names
|
|
298
|
-
this._auxTarget.textures[ 0 ].name = 'output';
|
|
299
|
-
this._auxTarget.textures[ 1 ].name = 'normal';
|
|
300
|
-
|
|
301
|
-
}
|
|
316
|
+
_unpackAuxKernel.auxTarget = aux;
|
|
317
|
+
_unpackAuxKernel.albedoTarget = _albedoTarget;
|
|
318
|
+
_unpackAuxKernel.normalTarget = _normalTarget;
|
|
319
|
+
renderer.compute( _unpackAuxKernel.kernel, _unpackAuxKernel.getDispatchSize( width, height ) );
|
|
302
320
|
|
|
303
|
-
|
|
304
|
-
auxTarget.setSize( width, height );
|
|
305
|
-
|
|
306
|
-
const originalTarget = renderer.getRenderTarget();
|
|
307
|
-
const originalMRT = renderer.getMRT();
|
|
308
|
-
const originalToneMapping = renderer.toneMapping;
|
|
309
|
-
|
|
310
|
-
// the buffers are data rather than an image, so they must not be tone mapped
|
|
311
|
-
renderer.toneMapping = NoToneMapping;
|
|
312
|
-
renderer.setRenderTarget( auxTarget );
|
|
313
|
-
renderer.setMRT( mrt( {
|
|
314
|
-
output: diffuseColor,
|
|
315
|
-
normal: vec4( normalView.mul( 0.5 ).add( 0.5 ), 1.0 ),
|
|
316
|
-
} ) );
|
|
317
|
-
|
|
318
|
-
renderer.render( scene, camera );
|
|
321
|
+
}
|
|
319
322
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
renderer.toneMapping = originalToneMapping;
|
|
323
|
+
// the unpacked images only live for a pass
|
|
324
|
+
_disposeAuxImages() {
|
|
323
325
|
|
|
324
|
-
|
|
326
|
+
this._albedoTarget.dispose();
|
|
327
|
+
this._normalTarget.dispose();
|
|
325
328
|
|
|
326
329
|
}
|
|
327
330
|
|
|
@@ -346,7 +349,7 @@ export class OIDNDenoiser {
|
|
|
346
349
|
if ( this.maxTileSize !== null ) options.maxTileSize = this.maxTileSize;
|
|
347
350
|
if ( this.dynamicTile !== null ) options.dynamicTile = this.dynamicTile;
|
|
348
351
|
|
|
349
|
-
return this.initUNetFromURL( url, { device
|
|
352
|
+
return this.initUNetFromURL( url, { device }, options );
|
|
350
353
|
|
|
351
354
|
} )();
|
|
352
355
|
|
|
@@ -358,7 +361,7 @@ export class OIDNDenoiser {
|
|
|
358
361
|
|
|
359
362
|
// Copies the denoiser's packed output buffer into a sampleable texture, using a compute pass
|
|
360
363
|
// since copyBufferToTexture requires 256 byte row alignment.
|
|
361
|
-
// TODO: remove the copy once oidn-
|
|
364
|
+
// TODO: remove the copy once oidn-webgpu can write into a caller-provided storage texture.
|
|
362
365
|
_copyOutputToTexture( output, width, height ) {
|
|
363
366
|
|
|
364
367
|
const device = this.renderer.backend.device;
|
package/src/webgpu/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Camera, Scene, Texture, ExternalTexture } from 'three';
|
|
2
|
-
import { WebGPURenderer } from 'three/webgpu';
|
|
1
|
+
import { Camera, Color, EventDispatcher, Scene, Texture, ExternalTexture } from 'three';
|
|
2
|
+
import { StorageArrayTexture, WebGPURenderer } from 'three/webgpu';
|
|
3
|
+
import { BVHWorker } from '../index.js';
|
|
3
4
|
|
|
4
5
|
// three.js type augmentation
|
|
5
6
|
|
|
@@ -12,6 +13,15 @@ declare module 'three' {
|
|
|
12
13
|
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// volume scattering from "KHR_materials_scatter"
|
|
17
|
+
export interface MeshPhysicalMaterial {
|
|
18
|
+
|
|
19
|
+
scatterStrength?: number;
|
|
20
|
+
multiscatterColor?: Color;
|
|
21
|
+
scatterAnisotropy?: number;
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export interface Object3D {
|
|
16
26
|
|
|
17
27
|
visibleToShadowRays?: boolean;
|
|
@@ -102,7 +112,14 @@ export interface OIDNDenoiserOptions {
|
|
|
102
112
|
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
export
|
|
115
|
+
export interface OIDNDenoiserEventMap {
|
|
116
|
+
|
|
117
|
+
'error': { error: Error };
|
|
118
|
+
'progress': { progress: number };
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class OIDNDenoiser extends EventDispatcher<OIDNDenoiserEventMap> {
|
|
106
123
|
|
|
107
124
|
constructor( options: OIDNDenoiserOptions );
|
|
108
125
|
|
|
@@ -118,8 +135,7 @@ export class OIDNDenoiser {
|
|
|
118
135
|
readonly running: boolean;
|
|
119
136
|
|
|
120
137
|
init( renderer: WebGPURenderer ): void;
|
|
121
|
-
|
|
122
|
-
update( target: Texture ): Texture | null;
|
|
138
|
+
update( target: Texture, aux?: StorageArrayTexture | null ): Texture | null;
|
|
123
139
|
denoise( color: Texture, albedo?: Texture | null, normal?: Texture | null ): Promise<void>;
|
|
124
140
|
reset(): void;
|
|
125
141
|
dispose(): void;
|
|
@@ -155,13 +171,23 @@ export interface DebugBoundsOptions {
|
|
|
155
171
|
|
|
156
172
|
}
|
|
157
173
|
|
|
158
|
-
export
|
|
174
|
+
export interface WebGPUPathTracerEventMap {
|
|
175
|
+
|
|
176
|
+
'complete': {};
|
|
177
|
+
'progress': { stage: 'building' | 'compiling' | 'rendering' | 'denoising', progress: number | null, samples?: SampleCounts };
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class WebGPUPathTracer extends EventDispatcher<WebGPUPathTracerEventMap> {
|
|
159
182
|
|
|
160
183
|
constructor( renderer: WebGPURenderer );
|
|
161
184
|
|
|
162
185
|
maxBounces: number;
|
|
163
186
|
frameBudget: number;
|
|
187
|
+
adaptiveFrameBudget: boolean;
|
|
188
|
+
targetFrameTime: number;
|
|
164
189
|
maxTransparentBounces: number;
|
|
190
|
+
maxSubsurfaceBounces: number;
|
|
165
191
|
maxSamples: number;
|
|
166
192
|
transmissiveBackground: TransmissiveBackgroundMode;
|
|
167
193
|
filterGlossyFactor: number;
|
|
@@ -174,17 +200,23 @@ export class WebGPUPathTracer {
|
|
|
174
200
|
fadeDuration: number;
|
|
175
201
|
dynamicLowRes: boolean;
|
|
176
202
|
lowResScale: number;
|
|
203
|
+
adaptiveDynamicRes: boolean;
|
|
177
204
|
renderScale: number;
|
|
178
205
|
synchronizeRenderSize: boolean;
|
|
206
|
+
renderToCanvas: boolean;
|
|
207
|
+
asyncCompile: boolean;
|
|
179
208
|
generateMissingAttributes: boolean;
|
|
180
209
|
commonAttributes: Array<string>;
|
|
181
210
|
stableNoise: boolean;
|
|
182
211
|
pause: boolean;
|
|
183
212
|
|
|
184
213
|
readonly target: Texture | null;
|
|
214
|
+
readonly frameTime: number;
|
|
215
|
+
readonly complete: boolean;
|
|
185
216
|
readonly fadeState: number;
|
|
186
217
|
readonly lowResTarget: Texture;
|
|
187
218
|
readonly lowResMode: boolean;
|
|
219
|
+
readonly compiling: boolean;
|
|
188
220
|
readonly textureAtlas: AtlasTexture;
|
|
189
221
|
|
|
190
222
|
getSampleCountsAsync(): Promise<SampleCounts>;
|
|
@@ -193,6 +225,8 @@ export class WebGPUPathTracer {
|
|
|
193
225
|
setUpscaler( upscaler: FSRUpscaler | null ): void;
|
|
194
226
|
|
|
195
227
|
setScene( scene: Scene, camera: Camera ): void;
|
|
228
|
+
setBVHWorker( WorkerConstructor: new () => BVHWorker & { dispose(): void } ): void;
|
|
229
|
+
setSceneAsync( scene: Scene, camera: Camera ): Promise<void>;
|
|
196
230
|
setRandom( random: RandomStrategy ): void;
|
|
197
231
|
setCamera( camera: Camera ): void;
|
|
198
232
|
setSize( x: number, y: number ): void;
|
|
@@ -208,6 +242,7 @@ export class WebGPUPathTracer {
|
|
|
208
242
|
renderDebugBounds( options?: DebugBoundsOptions ): void;
|
|
209
243
|
renderTextureAtlas( layer?: number ): void;
|
|
210
244
|
renderSampleDensity(): void;
|
|
245
|
+
renderAuxBuffer( buffer?: 'albedo' | 'normal' ): void;
|
|
211
246
|
getRenderTime(): number;
|
|
212
247
|
dispose(): void;
|
|
213
248
|
|
|
@@ -160,8 +160,10 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
|
|
|
160
160
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// the refracted half is not attenuated by the layers above
|
|
164
|
-
|
|
163
|
+
// the refracted half is not attenuated by the layers above. A volume is entered and exited,
|
|
164
|
+
// so each crossing takes the square root of the color, as in Cycles
|
|
165
|
+
let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
|
|
166
|
+
result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * transmissionTint;
|
|
165
167
|
|
|
166
168
|
} else {
|
|
167
169
|
|
|
@@ -228,11 +230,15 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
|
|
|
228
230
|
let metallic = metallicSpecular * metallicReflectance;
|
|
229
231
|
let dielectric = dielectricSpecular * dielectricReflectance;
|
|
230
232
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
+
// The energy the specular interface takes from the layers below, at the lobe's specular color
|
|
234
|
+
// and intensity. The table holds the albedo at the ior's f0, and the Schlick albedo is linear
|
|
235
|
+
// in f0, so the tinted albedo slides from it toward the conductor albedo at f0 = 1
|
|
236
|
+
let iorEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior );
|
|
237
|
+
let tintedEnergySS = iorEnergySS + ( dielectricF0 - surf.f0 ) * ( energySS - iorEnergySS ) / ( 1.0 - surf.f0 );
|
|
238
|
+
let fresnelEnergySS = surf.specularIntensity * tintedEnergySS * dielectricBoost;
|
|
233
239
|
|
|
234
240
|
result += attenuation * mix( ( 1.0 - surf.transmission ) * dielectric, metallic, surf.metalness );
|
|
235
|
-
attenuation *= ( 1.0 - surf.metalness ) * mix( 1.0 - fresnelEnergySS, 1.0 - filmFresnelMax, surf.iridescence );
|
|
241
|
+
attenuation *= ( 1.0 - surf.metalness ) * mix( 1.0 - fresnelEnergySS, vec3f( 1.0 - filmFresnelMax ), surf.iridescence );
|
|
236
242
|
|
|
237
243
|
}
|
|
238
244
|
|
|
@@ -385,6 +391,13 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
|
|
|
385
391
|
|
|
386
392
|
wi = refract( - wo, wh, surf.eta );
|
|
387
393
|
|
|
394
|
+
// "refract" returns zero on total internal reflection, so reflect instead and let "isDead" drop it
|
|
395
|
+
if ( dot( wi, wi ) == 0.0 ) {
|
|
396
|
+
|
|
397
|
+
wi = - normalize( reflect( wo, wh ) );
|
|
398
|
+
|
|
399
|
+
}
|
|
400
|
+
|
|
388
401
|
}
|
|
389
402
|
|
|
390
403
|
// the facet choice must agree with the resulting hemisphere - reflection above,
|
|
@@ -417,7 +430,8 @@ export class GltfCompliantMaterial extends PathtracingMaterial {
|
|
|
417
430
|
|
|
418
431
|
// Cycles represents a singular event with a large matching BSDF and PDF. Their
|
|
419
432
|
// ratio preserves throughput while forward MIS correctly gives the event full weight.
|
|
420
|
-
|
|
433
|
+
let transmissionTint = select( sqrt( surf.color ), surf.color, surf.thinWall );
|
|
434
|
+
result.color = ( 1.0 - surf.metalness ) * surf.transmission * transmissionTint *
|
|
421
435
|
deltaTransmittance * ${ SINGULAR_EVENT_SCALE };
|
|
422
436
|
result.direction = worldWi;
|
|
423
437
|
result.pdf = weights.transmission * deltaProbability * ${ SINGULAR_EVENT_SCALE };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MeshBasicNodeMaterial, NoBlending } from 'three/webgpu';
|
|
2
|
+
import { uv, varying, texture, uniform } from 'three/tsl';
|
|
3
|
+
import { wgslTagFn } from 'three-mesh-bvh/webgpu';
|
|
4
|
+
import { createAuxTarget } from '../../compute/targets.js';
|
|
5
|
+
import { unpackAuxAlbedoFn, unpackAuxNormalFn, AUX_LAYERS } from '../../nodes/aux.wgsl.js';
|
|
6
|
+
|
|
7
|
+
// Shows the albedo or normal the path tracer accumulates for the denoiser
|
|
8
|
+
export class AuxDebugMaterial extends MeshBasicNodeMaterial {
|
|
9
|
+
|
|
10
|
+
set texture( v ) {
|
|
11
|
+
|
|
12
|
+
this._texNode.value = v;
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
set showNormal( v ) {
|
|
17
|
+
|
|
18
|
+
this._showNormalUniform.value = v ? 1 : 0;
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
this.blending = NoBlending;
|
|
27
|
+
|
|
28
|
+
this._texNode = texture( createAuxTarget() );
|
|
29
|
+
this._showNormalUniform = uniform( 0, 'uint' );
|
|
30
|
+
|
|
31
|
+
const auxDebug = wgslTagFn/* wgsl */`
|
|
32
|
+
fn auxDebug( tex: texture_2d_array<u32>, coord: vec2f, showNormal: u32 ) -> vec4f {
|
|
33
|
+
|
|
34
|
+
// the rows are stored top down, so the quad's bottom up uv is flipped
|
|
35
|
+
let texel = vec2u( vec2f( coord.x, 1.0 - coord.y ) * vec2f( textureDimensions( tex, 0 ) ) );
|
|
36
|
+
var packed: vec3u;
|
|
37
|
+
for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
|
|
38
|
+
|
|
39
|
+
packed[ layer ] = textureLoad( tex, texel, layer, 0 ).r;
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let albedo = ${ unpackAuxAlbedoFn }( packed );
|
|
44
|
+
let normal = ${ unpackAuxNormalFn }( packed );
|
|
45
|
+
return vec4f( select( albedo, normal, showNormal == 1u ), 1.0 );
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
this.colorNode = auxDebug( this._texNode, varying( uv() ), this._showNormalUniform );
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
@@ -19,7 +19,8 @@ const transformStruct = new StructTypeNode( {
|
|
|
19
19
|
color: 'vec4f',
|
|
20
20
|
}, 'TransformStruct' );
|
|
21
21
|
|
|
22
|
-
// three-mesh-bvh's ray hit result
|
|
22
|
+
// three-mesh-bvh's ray hit result. Shadow rays also record the share of bsdf rays the faces they skipped
|
|
23
|
+
// would stop, and the share of light the partially transparent surfaces they passed block
|
|
23
24
|
const intersectionResultStruct = new StructTypeNode( {
|
|
24
25
|
indices: 'vec4u',
|
|
25
26
|
normal: 'vec3f',
|
|
@@ -28,7 +29,8 @@ const intersectionResultStruct = new StructTypeNode( {
|
|
|
28
29
|
objectIndex: 'uint',
|
|
29
30
|
side: 'float',
|
|
30
31
|
dist: 'float',
|
|
31
|
-
|
|
32
|
+
skippedOcclusion: 'float',
|
|
33
|
+
occlusion: 'float',
|
|
32
34
|
}, 'PathTracerIntersectionResult' );
|
|
33
35
|
|
|
34
36
|
// Pathtracer-specific version of the BVHComputeData tht includes material mapping, property structs
|
|
@@ -179,7 +181,7 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
179
181
|
|
|
180
182
|
const { textureAtlas, storage, fns } = this;
|
|
181
183
|
const textures = textureAtlas.texture;
|
|
182
|
-
const textureInfo = uniformArray( textureAtlas.textureInfo, 'uvec4' );
|
|
184
|
+
const textureInfo = uniformArray( textureAtlas.textureInfo, 'uvec4' ).setName( 'bvh_textureInfo' );
|
|
183
185
|
|
|
184
186
|
// build the single sampleTexel bound to this instance's textureInfo node
|
|
185
187
|
const sampleTexel = sampleTexelFunc( textureInfo, texture( textures ) );
|
|
@@ -193,7 +195,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
193
195
|
const scratchRayScalar = float( 1.0 ).toVar( 'bvh_rayScalar' );
|
|
194
196
|
const baseOpacityScalar = float( 1.0 ).toVar( 'bvh_baseOpacity' );
|
|
195
197
|
|
|
196
|
-
// shadow rays hit both sides,
|
|
198
|
+
// shadow rays hit both sides, pass through the objects not visible to them and partially transparent
|
|
199
|
+
// surfaces, and stop at the first opaque hit
|
|
197
200
|
const isShadowRay = bool( false ).toVar( 'bvh_isShadowRay' );
|
|
198
201
|
const skipShadowObject = bool( false ).toVar( 'bvh_skipShadowObject' );
|
|
199
202
|
|
|
@@ -211,8 +214,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
211
214
|
intersectsBoundsFn: wgslTagFn/* wgsl */`
|
|
212
215
|
fn rayIntersectsBounds( ray: ${ rayStruct }, bounds: ${ bvhNodeBoundsStruct }, result: ptr<function, ${ intersectionResultStruct }> ) -> u32 {
|
|
213
216
|
|
|
214
|
-
// early-out if our object is completely transparent
|
|
215
|
-
if ( ${ baseOpacityScalar } == 0.0 ) {
|
|
217
|
+
// early-out if our object is completely transparent, or a shadow ray is already occluded
|
|
218
|
+
if ( ${ baseOpacityScalar } == 0.0 || ( ${ isShadowRay } && result.didHit ) ) {
|
|
216
219
|
|
|
217
220
|
return 0u;
|
|
218
221
|
|
|
@@ -281,10 +284,9 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
281
284
|
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
var opacity = ${ baseOpacityScalar };
|
|
284
288
|
if ( material.transparent != 0 || material.alphaTest > 0.0 ) {
|
|
285
289
|
|
|
286
|
-
var opacity = ${ baseOpacityScalar };
|
|
287
|
-
|
|
288
290
|
// add support for vertex color opacity
|
|
289
291
|
if ( material.vertexColors == 1 ) {
|
|
290
292
|
|
|
@@ -341,12 +343,13 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
341
343
|
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
// shadow rays
|
|
346
|
+
// shadow rays skip the objects not visible to them. Record the share of bsdf rays this face
|
|
347
|
+
// would stop: all of them if it's opaque, its opacity if it's partially transparent
|
|
345
348
|
if ( ${ skipShadowObject } ) {
|
|
346
349
|
|
|
347
350
|
if ( material.side == 0 || triResult.side == material.side ) {
|
|
348
351
|
|
|
349
|
-
result.
|
|
352
|
+
result.skippedOcclusion += ( 1.0 - result.skippedOcclusion ) * select( 1.0, opacity, material.transparent != 0 );
|
|
350
353
|
|
|
351
354
|
}
|
|
352
355
|
|
|
@@ -354,6 +357,15 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
354
357
|
|
|
355
358
|
}
|
|
356
359
|
|
|
360
|
+
// shadow rays pass partially transparent surfaces, each blocking its opacity's share of the
|
|
361
|
+
// light still getting through, just as bsdf rays pass them by chance
|
|
362
|
+
if ( ${ isShadowRay } && material.transparent != 0 && opacity < 1.0 ) {
|
|
363
|
+
|
|
364
|
+
result.occlusion += ( 1.0 - result.occlusion ) * opacity;
|
|
365
|
+
continue;
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
357
369
|
result.didHit = true;
|
|
358
370
|
result.dist = triResult.dist;
|
|
359
371
|
result.normal = triResult.normal;
|
|
@@ -774,9 +786,8 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
774
786
|
floatArray[ index ++ ] = getField( m, 'iridescenceIOR', 1.3 );
|
|
775
787
|
|
|
776
788
|
// offset 32
|
|
777
|
-
|
|
778
|
-
floatArray[ index ++ ] =
|
|
779
|
-
floatArray[ index ++ ] = iridescenceThicknessRange[ 1 ];
|
|
789
|
+
floatArray[ index ++ ] = getField( m, 'iridescenceThicknessMinimum', 100 );
|
|
790
|
+
floatArray[ index ++ ] = getField( m, 'iridescenceThicknessMaximum', 400 );
|
|
780
791
|
floatArray[ index ++ ] = getField( m, 'diffuseRoughness', 0.0 );
|
|
781
792
|
floatArray[ index ++ ] = getField( m, 'dispersion', 0.0 );
|
|
782
793
|
|
|
@@ -943,6 +954,17 @@ export class PathtracerBVHComputeData extends BVHComputeData {
|
|
|
943
954
|
// anisotropyMap transform - offset 264
|
|
944
955
|
index += writeTextureMatrixToArray( m, 'anisotropyMap', floatArray, index );
|
|
945
956
|
|
|
957
|
+
// scattering volume - offset 276
|
|
958
|
+
const multiscatterColor = getField( m, 'multiscatterColor', null );
|
|
959
|
+
floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.r : 1.0;
|
|
960
|
+
floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.g : 1.0;
|
|
961
|
+
floatArray[ index ++ ] = multiscatterColor ? multiscatterColor.b : 1.0;
|
|
962
|
+
floatArray[ index ++ ] = getField( m, 'scatterStrength', 0.0 );
|
|
963
|
+
floatArray[ index ++ ] = getField( m, 'scatterAnisotropy', 0.0 );
|
|
964
|
+
|
|
965
|
+
// padding to the struct's 16 byte alignment
|
|
966
|
+
index += 3;
|
|
967
|
+
|
|
946
968
|
}
|
|
947
969
|
|
|
948
970
|
return { materialData: intArray, textures };
|