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
@@ -46,6 +46,14 @@ export class ComputeKernel {
46
46
  }
47
47
 
48
48
  const node = parameters[ key ];
49
+
50
+ // name storage buffers so equivalent kernels generate identical WGSL and hit the pipeline cache
51
+ if ( node.isStorageBufferNode && ! node.name ) {
52
+
53
+ node.setName( `kernel_${ key }` );
54
+
55
+ }
56
+
49
57
  if ( 'value' in node ) {
50
58
 
51
59
  Object.defineProperty( this, key, {
@@ -1,9 +1,12 @@
1
- import { DataTexture, Vector2, StorageTexture } from 'three/webgpu';
1
+ import { DataTexture, Vector2 } from 'three/webgpu';
2
2
  import { ComputeKernel } from './ComputeKernel.js';
3
+ import { createAuxTarget, createOutputTarget, createSampleCountTarget } from './targets.js';
3
4
  import { texture, sampler, uniform, globalId, textureStore } from 'three/tsl';
4
- import { rngInit, rngNextBounce, rand1, rand2, rand3, RNG_INDEX_RAY_JITTER, RNG_INDEX_BACKGROUND_SAMPLE, RNG_INDEX_DIRECT_LIGHT_SAMPLE, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH, RNG_INDEX_ALPHA_TEST } from '../nodes/random.wgsl.js';
5
- import { misHeuristicFn, weightedAlphaBlendFn } from '../nodes/sampling.wgsl.js';
6
- import { proxy, proxyFn, wgslTagFn, rayStruct } from 'three-mesh-bvh/webgpu';
5
+ import { blendAuxFn, isAuxSurfaceFn, AUX_LAYERS } from '../nodes/aux.wgsl.js';
6
+ import { rngInit, rngNextBounce, rand1, rand2, rand3, RNG_INDEX_RAY_JITTER, RNG_INDEX_BACKGROUND_SAMPLE, RNG_INDEX_DIRECT_LIGHT_SAMPLE, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH, RNG_INDEX_ALPHA_TEST, RNG_INDEX_MEDIUM_DISTANCE, RNG_INDEX_MEDIUM_DIRECTION } from '../nodes/random.wgsl.js';
7
+ import { misHeuristicFn, weightedAlphaBlendFn, sampleHenyeyGreensteinFn } from '../nodes/sampling.wgsl.js';
8
+ import { mediumStruct, enterMediumFn, mediumChannelPdfFn, sampleMediumDistanceFn, mediumDistancePdfFn, MEDIUM_THROUGHPUT_EPSILON } from '../nodes/medium.wgsl.js';
9
+ import { proxy, proxyFn, contextProxy, contextProxyFn, wgslTagFn, rayStruct } from 'three-mesh-bvh/webgpu';
7
10
  import { clampPathContributionFunc, isTerminatingScatterFunc, offsetRayOriginFunc } from '../nodes/utils.wgsl.js';
8
11
  import { lightRecordStruct } from '../nodes/structs.wgsl.js';
9
12
  import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, LIGHT_EPSILON, isMISWeightLightFn } from '../nodes/lights.wgsl.js';
@@ -15,15 +18,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
15
18
  constructor( ) {
16
19
 
17
20
  const params = {
18
- bvhData: { value: null },
19
21
  material: { value: null },
20
22
  envInfo: { value: null },
21
23
  lightsInfo: { value: null },
22
24
 
23
25
  // targets
24
- prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
25
- outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
26
- sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
26
+ prevOutputTarget: textureStore( createOutputTarget() ).toReadOnly(),
27
+ outputTarget: textureStore( createOutputTarget() ).toWriteOnly(),
28
+ sampleCountTarget: textureStore( createSampleCountTarget() ).toReadWrite(),
27
29
 
28
30
  // tiles
29
31
  offset: uniform( new Vector2() ),
@@ -33,6 +35,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
33
35
  seed: uniform( 0 ),
34
36
  maxBounces: uniform( 5 ),
35
37
  maxTransparentBounces: uniform( 5, 'uint' ),
38
+ maxSubsurfaceBounces: uniform( 256, 'uint' ),
36
39
  misEnabled: uniform( 1, 'uint' ),
37
40
  maxSamples: uniform( 0, 'uint' ),
38
41
  filterGlossy: uniform( 1 ),
@@ -42,6 +45,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
42
45
  backgroundInfo: { value: null },
43
46
 
44
47
  transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
48
+ accumulateAux: uniform( 0, 'uint' ),
49
+
50
+ // the per pixel first hit albedo and normal, as running means like the color
51
+ auxTarget: textureStore( createAuxTarget() ).toReadWrite(),
45
52
 
46
53
  textures: texture( new DataTexture() ),
47
54
  textureSampler: sampler( new DataTexture() ),
@@ -50,14 +57,16 @@ export class PathTracerMegaKernel extends ComputeKernel {
50
57
  globalId: globalId,
51
58
  };
52
59
 
53
- const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
54
- const raycastFirstHitFn = proxyFn( 'bvhData.value.fns.raycastFirstHit', params );
55
- const raycastShadowFn = proxyFn( 'bvhData.value.fns.raycastShadow', params );
56
- const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
57
- const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
58
- const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
60
+ const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
61
+ const raycastFirstHitFn = contextProxyFn( 'bvhData.fns.raycastFirstHit' );
62
+ const raycastShadowFn = contextProxyFn( 'bvhData.fns.raycastShadow' );
63
+ const sampleTrianglePointFn = contextProxyFn( 'bvhData.fns.sampleTrianglePoint' );
64
+ const getSurfaceRecordFn = contextProxyFn( 'bvhData.fns.getSurfaceRecord' );
65
+ const getCameraRayFn = contextProxyFn( 'bvhData.fns.getCameraRay' );
59
66
  const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
60
67
  const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
68
+ const materialsStorage = contextProxy( 'bvhData.storage.materials' );
69
+ const transformsStorage = contextProxy( 'bvhData.storage.transforms' );
61
70
 
62
71
  // environment resources
63
72
  const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
@@ -92,6 +101,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
92
101
  seed: u32,
93
102
  maxBounces: u32,
94
103
  maxTransparentBounces: u32,
104
+ maxSubsurfaceBounces: u32,
95
105
  misEnabled: u32,
96
106
  maxSamples: u32,
97
107
  filterGlossy: f32,
@@ -99,12 +109,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
99
109
  clampIndirect: f32,
100
110
 
101
111
  transmissiveBackground: u32,
112
+ accumulateAux: u32,
102
113
 
103
114
  ) -> void {
104
115
 
105
- let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
106
- let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
107
-
108
116
  // make sure we don't bleed over the edge of our tile
109
117
  if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
110
118
 
@@ -137,6 +145,13 @@ export class PathTracerMegaKernel extends ComputeKernel {
137
145
  var ray: ${ rayStruct };
138
146
  if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
139
147
 
148
+ // a declined pixel never takes a sample, so with a cap it counts as finished
149
+ if ( maxSamples != 0u ) {
150
+
151
+ textureStore( ${ params.sampleCountTarget }, indexUV, vec4( maxSamples ) );
152
+
153
+ }
154
+
140
155
  return;
141
156
 
142
157
  }
@@ -146,9 +161,22 @@ export class PathTracerMegaKernel extends ComputeKernel {
146
161
  var resultColor = vec4f( 0, 0, 0, 1 );
147
162
  var throughputColor = vec3f( 1.0 );
148
163
  var bsdfPdf = 0.0;
164
+
165
+ // Alpha pass throughs only move where the BVH traversal starts, "rayStart" along the ray, so the
166
+ // ray keeps the origin it scattered from, which its MIS weights are measured from
167
+ var rayStart = 0.0;
168
+ var traceOrigin = ray.origin;
149
169
  var isFullyTransmissive = true;
150
170
  var minPdf = 1.0;
151
171
  var transparentBounce = 0u;
172
+
173
+ // the scattering volume the path is in. Media don't nest
174
+ var medium: ${ mediumStruct };
175
+ var subsurfaceBounces = 0u;
176
+
177
+ // no light is sampled at a scatter event, so whatever the next segment reaches takes full weight
178
+ var mediumScattered = false;
179
+
152
180
  // A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
153
181
  var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
154
182
 
@@ -166,14 +194,81 @@ export class PathTracerMegaKernel extends ComputeKernel {
166
194
  var lightHits = vec3f( 0.0 );
167
195
  var cameraHitLight = false;
168
196
 
197
+ // The first surface the path scatters off fills the denoiser's auxiliary images. Until one is found
198
+ // the last mirror or transmission hit stands in, for paths that reach the environment. The albedo
199
+ // is tinted by the throughput of the surfaces looked past to reach it
200
+ var auxFound = false;
201
+ var auxAlbedo = vec3f( 0.0 );
202
+ var auxNormal = vec3f( 0.0 );
203
+
169
204
  for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
170
205
 
206
+ // Inside a medium, sample a scatter event before the next surface and only trace up to it
207
+ var channelPdf = vec3f( 0.0 );
208
+ var scatterDist = 0.0;
209
+ var traceRay = ray;
210
+ traceRay.origin = traceOrigin;
211
+ if ( medium.inside ) {
212
+
213
+ channelPdf = ${ mediumChannelPdfFn }( throughputColor, medium.albedo );
214
+ scatterDist = ${ sampleMediumDistanceFn }( medium, channelPdf, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DISTANCE } ) );
215
+ traceRay.maxDist = select( scatterDist, min( ray.maxDist, scatterDist ), ray.maxDist > 0.0 );
216
+
217
+ }
218
+
171
219
  var hitResult: ${ raycastOutput };
172
- let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
220
+ let didHit = ${ raycastFirstHitFn }( traceRay, &hitResult );
173
221
  let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
174
222
 
175
- // NEE's total weight at the vertex this ray left, for MIS. Camera rays have none
176
- let isMISWeighted = misEnabled != 0u && bounce > 0u;
223
+ let segmentInMedium = medium.inside;
224
+ if ( medium.inside ) {
225
+
226
+ let scattered = scatterDist < surfaceDist;
227
+ let segmentDist = select( surfaceDist, scatterDist, scattered );
228
+ let transmittance = exp( - medium.extinction * segmentDist );
229
+ let distancePdf = dot( channelPdf, ${ mediumDistancePdfFn }( medium, segmentDist, scattered ) );
230
+ if ( distancePdf <= 0.0 ) {
231
+
232
+ break;
233
+
234
+ }
235
+
236
+ if ( ! scattered ) {
237
+
238
+ // reaching the surface, weighted by the probability of no scatter event before it
239
+ throughputColor *= transmittance / distancePdf;
240
+
241
+ } else {
242
+
243
+ throughputColor *= medium.albedo * medium.extinction * transmittance / distancePdf;
244
+ ray.origin = traceOrigin + ray.direction * scatterDist;
245
+ traceOrigin = ray.origin;
246
+ rayStart = 0.0;
247
+ ray.direction = ${ sampleHenyeyGreensteinFn }( ray.direction, medium.anisotropy, ${ rand2 }( ${ RNG_INDEX_MEDIUM_DIRECTION } ) );
248
+ ray.maxDist = 0.0;
249
+ mediumScattered = true;
250
+ isFullyTransmissive = false;
251
+
252
+ // subsurface bounces are capped separately from bounces
253
+ subsurfaceBounces ++;
254
+ let maxThroughput = max( max( throughputColor.r, throughputColor.g ), throughputColor.b );
255
+ if ( subsurfaceBounces >= maxSubsurfaceBounces || maxThroughput < ${ MEDIUM_THROUGHPUT_EPSILON } ) {
256
+
257
+ break;
258
+
259
+ }
260
+
261
+ ${ rngNextBounce }();
262
+ bounce --;
263
+ continue;
264
+
265
+ }
266
+
267
+ }
268
+
269
+ // NEE's total weight at the vertex this ray left, for MIS. Camera rays have none, and
270
+ // neither do scatter events in a medium
271
+ let isMISWeighted = misEnabled != 0u && bounce > 0u && ! mediumScattered;
177
272
  var lightTotalWeight = 0.0;
178
273
  if ( isMISWeighted ) {
179
274
 
@@ -181,7 +276,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
181
276
 
182
277
  }
183
278
 
184
- // forward hits on area lights. Camera rays only see visibleToCameraRays lights, at full weight
279
+ // forward hits on area lights along the traced segment. Camera rays only see visibleToCameraRays
280
+ // lights, at full weight
185
281
  for ( var li = 0u; li < lightsCount; li ++ ) {
186
282
 
187
283
  if ( bounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
@@ -191,7 +287,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
191
287
  }
192
288
 
193
289
  var lightRec: ${ lightRecordStruct };
194
- if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
290
+ if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist > rayStart && lightRec.dist < rayStart + surfaceDist ) {
195
291
 
196
292
  var misWeight = 1.0;
197
293
  if ( isMISWeighted ) {
@@ -210,8 +306,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
210
306
 
211
307
  if ( didHit ) {
212
308
 
213
- let objectInfo = transforms[ hitResult.objectIndex ];
214
- var materialInfo = ( *materials )[ objectInfo.materialIndex ];
309
+ // whole structs are read from the buffer, not a pointer alias, which Safari fails to compile
310
+ let objectInfo = ${ transformsStorage }[ hitResult.objectIndex ];
311
+ var materialInfo = ${ materialsStorage }[ objectInfo.materialIndex ];
215
312
 
216
313
  // a matte surface hit by the camera ray renders as a fully transparent
217
314
  if ( materialInfo.matte != 0 && bounce == 0u ) {
@@ -239,7 +336,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
239
336
  var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
240
337
 
241
338
  // Stochastically pass through partially transparent surfaces by restarting
242
- // the ray at the hit point, advancing the rng but not the bounce count.
339
+ // the traversal at the hit point, advancing the rng but not the bounce count.
243
340
  if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
244
341
 
245
342
  // stop once the transparent bounces run out
@@ -249,7 +346,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
249
346
 
250
347
  }
251
348
 
252
- ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
349
+ traceOrigin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
350
+ rayStart += hitResult.dist;
253
351
  ${ rngNextBounce }();
254
352
  transparentBounce ++;
255
353
  bounce --;
@@ -271,8 +369,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
271
369
 
272
370
  }
273
371
 
274
- // attenuate the light transmitted through the volume when exiting a backface
275
- if ( hitResult.side < 0.0 && materialInfo.transmission > 0.0 ) {
372
+ // attenuate the light transmitted through the volume when exiting a backface. A scattering
373
+ // medium has already applied its transmittance above
374
+ if ( hitResult.side < 0.0 && materialInfo.transmission > 0.0 && ! segmentInMedium ) {
276
375
 
277
376
  throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
278
377
 
@@ -318,20 +417,20 @@ export class PathTracerMegaKernel extends ComputeKernel {
318
417
  shadowRay.direction = lightRec.direction;
319
418
  shadowRay.maxDist = lightRec.dist * ( 1.0 - ${ LIGHT_EPSILON } );
320
419
 
321
- // opaque occlusion up to the light distance. A shadow-specific any hit
322
- // traversal could support tinted shadows from transmissive and partially
323
- // opaque objects
420
+ // opaque occlusion up to the light distance, attenuated by the partially
421
+ // transparent surfaces passed
324
422
  var shadowHit: ${ raycastOutput };
325
423
  let occluded = ${ raycastShadowFn }( shadowRay, &shadowHit );
326
424
  if ( ! occluded ) {
327
425
 
328
426
  let lightPdf = lightRec.pdf * selectionPdf;
329
427
 
330
- // env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight,
331
- // or when a bsdf ray could never reach the light
332
- let isMISWeighted = ${ isMISWeightLightFn }( lightRec.lightType ) && ! shadowHit.forwardBlocked;
333
- let misWeight = select( 1.0, ${ misHeuristicFn }( lightPdf, evalRec.pdf ), isMISWeighted );
334
- let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight / lightPdf;
428
+ // env + area lights are also found by bsdf rays, so MIS-weight them. Punctual lights, and lights
429
+ // past the bounce limit, take full weight. Bsdf rays stopped by faces the shadow ray skipped can't
430
+ // find the light, so their share of the weight moves to the light sample
431
+ let isMISWeighted = ${ isMISWeightLightFn }( lightRec.lightType ) && bounce + 1u < maxBounces;
432
+ let misWeight = select( 1.0, mix( ${ misHeuristicFn }( lightPdf, evalRec.pdf ), 1.0, shadowHit.skippedOcclusion ), isMISWeighted );
433
+ let directLight = throughputColor * lightRec.emission * evalRec.color * misWeight * ( 1.0 - shadowHit.occlusion ) / lightPdf;
335
434
  let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
336
435
  resultColor += vec4f( contribution, 0.0 );
337
436
 
@@ -346,6 +445,15 @@ export class PathTracerMegaKernel extends ComputeKernel {
346
445
  }
347
446
 
348
447
  let scatterRec = ${ bsdfSampleFn }( view, surface );
448
+
449
+ if ( ! auxFound ) {
450
+
451
+ auxFound = ${ isAuxSurfaceFn }( surface, scatterRec );
452
+ auxAlbedo = saturate( throughputColor * surface.color );
453
+ auxNormal = surface.normal;
454
+
455
+ }
456
+
349
457
  if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
350
458
 
351
459
  break;
@@ -376,6 +484,28 @@ export class PathTracerMegaKernel extends ComputeKernel {
376
484
  throughputColor *= scatterRec.color;
377
485
  throughputColor /= scatterRec.pdf;
378
486
  bsdfPdf = scatterRec.pdf;
487
+ mediumScattered = false;
488
+
489
+ // As in "KHR_materials_scatter", the medium's surface bsdf is its boundary: the walk ends when
490
+ // light refracts out through it, and continues inside when it reflects back
491
+ if ( segmentInMedium && hitResult.objectIndex == medium.object && hitResult.side < 0.0 && scatterRec.isTransmissive ) {
492
+
493
+ medium.inside = false;
494
+
495
+ }
496
+
497
+ // refracting into a front face of a scattering material starts a random walk inside it
498
+ if ( scatterRec.isTransmissive && ! surface.thinWall && hitResult.side > 0.0 ) {
499
+
500
+ let entered = ${ enterMediumFn }( materialInfo, hitResult.objectIndex );
501
+
502
+ if ( entered.inside ) {
503
+
504
+ medium = entered;
505
+
506
+ }
507
+
508
+ }
379
509
 
380
510
  // exit if our throughput is 0.0
381
511
  if ( all( throughputColor == vec3f( 0.0 ) ) ) {
@@ -387,6 +517,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
387
517
  ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
388
518
  ray.direction = scatterRec.direction;
389
519
  ray.maxDist = 0.0;
520
+ traceOrigin = ray.origin;
521
+ rayStart = 0.0;
390
522
 
391
523
  } else {
392
524
 
@@ -490,6 +622,25 @@ export class PathTracerMegaKernel extends ComputeKernel {
490
622
  textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
491
623
  textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
492
624
 
625
+ // the auxiliary images are blended the same way, in the same row order
626
+ if ( accumulateAux != 0u ) {
627
+
628
+ var auxPacked: vec3u;
629
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
630
+
631
+ auxPacked[ layer ] = textureLoad( ${ params.auxTarget }, colorIndex, layer ).r;
632
+
633
+ }
634
+
635
+ auxPacked = ${ blendAuxFn }( auxPacked, auxAlbedo, auxNormal, 1.0 / f32( nextSampleCount ) );
636
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
637
+
638
+ textureStore( ${ params.auxTarget }, colorIndex, layer, vec4u( auxPacked[ layer ] ) );
639
+
640
+ }
641
+
642
+ }
643
+
493
644
  }`;
494
645
 
495
646
  super( shader( params ) );
@@ -37,11 +37,11 @@ export class PrimeSampleCountersKernel extends ComputeKernel {
37
37
  const fn = wgslTagFn/* wgsl */`
38
38
  fn compute() -> void {
39
39
 
40
- atomicStore( &${ params.counters }.minSamples, 0xFFFFFFFFu );
41
- atomicStore( &${ params.counters }.maxSamples, 0u );
42
- atomicStore( &${ params.counters }.totalLo, 0u );
43
- atomicStore( &${ params.counters }.totalHi, 0u );
44
- atomicStore( &${ params.counters }.pixelCount, 0u );
40
+ atomicStore( &${ params.counters }[ 0 ].minSamples, 0xFFFFFFFFu );
41
+ atomicStore( &${ params.counters }[ 0 ].maxSamples, 0u );
42
+ atomicStore( &${ params.counters }[ 0 ].totalLo, 0u );
43
+ atomicStore( &${ params.counters }[ 0 ].totalHi, 0u );
44
+ atomicStore( &${ params.counters }[ 0 ].pixelCount, 0u );
45
45
 
46
46
  }
47
47
  `;
@@ -86,16 +86,16 @@ export class TallySampleCountsKernel extends ComputeKernel {
86
86
 
87
87
  let samples = combinedField & ${ SAMPLE_COUNT_MASK }u;
88
88
 
89
- atomicMin( &${ params.counters }.minSamples, samples );
90
- atomicMax( &${ params.counters }.maxSamples, samples );
91
- atomicAdd( &${ params.counters }.pixelCount, 1u );
89
+ atomicMin( &${ params.counters }[ 0 ].minSamples, samples );
90
+ atomicMax( &${ params.counters }[ 0 ].maxSamples, samples );
91
+ atomicAdd( &${ params.counters }[ 0 ].pixelCount, 1u );
92
92
 
93
93
  // the sum overflowed if it came out smaller than the value already there, so carry
94
94
  // the wrap into "totalHi"
95
- let previousTotal = atomicAdd( &${ params.counters }.totalLo, samples );
95
+ let previousTotal = atomicAdd( &${ params.counters }[ 0 ].totalLo, samples );
96
96
  if ( previousTotal + samples < previousTotal ) {
97
97
 
98
- atomicAdd( &${ params.counters }.totalHi, 1u );
98
+ atomicAdd( &${ params.counters }[ 0 ].totalHi, 1u );
99
99
 
100
100
  }
101
101
 
@@ -0,0 +1,48 @@
1
+ import { ComputeKernel } from './ComputeKernel.js';
2
+ import { textureStore, globalId } from 'three/tsl';
3
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
4
+ import { createAuxImageTarget, createAuxTarget } from './targets.js';
5
+ import { unpackAuxAlbedoFn, unpackAuxNormalFn, AUX_LAYERS } from '../nodes/aux.wgsl.js';
6
+
7
+ // Unpacks the auxiliary target into separate albedo and normal images
8
+ export class UnpackAuxKernel extends ComputeKernel {
9
+
10
+ constructor() {
11
+
12
+ const params = {
13
+ auxTarget: textureStore( createAuxTarget() ).toReadOnly(),
14
+ albedoTarget: textureStore( createAuxImageTarget() ).toWriteOnly(),
15
+ normalTarget: textureStore( createAuxImageTarget() ).toWriteOnly(),
16
+ globalId: globalId,
17
+ };
18
+
19
+ const fn = wgslTagFn/* wgsl */`
20
+ fn compute( globalId: vec3u ) -> void {
21
+
22
+ let dims = textureDimensions( ${ params.auxTarget } );
23
+ if ( globalId.x >= dims.x || globalId.y >= dims.y ) {
24
+
25
+ return;
26
+
27
+ }
28
+
29
+ var packed: vec3u;
30
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
31
+
32
+ packed[ layer ] = textureLoad( ${ params.auxTarget }, globalId.xy, layer ).r;
33
+
34
+ }
35
+
36
+ textureStore( ${ params.albedoTarget }, globalId.xy, vec4f( ${ unpackAuxAlbedoFn }( packed ), 1.0 ) );
37
+ textureStore( ${ params.normalTarget }, globalId.xy, vec4f( ${ unpackAuxNormalFn }( packed ), 1.0 ) );
38
+
39
+ }
40
+ `;
41
+
42
+ super( fn( params ) );
43
+
44
+ this.defineUniformAccessors( params );
45
+
46
+ }
47
+
48
+ }
@@ -12,10 +12,12 @@ export class ZeroOutKernel extends ComputeKernel {
12
12
  outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
13
13
  };
14
14
 
15
+ // resolved when the shader is built, so an array texture target is cleared a layer per z
16
+ const coord = () => params.outputTarget.value.isArrayTexture ? 'globalId.xy, globalId.z' : 'globalId.xy';
15
17
  const fn = wgslTagFn/* wgsl */`
16
18
  fn compute( globalId: vec3u ) -> void {
17
19
 
18
- textureStore( ${ params.outputTarget }, globalId.xy, vec4( 0, 0, 0, 1 ) );
20
+ textureStore( ${ params.outputTarget }, ${ coord }, vec4( 0, 0, 0, 1 ) );
19
21
 
20
22
  }
21
23
  `;
@@ -0,0 +1,55 @@
1
+ import { ColorManagement, FloatType, HalfFloatType, RGBAFormat } from 'three';
2
+ import { RedIntegerFormat, StorageArrayTexture, StorageTexture, UnsignedIntType } from 'three/webgpu';
3
+ import { AUX_LAYERS } from '../nodes/aux.wgsl.js';
4
+
5
+ // The storage textures the backends accumulate into. The kernels are created with these too, so
6
+ // their pipelines are compiled against the formats they are dispatched with.
7
+
8
+ // the color accumulation, in the working color space
9
+ export function createOutputTarget( name = '' ) {
10
+
11
+ const target = new StorageTexture( 1, 1 );
12
+ target.format = RGBAFormat;
13
+ target.type = FloatType;
14
+ target.colorSpace = ColorManagement.workingColorSpace;
15
+ target.name = name;
16
+ target.generateMipmaps = false;
17
+ return target;
18
+
19
+ }
20
+
21
+ // the per pixel sample count and its flags
22
+ export function createSampleCountTarget( name = '' ) {
23
+
24
+ const target = new StorageTexture( 1, 1 );
25
+ target.format = RedIntegerFormat;
26
+ target.type = UnsignedIntType;
27
+ target.name = name;
28
+ target.generateMipmaps = false;
29
+ return target;
30
+
31
+ }
32
+
33
+ // the denoiser's first hit albedo and normal, packed as halves into the layers. See "aux.wgsl.js"
34
+ export function createAuxTarget( name = '' ) {
35
+
36
+ const target = new StorageArrayTexture( 1, 1, AUX_LAYERS );
37
+ target.format = RedIntegerFormat;
38
+ target.type = UnsignedIntType;
39
+ target.name = name;
40
+ target.generateMipmaps = false;
41
+ return target;
42
+
43
+ }
44
+
45
+ // an albedo or normal image unpacked from the auxiliary target, for the denoiser
46
+ export function createAuxImageTarget( name = '' ) {
47
+
48
+ const target = new StorageTexture( 1, 1 );
49
+ target.format = RGBAFormat;
50
+ target.type = HalfFloatType;
51
+ target.name = name;
52
+ target.generateMipmaps = false;
53
+ return target;
54
+
55
+ }