three-gpu-pathtracer 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +4 -2
  2. package/build/index.module.js +23 -5
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +22 -4
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +8 -8
  7. package/src/core/PathTracingSceneGenerator.js +1 -1
  8. package/src/core/utils/sceneUpdateUtils.js +2 -1
  9. package/src/uniforms/EquirectHdrInfoUniform.js +4 -2
  10. package/src/uniforms/LightsInfoUniformStruct.js +16 -1
  11. package/src/webgpu/API.md +167 -19
  12. package/src/webgpu/AtlasTexture.js +32 -12
  13. package/src/webgpu/EquirectHdrInfoNode.js +97 -46
  14. package/src/webgpu/LightsInfoNode.js +142 -4
  15. package/src/webgpu/MegaKernelPathTracer.js +11 -3
  16. package/src/webgpu/PathTracerBackend.js +174 -21
  17. package/src/webgpu/TurquinTexture.js +2 -0
  18. package/src/webgpu/WaveFrontPathTracer.js +169 -71
  19. package/src/webgpu/WebGPUPathTracer.js +637 -51
  20. package/src/webgpu/compute/ComputeKernel.js +18 -0
  21. package/src/webgpu/compute/PathTracerMegaKernel.js +253 -71
  22. package/src/webgpu/compute/TallySampleCountsKernel.js +10 -10
  23. package/src/webgpu/compute/UnpackAuxKernel.js +48 -0
  24. package/src/webgpu/compute/ZeroOutKernel.js +3 -1
  25. package/src/webgpu/compute/targets.js +55 -0
  26. package/src/webgpu/compute/wavefront/LogicKernel.js +136 -55
  27. package/src/webgpu/compute/wavefront/MaterialKernel.js +153 -66
  28. package/src/webgpu/compute/wavefront/MediumKernel.js +154 -0
  29. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +5 -5
  30. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +5 -5
  31. package/src/webgpu/compute/wavefront/QueueMediumWalksKernel.js +52 -0
  32. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +6 -6
  33. package/src/webgpu/compute/wavefront/TraceRayKernel.js +11 -9
  34. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +18 -23
  35. package/src/webgpu/compute/wavefront/structs.js +63 -35
  36. package/src/webgpu/constants.js +2 -2
  37. package/src/webgpu/denoise/OIDNDenoiser.js +81 -78
  38. package/src/webgpu/index.d.ts +79 -6
  39. package/src/webgpu/materials/GltfCompliantMaterial.js +20 -6
  40. package/src/webgpu/materials/debug/AuxDebugMaterial.js +54 -0
  41. package/src/webgpu/nodes/PathtracerBVHComputeData.js +109 -17
  42. package/src/webgpu/nodes/aux.wgsl.js +61 -0
  43. package/src/webgpu/nodes/eon.wgsl.js +1 -1
  44. package/src/webgpu/nodes/lights.wgsl.js +21 -1
  45. package/src/webgpu/nodes/material.wgsl.js +2 -1
  46. package/src/webgpu/nodes/medium.wgsl.js +165 -0
  47. package/src/webgpu/nodes/random.wgsl.js +2 -0
  48. package/src/webgpu/nodes/sampling.wgsl.js +44 -3
  49. package/src/webgpu/nodes/structs.wgsl.js +8 -1
  50. package/src/webgpu/upscale/FSRUpscaler.js +3 -2
@@ -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, {
@@ -73,11 +81,21 @@ export class ComputeKernel {
73
81
 
74
82
  // forward the call parameters so they remain accessible via "computeNode.parameters"
75
83
  node.parameters = this._fn.parameters;
84
+
85
+ // release the renderer's pipeline and bindings for the kernel being replaced
86
+ this.kernel?.dispose();
76
87
  this.kernel = node.computeKernel( [ x, y, z ] );
77
88
  return this;
78
89
 
79
90
  }
80
91
 
92
+ dispose() {
93
+
94
+ this.kernel?.dispose();
95
+ this.kernel = null;
96
+
97
+ }
98
+
81
99
  getDispatchSize( tx = 1, ty = 1, tz = 1, target = [] ) {
82
100
 
83
101
  const [ wgx, wgy, wgz ] = this.workgroupSize;
@@ -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,13 +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 sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
56
- const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
57
- 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' );
58
66
  const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
59
67
  const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
68
+ const materialsStorage = contextProxy( 'bvhData.storage.materials' );
69
+ const transformsStorage = contextProxy( 'bvhData.storage.transforms' );
60
70
 
61
71
  // environment resources
62
72
  const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
@@ -64,12 +74,17 @@ export class PathTracerMegaKernel extends ComputeKernel {
64
74
  const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
65
75
  const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
66
76
  const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
77
+ const getEnvWeight = proxy( 'envInfo.value.getWeight', params );
67
78
  const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
68
79
 
69
80
  // analytic scene lights pulled off the lightsInfo provider ( LightsInfoNode )
70
81
  const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
71
82
  const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
72
83
  const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
84
+ const getLightWeightFn = proxyFn( 'lightsInfo.value.getLightWeight', params );
85
+ const getLightsWeightFn = proxyFn( 'lightsInfo.value.getLightsWeight', params );
86
+ const selectLightFn = proxyFn( 'lightsInfo.value.selectLight', params );
87
+ const isLightVisibleToCameraFn = proxyFn( 'lightsInfo.value.isLightVisibleToCamera', params );
73
88
 
74
89
  const shader = wgslTagFn/* wgsl */`
75
90
 
@@ -86,6 +101,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
86
101
  seed: u32,
87
102
  maxBounces: u32,
88
103
  maxTransparentBounces: u32,
104
+ maxSubsurfaceBounces: u32,
89
105
  misEnabled: u32,
90
106
  maxSamples: u32,
91
107
  filterGlossy: f32,
@@ -93,12 +109,10 @@ export class PathTracerMegaKernel extends ComputeKernel {
93
109
  clampIndirect: f32,
94
110
 
95
111
  transmissiveBackground: u32,
112
+ accumulateAux: u32,
96
113
 
97
114
  ) -> void {
98
115
 
99
- let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
100
- let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
101
-
102
116
  // make sure we don't bleed over the edge of our tile
103
117
  if ( globalId.x >= tileSize.x || globalId.y >= tileSize.y ) {
104
118
 
@@ -131,6 +145,13 @@ export class PathTracerMegaKernel extends ComputeKernel {
131
145
  var ray: ${ rayStruct };
132
146
  if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
133
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
+
134
155
  return;
135
156
 
136
157
  }
@@ -140,59 +161,154 @@ export class PathTracerMegaKernel extends ComputeKernel {
140
161
  var resultColor = vec4f( 0, 0, 0, 1 );
141
162
  var throughputColor = vec3f( 1.0 );
142
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;
143
169
  var isFullyTransmissive = true;
144
170
  var minPdf = 1.0;
145
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
+
146
180
  // A negative value marks a sampled wavelength whose RGB reconstruction weight has not been applied yet.
147
181
  var dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
148
182
 
149
- // one-sample NEE selects between the analytic lights and the environment -
150
- // lightsDenom is the number of options
183
+ // one-sample NEE chooses between the analytic lights and the environment
151
184
  let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
152
185
  let lightsCount = ${ lightsCountNode };
153
- var lightsDenom = f32( lightsCount );
186
+ var envWeight = 0.0;
154
187
  if ( envActive ) {
155
188
 
156
- lightsDenom += 1.0;
189
+ envWeight = ${ getEnvWeight }();
157
190
 
158
191
  }
159
192
 
193
+ // added last, since a camera ray miss overwrites the pixel with the background
194
+ var lightHits = vec3f( 0.0 );
195
+ var cameraHitLight = false;
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
+
160
204
  for ( var bounce = 0u; bounce < maxBounces; bounce ++ ) {
161
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
+
162
219
  var hitResult: ${ raycastOutput };
163
- let didHit = ${ raycastFirstHitFn }( ray, &hitResult );
220
+ let didHit = ${ raycastFirstHitFn }( traceRay, &hitResult );
164
221
  let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
165
222
 
166
- // forward hits: a bsdf-sampled ray that lands on a area light. MIS-weighted
167
- // only when NEE is also sampling the lights
168
- if ( bounce > 0u ) {
223
+ let segmentInMedium = medium.inside;
224
+ if ( medium.inside ) {
169
225
 
170
- for ( var li = 0u; li < lightsCount; li ++ ) {
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 ) {
171
231
 
172
- var lightRec: ${ lightRecordStruct };
173
- if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
232
+ break;
174
233
 
175
- var misWeight = 1.0;
176
- if ( misEnabled != 0u ) {
234
+ }
177
235
 
178
- let lightPdf = lightRec.pdf / lightsDenom;
179
- misWeight = ${ misHeuristicFn }( bsdfPdf, lightPdf );
236
+ if ( ! scattered ) {
180
237
 
181
- }
238
+ // reaching the surface, weighted by the probability of no scatter event before it
239
+ throughputColor *= transmittance / distancePdf;
240
+
241
+ } else {
182
242
 
183
- let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
184
- resultColor += vec4f( lightHit, 0.0 );
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;
185
258
 
186
259
  }
187
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;
272
+ var lightTotalWeight = 0.0;
273
+ if ( isMISWeighted ) {
274
+
275
+ lightTotalWeight = ${ getLightsWeightFn }( ray.origin ) + envWeight;
276
+
277
+ }
278
+
279
+ // forward hits on area lights along the traced segment. Camera rays only see visibleToCameraRays
280
+ // lights, at full weight
281
+ for ( var li = 0u; li < lightsCount; li ++ ) {
282
+
283
+ if ( bounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
284
+
285
+ continue;
286
+
287
+ }
288
+
289
+ var lightRec: ${ lightRecordStruct };
290
+ if ( ${ intersectLightAtIndexFn }( ray.origin, ray.direction, li, &lightRec ) && lightRec.dist > rayStart && lightRec.dist < rayStart + surfaceDist ) {
291
+
292
+ var misWeight = 1.0;
293
+ if ( isMISWeighted ) {
294
+
295
+ let selectionPdf = ${ getLightWeightFn }( li, ray.origin ) / lightTotalWeight;
296
+ misWeight = ${ misHeuristicFn }( bsdfPdf, lightRec.pdf * selectionPdf );
297
+
298
+ }
299
+
300
+ lightHits += ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, bounce, clampDirect, clampIndirect );
301
+ cameraHitLight = cameraHitLight || bounce == 0u;
302
+
188
303
  }
189
304
 
190
305
  }
191
306
 
192
307
  if ( didHit ) {
193
308
 
194
- let objectInfo = transforms[ hitResult.objectIndex ];
195
- 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 ];
196
312
 
197
313
  // a matte surface hit by the camera ray renders as a fully transparent
198
314
  if ( materialInfo.matte != 0 && bounce == 0u ) {
@@ -220,7 +336,7 @@ export class PathTracerMegaKernel extends ComputeKernel {
220
336
  var surface = ${ getSurfaceRecordFn }( materialInfo, vertexData, hitResult.side, hitResult.normal, view, blurRoughness );
221
337
 
222
338
  // Stochastically pass through partially transparent surfaces by restarting
223
- // 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.
224
340
  if ( ${ rand1 }( ${ RNG_INDEX_ALPHA_TEST } ) > surface.opacity ) {
225
341
 
226
342
  // stop once the transparent bounces run out
@@ -230,7 +346,8 @@ export class PathTracerMegaKernel extends ComputeKernel {
230
346
 
231
347
  }
232
348
 
233
- ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
349
+ traceOrigin = ${ offsetRayOriginFunc }( vertexData.position.xyz, ray.direction, hitResult.normal );
350
+ rayStart += hitResult.dist;
234
351
  ${ rngNextBounce }();
235
352
  transparentBounce ++;
236
353
  bounce --;
@@ -252,8 +369,9 @@ export class PathTracerMegaKernel extends ComputeKernel {
252
369
 
253
370
  }
254
371
 
255
- // attenuate the light transmitted through the volume when exiting a backface
256
- 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 ) {
257
375
 
258
376
  throughputColor *= ${ transmissionAttenuationFunc }( hitResult.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
259
377
 
@@ -263,30 +381,31 @@ export class PathTracerMegaKernel extends ComputeKernel {
263
381
  let emission = ${ clampPathContributionFunc }( throughputColor * surface.emission, bounce, clampDirect, clampIndirect );
264
382
  resultColor += vec4f( emission, 0.0 );
265
383
 
266
- // next event estimation: draw one light or the environment, each with
267
- // probability 1 / lightsDenom
268
- if ( misEnabled != 0u && lightsDenom > 0.0 ) {
384
+ // next event estimation: choose one light or the environment by its estimated
385
+ // contribution
386
+ if ( misEnabled != 0u ) {
269
387
 
270
- // pick one light or the environment with a single sample
271
- // TODO: importance-sample the selection by light intensity and solid angle
272
388
  let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
273
- let lightIndex = min( u32( ruv.x * lightsDenom ), u32( lightsDenom ) - 1u );
274
- var lightRec: ${ lightRecordStruct };
275
- if ( envActive && lightIndex == lightsCount ) {
276
-
277
- // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
278
- let envSample = ${ sampleEnvDir }( ruv.yz );
279
- lightRec.direction = envSample.direction;
280
- lightRec.emission = envSample.color;
281
- lightRec.pdf = envSample.pdf;
282
- lightRec.dist = ${ LIGHT_FAR_DISTANCE };
283
- lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
389
+ var selectionPdf = 0.0;
390
+ let lightIndex = ${ selectLightFn }( vertexData.position.xyz, envWeight, ruv.x, &selectionPdf );
391
+ if ( selectionPdf > 0.0 ) {
284
392
 
285
- } else {
393
+ var lightRec: ${ lightRecordStruct };
394
+ if ( envActive && lightIndex == lightsCount ) {
286
395
 
287
- lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
396
+ // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
397
+ let envSample = ${ sampleEnvDir }( ruv.yz );
398
+ lightRec.direction = envSample.direction;
399
+ lightRec.emission = envSample.color;
400
+ lightRec.pdf = envSample.pdf;
401
+ lightRec.dist = ${ LIGHT_FAR_DISTANCE };
402
+ lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
288
403
 
289
- }
404
+ } else {
405
+
406
+ lightRec = ${ randomLightSampleFn }( lightIndex, vertexData.position.xyz, ruv.yz );
407
+
408
+ }
290
409
 
291
410
  if ( lightRec.pdf > 0.0 ) {
292
411
 
@@ -296,21 +415,22 @@ export class PathTracerMegaKernel extends ComputeKernel {
296
415
  var shadowRay: ${ rayStruct };
297
416
  shadowRay.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, lightRec.direction, hitResult.normal );
298
417
  shadowRay.direction = lightRec.direction;
299
- shadowRay.maxDist = lightRec.dist - ${ LIGHT_EPSILON };
418
+ shadowRay.maxDist = lightRec.dist * ( 1.0 - ${ LIGHT_EPSILON } );
300
419
 
301
- // opaque occlusion up to the light distance. A shadow-specific any hit
302
- // traversal could support tinted shadows from transmissive and partially
303
- // opaque objects
420
+ // opaque occlusion up to the light distance, attenuated by the partially
421
+ // transparent surfaces passed
304
422
  var shadowHit: ${ raycastOutput };
305
- let occluded = ${ raycastFirstHitFn }( shadowRay, &shadowHit );
423
+ let occluded = ${ raycastShadowFn }( shadowRay, &shadowHit );
306
424
  if ( ! occluded ) {
307
425
 
308
- var lightPdf = lightRec.pdf;
309
- lightPdf /= lightsDenom;
426
+ let lightPdf = lightRec.pdf * selectionPdf;
310
427
 
311
- // env + area lights are also bsdf-sampled, so MIS-weight them - punctual lights take full weight
312
- let misWeight = select( 1.0, ${ misHeuristicFn }( lightPdf, evalRec.pdf ), ${ isMISWeightLightFn }( lightRec.lightType ) );
313
- 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;
314
434
  let contribution = ${ clampPathContributionFunc }( directLight, bounce + 1u, clampDirect, clampIndirect );
315
435
  resultColor += vec4f( contribution, 0.0 );
316
436
 
@@ -320,9 +440,20 @@ export class PathTracerMegaKernel extends ComputeKernel {
320
440
 
321
441
  }
322
442
 
443
+ }
444
+
323
445
  }
324
446
 
325
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
+
326
457
  if ( ${ isTerminatingScatterFunc }( scatterRec ) ) {
327
458
 
328
459
  break;
@@ -353,6 +484,28 @@ export class PathTracerMegaKernel extends ComputeKernel {
353
484
  throughputColor *= scatterRec.color;
354
485
  throughputColor /= scatterRec.pdf;
355
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
+ }
356
509
 
357
510
  // exit if our throughput is 0.0
358
511
  if ( all( throughputColor == vec3f( 0.0 ) ) ) {
@@ -364,16 +517,18 @@ export class PathTracerMegaKernel extends ComputeKernel {
364
517
  ray.origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, hitResult.normal );
365
518
  ray.direction = scatterRec.direction;
366
519
  ray.maxDist = 0.0;
520
+ traceOrigin = ray.origin;
521
+ rayStart = 0.0;
367
522
 
368
523
  } else {
369
524
 
370
525
  if ( bounce > 0u && ! isFullyTransmissive ) {
371
526
 
372
527
  var misWeight = 1.0;
373
- if ( misEnabled != 0u && envActive ) {
528
+ if ( isMISWeighted && envActive ) {
374
529
 
375
530
  // match the env pdf scaling used by the NEE selection so the two estimators balance
376
- let envPdf = ${ getEnvDirPdf }( ray.direction ) / lightsDenom;
531
+ let envPdf = ${ getEnvDirPdf }( ray.direction ) * envWeight / lightTotalWeight;
377
532
  misWeight = ${ misHeuristicFn }( bsdfPdf, envPdf );
378
533
 
379
534
  }
@@ -450,6 +605,14 @@ export class PathTracerMegaKernel extends ComputeKernel {
450
605
 
451
606
  }
452
607
 
608
+ // visible lights are opaque against a transparent background
609
+ resultColor += vec4f( lightHits, 0.0 );
610
+ if ( cameraHitLight ) {
611
+
612
+ resultColor.a = 1.0;
613
+
614
+ }
615
+
453
616
  let nextSampleCount = sampleCount + 1;
454
617
  // store the color rows top down to match a rasterized render target
455
618
  let colorIndex = vec2u( indexUV.x, targetDimensions.y - 1u - indexUV.y );
@@ -459,6 +622,25 @@ export class PathTracerMegaKernel extends ComputeKernel {
459
622
  textureStore( ${ params.sampleCountTarget }, indexUV, vec4( nextSampleCount ) );
460
623
  textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
461
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
+
462
644
  }`;
463
645
 
464
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
  `;