three-gpu-pathtracer 0.0.27 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +0 -1
  2. package/build/index.module.js +5 -3
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +4 -2
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +7 -7
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  9. package/src/webgpu/API.md +167 -19
  10. package/src/webgpu/AtlasTexture.js +2 -3
  11. package/src/webgpu/EquirectHdrInfoNode.js +89 -48
  12. package/src/webgpu/LightsInfoNode.js +1 -1
  13. package/src/webgpu/MegaKernelPathTracer.js +10 -3
  14. package/src/webgpu/PathTracerBackend.js +172 -21
  15. package/src/webgpu/WaveFrontPathTracer.js +156 -71
  16. package/src/webgpu/WebGPUPathTracer.js +628 -48
  17. package/src/webgpu/compute/ComputeKernel.js +8 -0
  18. package/src/webgpu/compute/PathTracerMegaKernel.js +187 -36
  19. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  20. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  21. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  22. package/src/webgpu/compute/targets.js +55 -0
  23. package/src/webgpu/compute/wavefront/LogicKernel.js +67 -20
  24. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  25. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  26. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  27. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  28. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  29. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  30. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  31. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -25
  32. package/src/webgpu/compute/wavefront/structs.js +63 -37
  33. package/src/webgpu/constants.js +2 -2
  34. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  35. package/src/webgpu/index.d.ts +41 -6
  36. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  37. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  38. package/src/webgpu/nodes/PathtracerBVHComputeData.js +35 -13
  39. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  40. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  41. package/src/webgpu/nodes/material.wgsl.js +2 -1
  42. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  43. package/src/webgpu/nodes/random.wgsl.js +2 -0
  44. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  45. package/src/webgpu/nodes/structs.wgsl.js +7 -1
  46. package/src/webgpu/upscale/FSRUpscaler.js +1 -1
@@ -1,8 +1,20 @@
1
- import { ExternalTexture, NearestFilter, NoToneMapping, RenderTarget, UnsignedByteType } from 'three/webgpu';
2
- import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
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 { Camera, Scene, Texture, WebGPURenderer } from 'three/webgpu' */
5
- /** @import { DynamicTileSetting } from 'oidn-web' */
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-web';
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-web" must be provided.' );
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 skips a scene render
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-web. Null keeps its defaults
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
- this._auxTarget = null;
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
- * @param {Scene} scene
126
- * @param {Camera} camera
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
- const auxTarget = this._renderAuxiliaryBuffers( target.width, target.height );
154
- albedo = auxTarget.textures[ 0 ];
155
- normal = auxTarget.textures[ 1 ];
165
+ this._unpackAux( aux );
166
+ albedo = this._albedoTarget;
167
+ normal = this._normalTarget;
156
168
 
157
169
  }
158
170
 
159
- this.denoise( target, albedo, normal );
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: output => this._copyOutputToTexture( output, width, height ),
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
- // Rasterizes the albedo and normal buffers that guide the filter. oidn-web takes normals
280
- // mapped into [0,1], with (0.5, 0.5, 1) as a flat normal.
281
- _renderAuxiliaryBuffers( width, height ) {
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, scene, camera } = this;
310
+ const { renderer, _unpackAuxKernel, _albedoTarget, _normalTarget } = this;
311
+ const { width, height } = aux.image;
284
312
 
285
- if ( ! this._auxTarget ) {
313
+ _albedoTarget.setSize( width, height );
314
+ _normalTarget.setSize( width, height );
286
315
 
287
- // Eight bits per channel since both buffers hold [0,1]. Multisampled so the rasterized
288
- // silhouettes match the jittered path traced ones.
289
- this._auxTarget = new RenderTarget( 1, 1, {
290
- count: 2,
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
- const auxTarget = this._auxTarget;
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
- renderer.setMRT( originalMRT );
321
- renderer.setRenderTarget( originalTarget );
322
- renderer.toneMapping = originalToneMapping;
323
+ // the unpacked images only live for a pass
324
+ _disposeAuxImages() {
323
325
 
324
- return auxTarget;
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, adapterInfo: device.adapterInfo }, options );
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-web can write into a caller-provided storage texture.
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;
@@ -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 class OIDNDenoiser {
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
- setScene( scene: Scene, camera: Camera ): void;
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 class WebGPUPathTracer {
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
- result += ( 1.0 - surf.metalness ) * surf.transmission * refraction * surf.color;
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
- // the energy the specular interface takes from the layers below
232
- let fresnelEnergySS = ${ this.turquinTexture.sampleDielectricFn }( NdotV, surf.roughness, surf.ior ) * dielectricBoost;
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
- result.color = ( 1.0 - surf.metalness ) * surf.transmission * surf.color *
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, plus whether a shadow ray passed a face a bsdf ray would hit
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
- forwardBlocked: 'bool',
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, and pass through the objects not visible to them
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 pass the objects not visible to them, but a bsdf ray would hit this face
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.forwardBlocked = true;
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
- const iridescenceThicknessRange = getField( m, 'iridescenceThicknessRange', [ 100, 400 ] );
778
- floatArray[ index ++ ] = iridescenceThicknessRange[ 0 ];
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 };