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,5 +1,7 @@
1
- import { StorageBufferAttribute, StorageTexture } from 'three/webgpu';
1
+ import { StorageBufferAttribute } from 'three/webgpu';
2
2
  import { ComputeKernel } from '../ComputeKernel.js';
3
+ import { createAuxTarget, createOutputTarget, createSampleCountTarget } from '../targets.js';
4
+ import { blendAuxFn, AUX_LAYERS } from '../../nodes/aux.wgsl.js';
3
5
  import { uniform, storage, textureStore, globalId } from 'three/tsl';
4
6
  import { proxy, proxyFn, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
7
  import { misHeuristicFn, weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js';
@@ -12,7 +14,7 @@ import {
12
14
  } from '../../nodes/random.wgsl.js';
13
15
  import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, isMISWeightLightFn } from '../../nodes/lights.wgsl.js';
14
16
  import { lightRecordStruct, scatterRecordStruct } from '../../nodes/structs.wgsl.js';
15
- import { rayDataStruct, intersectionResultStruct } from './structs.js';
17
+ import { rayDataStruct, intersectionResultStruct, shadowTraceResultStruct } from './structs.js';
16
18
  import { SAMPLE_COUNT_MASK, SAMPLE_DISPATCHED_FLAG } from '../../constants.js';
17
19
 
18
20
  // Path logic over the persistent slot pool: resolves the previous frame's shadow and bounce trace
@@ -28,9 +30,12 @@ export class LogicKernel extends ComputeKernel {
28
30
  lightsInfo: { value: null },
29
31
 
30
32
  // targets
31
- prevOutputTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
32
- outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
33
- sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
33
+ prevOutputTarget: textureStore( createOutputTarget() ).toReadOnly(),
34
+ outputTarget: textureStore( createOutputTarget() ).toWriteOnly(),
35
+ sampleCountTarget: textureStore( createSampleCountTarget() ).toReadWrite(),
36
+
37
+ // the per pixel first hit albedo and normal, as running means like the color
38
+ auxTarget: textureStore( createAuxTarget() ).toReadWrite(),
34
39
 
35
40
  // settings
36
41
  misEnabled: uniform( 1, 'uint' ),
@@ -39,10 +44,11 @@ export class LogicKernel extends ComputeKernel {
39
44
  clampDirect: uniform( 0 ),
40
45
  clampIndirect: uniform( 10 ),
41
46
  transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
47
+ accumulateAux: uniform( 0, 'uint' ),
42
48
 
43
49
  rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
44
50
  rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
45
- shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
51
+ shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), shadowTraceResultStruct ),
46
52
 
47
53
  globalId: globalId,
48
54
  };
@@ -75,6 +81,7 @@ export class LogicKernel extends ComputeKernel {
75
81
  clampDirect: f32,
76
82
  clampIndirect: f32,
77
83
  transmissiveBackground: u32,
84
+ accumulateAux: u32,
78
85
 
79
86
  globalId: vec3u
80
87
  ) -> void {
@@ -92,7 +99,8 @@ export class LogicKernel extends ComputeKernel {
92
99
  }
93
100
 
94
101
  // skip slots that have never spawned a ray
95
- let input = ( *rayDataStorage )[ index ];
102
+ // whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
103
+ let input = ${ params.rayDataStorage }[ index ];
96
104
  if ( input.rayIntersectionIndex < 0 ) {
97
105
 
98
106
  return;
@@ -100,7 +108,7 @@ export class LogicKernel extends ComputeKernel {
100
108
  }
101
109
 
102
110
  let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
103
- ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
111
+ ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth + input.subsurfaceBounces );
104
112
 
105
113
  // one-sample NEE chooses between the analytic lights and the environment, matched with
106
114
  // the megakernel
@@ -124,15 +132,16 @@ export class LogicKernel extends ComputeKernel {
124
132
  // is negative when no shadow ray was enqueued last frame
125
133
  if ( input.shadowRayIntersectionIndex >= 0 && input.lightPdf > 0.0 ) {
126
134
 
127
- let shadowHit = ( *shadowRayIntersectionsStorage )[ u32( input.shadowRayIntersectionIndex ) ];
135
+ let shadowHit = ${ params.shadowRayIntersectionsStorage }[ u32( input.shadowRayIntersectionIndex ) ];
128
136
  let occluded = shadowHit.objectIndex >= 0;
129
137
  if ( ! occluded ) {
130
138
 
131
- // env + area lights are also bsdf-sampled, so MIS-weight them; punctual take full weight,
132
- // or when a bsdf ray could never reach the light
133
- let isMISWeighted = ${ isMISWeightLightFn }( input.lightType ) && shadowHit.forwardBlocked == 0u;
134
- let misWeight = select( 1.0, ${ misHeuristicFn }( input.lightPdf, input.lightBsdfPdf ), isMISWeighted );
135
- let directLight = throughputColor * input.lightEmission * input.lightBsdf * misWeight / input.lightPdf;
139
+ // env + area lights are also found by bsdf rays, so MIS-weight them. Punctual lights, and lights
140
+ // past the bounce limit, take full weight. Bsdf rays stopped by faces the shadow ray skipped can't
141
+ // find the light, so their share of the weight moves to the light sample
142
+ let isMISWeighted = ${ isMISWeightLightFn }( input.lightType ) && input.currentBounce < maxBounces;
143
+ let misWeight = select( 1.0, mix( ${ misHeuristicFn }( input.lightPdf, input.lightBsdfPdf ), 1.0, shadowHit.skippedOcclusion ), isMISWeighted );
144
+ let directLight = throughputColor * input.lightEmission * input.lightBsdf * misWeight * ( 1.0 - shadowHit.occlusion ) / input.lightPdf;
136
145
  let contribution = ${ clampPathContributionFunc }( directLight, input.currentBounce, clampDirect, clampIndirect );
137
146
  resultColor += vec4f( contribution, 0.0 );
138
147
 
@@ -154,17 +163,33 @@ export class LogicKernel extends ComputeKernel {
154
163
  // MaterialKernel, which stages a zeroed pdf and skips the bounce trace when they fire
155
164
  var isTerminated = all( throughputColor == vec3f( 0.0 ) ) || input.currentBounce >= maxBounces || ${ isTerminatingScatterFunc }( scatterRec );
156
165
 
166
+ // apply the scatter across the traced segment, including the weight of a medium walk
157
167
  if ( ! isTerminated ) {
158
168
 
159
- // apply the scatter across the traced segment
160
169
  throughputColor *= scatterRec.color / scatterRec.pdf;
161
170
 
162
- let hitResult = ( *rayIntersectionsStorage )[ u32( input.rayIntersectionIndex ) ];
171
+ }
172
+
173
+ let isWalking = ! isTerminated && input.walkPending != 0u;
174
+ if ( isWalking ) {
175
+
176
+ // the walk hasn't reached a surface yet, so there's no hit to stage. Clear what was
177
+ // resolved and let MediumKernel accumulate the walk's weight
178
+ rayDataStorage[ index ].emission = vec3f( 0.0 );
179
+ rayDataStorage[ index ].lightPdf = 0.0;
180
+ rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
181
+ rayDataStorage[ index ].scatterColor = vec3f( 1.0 );
182
+ rayDataStorage[ index ].scatterPdf = 1.0;
183
+
184
+ } else if ( ! isTerminated ) {
185
+
186
+ let hitResult = ${ params.rayIntersectionsStorage }[ u32( input.rayIntersectionIndex ) ];
163
187
  let didHit = hitResult.objectIndex >= 0;
164
188
  let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
165
189
 
166
- // NEE's total weight at the vertex this segment left, for MIS. Camera segments have none
167
- let isMISWeighted = misEnabled != 0u && input.currentBounce > 0u;
190
+ // NEE's total weight at the vertex this segment left, for MIS. Camera segments have none,
191
+ // and neither do scatter events in a medium
192
+ let isMISWeighted = misEnabled != 0u && input.currentBounce > 0u && input.mediumScattered == 0u;
168
193
  var lightTotalWeight = 0.0;
169
194
  if ( isMISWeighted ) {
170
195
 
@@ -172,7 +197,8 @@ export class LogicKernel extends ComputeKernel {
172
197
 
173
198
  }
174
199
 
175
- // forward hits on area lights. Camera rays only see visibleToCameraRays lights, at full weight
200
+ // forward hits on area lights along the traced segment, which starts "rayStart" along the ray.
201
+ // Camera rays only see visibleToCameraRays lights, at full weight
176
202
  for ( var li = 0u; li < lightsCount; li ++ ) {
177
203
 
178
204
  if ( input.currentBounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
@@ -182,7 +208,7 @@ export class LogicKernel extends ComputeKernel {
182
208
  }
183
209
 
184
210
  var lightRec: ${ lightRecordStruct };
185
- if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
211
+ if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist > input.rayStart && lightRec.dist < input.rayStart + surfaceDist ) {
186
212
 
187
213
  var misWeight = 1.0;
188
214
  if ( isMISWeighted ) {
@@ -355,6 +381,27 @@ export class LogicKernel extends ComputeKernel {
355
381
  textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ${ SAMPLE_DISPATCHED_FLAG }u | sampleCount ) );
356
382
  textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
357
383
 
384
+ // the auxiliary images are blended the same way, in the same row order
385
+ if ( accumulateAux != 0u ) {
386
+
387
+ var auxPacked: vec3u;
388
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
389
+
390
+ auxPacked[ layer ] = textureLoad( ${ params.auxTarget }, colorIndex, layer ).r;
391
+
392
+ }
393
+
394
+ let albedo = unpack4x8unorm( input.auxAlbedo ).rgb;
395
+ let normal = unpack4x8snorm( input.auxNormal ).xyz;
396
+ auxPacked = ${ blendAuxFn }( auxPacked, albedo, normal, 1.0 / f32( sampleCount ) );
397
+ for ( var layer = 0u; layer < ${ AUX_LAYERS }u; layer ++ ) {
398
+
399
+ textureStore( ${ params.auxTarget }, colorIndex, layer, vec4u( auxPacked[ layer ] ) );
400
+
401
+ }
402
+
403
+ }
404
+
358
405
  rayDataStorage[ index ].objectIndex = - 1;
359
406
 
360
407
  } else {
@@ -1,14 +1,20 @@
1
1
  import { Vector2 } from 'three';
2
- import { StorageBufferAttribute, StorageTexture } from 'three/webgpu';
2
+ import { StorageBufferAttribute } from 'three/webgpu';
3
3
  import { ComputeKernel } from '../ComputeKernel.js';
4
+ import { createSampleCountTarget } from '../targets.js';
4
5
  import { uniform, storage, textureStore, globalId } from 'three/tsl';
5
- import { proxy, proxyFn, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
6
+ import { proxyFn, contextProxy, contextProxyFn, rayStruct, wgslTagFn } from 'three-mesh-bvh/webgpu';
6
7
  import { rngInit, rand1, rand2, RNG_INDEX_RAY_JITTER, RNG_INDEX_ALPHA_TEST, RNG_INDEX_RUSSIAN_ROULETTE, RNG_INDEX_DISPERSION_WAVELENGTH } from '../../nodes/random.wgsl.js';
7
- import { rayDataStruct, rayQueueAtomicStruct, pixelQueueStruct } from './structs.js';
8
+ import {
9
+ rayDataStruct, traceQueuedRayStruct,
10
+ RAY_QUEUE_LENGTH, SHADOW_RAY_QUEUE_LENGTH, PIXEL_QUEUE_CURRENT, PIXEL_QUEUE_ELEMENT_COUNT, PIXEL_QUEUE_HEADER,
11
+ } from './structs.js';
12
+ import { enterMediumFn, loadMediumFn, storeMediumFn } from '../../nodes/medium.wgsl.js';
8
13
  import { SAMPLE_ACTIVE_FLAG, SAMPLE_COUNT_MASK, SAMPLE_DISPATCHED_FLAG } from '../../constants.js';
9
14
  import { applyDispersionFunc, dispersionColorWeightFunc, DISPERSION_MIN_WAVELENGTH, DISPERSION_MAX_WAVELENGTH, transmissionAttenuationFunc } from '../../nodes/material.wgsl.js';
10
15
  import { isTerminatingScatterFunc, offsetRayOriginFunc } from '../../nodes/utils.wgsl.js';
11
16
  import { LIGHT_EPSILON } from '../../nodes/lights.wgsl.js';
17
+ import { isAuxSurfaceFn } from '../../nodes/aux.wgsl.js';
12
18
 
13
19
  // Pure material evaluation and ray generation: terminated slots pull a recycled pixel and emit a
14
20
  // fresh camera ray; live slots evaluate the surface staged by LogicKernel, sample the bsdf, and
@@ -18,7 +24,6 @@ export class MaterialKernel extends ComputeKernel {
18
24
  constructor( ) {
19
25
 
20
26
  const params = {
21
- bvhData: { value: null },
22
27
  material: { value: null },
23
28
 
24
29
  seed: uniform( 0, 'uint' ),
@@ -29,21 +34,24 @@ export class MaterialKernel extends ComputeKernel {
29
34
  maxTransparentBounces: uniform( 5, 'uint' ),
30
35
  maxBounces: uniform( 5, 'uint' ),
31
36
 
32
- sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadWrite(),
37
+ sampleCountTarget: textureStore( createSampleCountTarget() ).toReadWrite(),
33
38
 
34
39
  rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
35
- rayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueAtomicStruct ),
36
- shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueAtomicStruct ),
37
- pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), pixelQueueStruct ),
40
+ rayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
41
+ shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
42
+ queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
43
+ pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
38
44
 
39
45
  globalId: globalId,
40
46
  };
41
47
 
42
- const getCameraRayFn = proxyFn( 'bvhData.value.fns.getCameraRay', params );
43
- const sampleTrianglePointFn = proxyFn( 'bvhData.value.fns.sampleTrianglePoint', params );
44
- const getSurfaceRecordFn = proxyFn( 'bvhData.value.fns.getSurfaceRecord', params );
48
+ const getCameraRayFn = contextProxyFn( 'bvhData.fns.getCameraRay' );
49
+ const sampleTrianglePointFn = contextProxyFn( 'bvhData.fns.sampleTrianglePoint' );
50
+ const getSurfaceRecordFn = contextProxyFn( 'bvhData.fns.getSurfaceRecord' );
45
51
  const bsdfSampleFn = proxyFn( 'material.value.bsdfSample', params );
46
52
  const bsdfEvalPdfFn = proxyFn( 'material.value.bsdfEvalPdf', params );
53
+ const materialsStorage = contextProxy( 'bvhData.storage.materials' );
54
+ const transformsStorage = contextProxy( 'bvhData.storage.transforms' );
47
55
 
48
56
  const fn = wgslTagFn/* wgsl */`
49
57
 
@@ -62,11 +70,9 @@ export class MaterialKernel extends ComputeKernel {
62
70
  let rayDataStorage = &${ params.rayDataStorage };
63
71
  let rayQueue = &${ params.rayQueue };
64
72
  let shadowRayQueue = &${ params.shadowRayQueue };
73
+ let queueCounters = &${ params.queueCounters };
65
74
  let pixelQueue = &${ params.pixelQueue };
66
75
 
67
- let materials = &${ proxy( 'bvhData.value.storage.materials', params ) };
68
- let transforms = &${ proxy( 'bvhData.value.storage.transforms', params ) };
69
-
70
76
  // bound by "rayCount" rather than the pool length. The dispatch rounds up to the
71
77
  // workgroup size and those extra slots hold a zeroed pixel index
72
78
  let index = globalId.x;
@@ -76,20 +82,21 @@ export class MaterialKernel extends ComputeKernel {
76
82
 
77
83
  }
78
84
 
79
- let input = ( *rayDataStorage )[ index ];
85
+ // whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
86
+ let input = ${ params.rayDataStorage }[ index ];
80
87
  if ( input.objectIndex < 0 ) {
81
88
 
82
89
  // the slot's path has terminated: recycle the pixel through the overflow queue and
83
90
  // generate a fresh camera ray
84
91
  var pixelIndex = input.pixelIndex;
85
- let elementCount = atomicLoad( &pixelQueue.elementCount );
92
+ let elementCount = atomicLoad( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ] );
86
93
  if ( elementCount > 0u ) {
87
94
 
88
95
  // TODO: If we've pulled off a pixel that's already finished we currently just
89
96
  // write a no-op ray, wasting a frame. It may be better to iterate over a few
90
97
  // points in the queue to see if we can find one we can use.
91
- let queueIndex = atomicAdd( &pixelQueue.current, 1u ) % elementCount;
92
- pixelIndex = atomicExchange( &pixelQueue.elements[ queueIndex ], pixelIndex );
98
+ let queueIndex = atomicAdd( &pixelQueue[ ${ PIXEL_QUEUE_CURRENT }u ], 1u ) % elementCount;
99
+ pixelIndex = atomicExchange( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + queueIndex ], pixelIndex );
93
100
 
94
101
  }
95
102
 
@@ -116,9 +123,16 @@ export class MaterialKernel extends ComputeKernel {
116
123
  var ray: ${ rayStruct };
117
124
  if ( ! ${ getCameraRayFn }( jitteredUv, vec2f( targetDimensions ), &ray ) ) {
118
125
 
119
- // the camera declined the pixel, so leave the slot dormant for this round
126
+ // the camera declined the pixel, so leave the slot dormant for this round. With a
127
+ // sample cap the pixel counts as finished, since it will never take a sample
120
128
  // TODO: same as above - this work is a bit wasteful and leaves slots empty for
121
129
  // a frame. Is it possible to quickly skip rays outside the mask or are finished?
130
+ if ( maxSamples != 0u ) {
131
+
132
+ textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ( combinedField & ~${ SAMPLE_COUNT_MASK }u ) | maxSamples ) );
133
+
134
+ }
135
+
122
136
  rayDataStorage[ index ].pixelIndex = pixelIndex;
123
137
  rayDataStorage[ index ].rayIntersectionIndex = - 1;
124
138
  rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
@@ -128,14 +142,13 @@ export class MaterialKernel extends ComputeKernel {
128
142
 
129
143
  ray.direction = normalize( ray.direction );
130
144
 
131
- let rayIndex = atomicAdd( &rayQueue.length, 1u );
132
- rayQueue.elements[ rayIndex ].origin = ray.origin;
133
- rayQueue.elements[ rayIndex ].direction = ray.direction;
134
- rayQueue.elements[ rayIndex ].pixelIndex = pixelIndex;
135
- rayQueue.elements[ rayIndex ].currentBounce = 0u;
136
- rayQueue.elements[ rayIndex ].seed = seed + samples;
137
- rayQueue.elements[ rayIndex ].alphaDepth = 0u;
138
- rayQueue.elements[ rayIndex ].maxDist = ray.maxDist;
145
+ let rayIndex = atomicAdd( &queueCounters[ ${ RAY_QUEUE_LENGTH }u ], 1u );
146
+ rayQueue[ rayIndex ].origin = ray.origin;
147
+ rayQueue[ rayIndex ].direction = ray.direction;
148
+ rayQueue[ rayIndex ].pixelIndex = pixelIndex;
149
+ rayQueue[ rayIndex ].rngIndex = 0u;
150
+ rayQueue[ rayIndex ].seed = seed + samples;
151
+ rayQueue[ rayIndex ].maxDist = ray.maxDist;
139
152
 
140
153
  rayDataStorage[ index ].origin = ray.origin;
141
154
  rayDataStorage[ index ].direction = ray.direction;
@@ -151,22 +164,33 @@ export class MaterialKernel extends ComputeKernel {
151
164
  rayDataStorage[ index ].emission = vec3f( 0.0 );
152
165
  rayDataStorage[ index ].lightPdf = 0.0;
153
166
  rayDataStorage[ index ].alphaDepth = 0u;
167
+ rayDataStorage[ index ].rayStart = 0.0;
154
168
  rayDataStorage[ index ].maxDist = ray.maxDist;
155
169
  rayDataStorage[ index ].rayIntersectionIndex = i32( rayIndex );
156
170
  rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
157
171
  rayDataStorage[ index ].dispersionWavelength = - mix( ${ DISPERSION_MIN_WAVELENGTH }.0, ${ DISPERSION_MAX_WAVELENGTH }.0, ${ rand1 }( ${ RNG_INDEX_DISPERSION_WAVELENGTH } ) );
172
+ rayDataStorage[ index ].auxAlbedo = 0u;
173
+ rayDataStorage[ index ].auxNormal = 0u;
174
+ rayDataStorage[ index ].subsurfaceBounces = 0u;
175
+ rayDataStorage[ index ].walkPending = 0u;
176
+ rayDataStorage[ index ].mediumScattered = 0u;
177
+ rayDataStorage[ index ].medium.inside = 0u;
158
178
 
159
179
  // write the active params & dispatched flag
160
180
  textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ${ SAMPLE_ACTIVE_FLAG }u | ${ SAMPLE_DISPATCHED_FLAG }u | samples ) );
161
181
 
162
- } else {
182
+ } else if ( input.walkPending == 0u ) {
163
183
 
164
- // evaluate the surface staged by LogicKernel
184
+ // evaluate the surface staged by LogicKernel. Paths still walking a medium have none
165
185
  let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
166
- ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
186
+ ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth + input.subsurfaceBounces );
167
187
 
168
- let objectInfo = transforms[ u32( input.objectIndex ) ];
169
- var materialInfo = ( *materials )[ objectInfo.materialIndex ];
188
+ // the medium the path is in, which the bsdf sample may enter or exit
189
+ var medium = ${ loadMediumFn }( input.medium );
190
+ let segmentInMedium = medium.inside;
191
+
192
+ let objectInfo = ${ transformsStorage }[ u32( input.objectIndex ) ];
193
+ var materialInfo = ${ materialsStorage }[ objectInfo.materialIndex ];
170
194
 
171
195
  // a matte surface hit by the camera ray renders as a fully transparent
172
196
  let isMatte = materialInfo.matte != 0 && input.currentBounce == 0u;
@@ -215,16 +239,31 @@ export class MaterialKernel extends ComputeKernel {
215
239
 
216
240
  }
217
241
 
218
- let alphaIndex = atomicAdd( &rayQueue.length, 1u );
219
- rayQueue.elements[ alphaIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.direction, input.normal );
220
- rayQueue.elements[ alphaIndex ].direction = input.direction;
221
- rayQueue.elements[ alphaIndex ].pixelIndex = input.pixelIndex;
222
- rayQueue.elements[ alphaIndex ].currentBounce = input.currentBounce;
223
- rayQueue.elements[ alphaIndex ].seed = input.seed;
224
- rayQueue.elements[ alphaIndex ].alphaDepth = input.alphaDepth + 1u;
225
- // the origin advanced to the hit point, so the remaining budget shrinks by the
226
- // distance already traced
227
- rayQueue.elements[ alphaIndex ].maxDist = max( input.maxDist - input.dist, 0.0 );
242
+ let alphaOrigin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.direction, input.normal );
243
+ if ( segmentInMedium ) {
244
+
245
+ // inside a medium, MediumKernel walks on past the surface, from the surface
246
+ rayDataStorage[ index ].walkPending = 1u;
247
+ rayDataStorage[ index ].origin = alphaOrigin;
248
+ rayDataStorage[ index ].rayStart = 0.0;
249
+
250
+ } else {
251
+
252
+ let alphaIndex = atomicAdd( &queueCounters[ ${ RAY_QUEUE_LENGTH }u ], 1u );
253
+ rayQueue[ alphaIndex ].origin = alphaOrigin;
254
+ rayQueue[ alphaIndex ].direction = input.direction;
255
+ rayQueue[ alphaIndex ].pixelIndex = input.pixelIndex;
256
+ rayQueue[ alphaIndex ].rngIndex = input.currentBounce + input.alphaDepth + 1u + input.subsurfaceBounces;
257
+ rayQueue[ alphaIndex ].seed = input.seed;
258
+ // the origin advanced to the hit point, so the remaining budget shrinks by the
259
+ // distance already traced
260
+ rayQueue[ alphaIndex ].maxDist = max( input.maxDist - input.dist, 0.0 );
261
+ rayDataStorage[ index ].rayIntersectionIndex = i32( alphaIndex );
262
+
263
+ // the ray keeps its origin, and only the queued traversal starts past the surface
264
+ rayDataStorage[ index ].rayStart = input.rayStart + input.dist;
265
+
266
+ }
228
267
 
229
268
  // the surface is skipped, so no scatter or emission is staged for LogicKernel.
230
269
  // "pdf" is left alone so the previous scatter still weights the forward MIS,
@@ -233,8 +272,6 @@ export class MaterialKernel extends ComputeKernel {
233
272
  rayDataStorage[ index ].emission = vec3f( 0.0 );
234
273
  rayDataStorage[ index ].scatterColor = vec3f( input.scatterPdf );
235
274
  rayDataStorage[ index ].lightPdf = 0.0;
236
- rayDataStorage[ index ].origin = rayQueue.elements[ alphaIndex ].origin;
237
- rayDataStorage[ index ].rayIntersectionIndex = i32( alphaIndex );
238
275
  rayDataStorage[ index ].shadowRayIntersectionIndex = - 1;
239
276
  return;
240
277
 
@@ -259,8 +296,9 @@ export class MaterialKernel extends ComputeKernel {
259
296
  }
260
297
 
261
298
  // attenuate the light transmitted through the volume when exiting a backface so
262
- // the surface's emission and NEE resolve against the attenuated throughput
263
- if ( input.side < 0.0 && materialInfo.transmission > 0.0 ) {
299
+ // the surface's emission and NEE resolve against the attenuated throughput. MediumKernel
300
+ // already applied a scattering medium's transmittance
301
+ if ( input.side < 0.0 && materialInfo.transmission > 0.0 && ! segmentInMedium ) {
264
302
 
265
303
  throughputColor *= ${ transmissionAttenuationFunc }( input.dist, materialInfo.attenuationColor, materialInfo.attenuationDistance );
266
304
  rayDataStorage[ index ].throughputColor = throughputColor;
@@ -271,6 +309,20 @@ export class MaterialKernel extends ComputeKernel {
271
309
  var scatterRec = ${ bsdfSampleFn }( view, surface );
272
310
  let newBounce = input.currentBounce + 1u;
273
311
 
312
+ // The first surface the path scatters off fills the denoiser's auxiliary images. Until one is
313
+ // found the last mirror or transmission hit stands in, for paths that reach the environment.
314
+ // The albedo is tinted by the throughput of the surfaces looked past to reach it. The packed
315
+ // albedo's alpha marks the surface found
316
+ let auxFound = unpack4x8unorm( input.auxAlbedo ).a != 0.0;
317
+ if ( ! auxFound ) {
318
+
319
+ let isAuxSurface = ${ isAuxSurfaceFn }( surface, scatterRec );
320
+ let auxAlbedo = saturate( throughputColor * surface.color );
321
+ rayDataStorage[ index ].auxAlbedo = pack4x8unorm( vec4f( auxAlbedo, select( 0.0, 1.0, isAuxSurface ) ) );
322
+ rayDataStorage[ index ].auxNormal = pack4x8snorm( vec4f( surface.normal, 0.0 ) );
323
+
324
+ }
325
+
274
326
  // decide termination now so finished paths skip the bounce trace entirely - a
275
327
  // zeroed pdf reads as a terminating scatter in LogicKernel, which still resolves
276
328
  // the surface's emission and NEE before freeing the slot
@@ -301,23 +353,59 @@ export class MaterialKernel extends ComputeKernel {
301
353
  rayDataStorage[ index ].isFullyTransmissive = input.isFullyTransmissive & select( 0u, 1u, scatterRec.isTransmissive );
302
354
  rayDataStorage[ index ].emission = surface.emission;
303
355
  rayDataStorage[ index ].currentBounce = newBounce;
356
+ rayDataStorage[ index ].mediumScattered = 0u;
357
+
358
+ // As in "KHR_materials_scatter", the surface bsdf is the medium's boundary: refracting out
359
+ // ends the walk, reflecting continues it
360
+ if ( segmentInMedium && u32( input.objectIndex ) == medium.object && input.side < 0.0 && scatterRec.isTransmissive ) {
361
+
362
+ medium.inside = false;
363
+
364
+ }
365
+
366
+ // refracting into a front face of a scattering material starts a random walk inside it
367
+ if ( scatterRec.isTransmissive && ! surface.thinWall && input.side > 0.0 ) {
368
+
369
+ let entered = ${ enterMediumFn }( materialInfo, u32( input.objectIndex ) );
370
+ if ( entered.inside ) {
371
+
372
+ medium = entered;
373
+
374
+ }
375
+
376
+ }
377
+
378
+ // keep the medium the path left or entered
379
+ if ( segmentInMedium || medium.inside ) {
380
+
381
+ // a whole struct is written to the buffer, not through the pointer alias, which Safari fails to compile
382
+ ${ params.rayDataStorage }[ index ].medium = ${ storeMediumFn }( medium );
383
+
384
+ }
304
385
 
305
386
  // the NEE shadow ray below still resolves the surface's direct light, so only
306
- // the bounce segment is skipped for finished paths
307
- if ( ! isTerminated ) {
308
-
309
- let rayIndex = atomicAdd( &rayQueue.length, 1u );
310
- rayQueue.elements[ rayIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, input.normal );
311
- rayQueue.elements[ rayIndex ].direction = scatterRec.direction;
312
- rayQueue.elements[ rayIndex ].pixelIndex = input.pixelIndex;
313
- rayQueue.elements[ rayIndex ].currentBounce = newBounce;
314
- rayQueue.elements[ rayIndex ].seed = input.seed;
315
- rayQueue.elements[ rayIndex ].alphaDepth = input.alphaDepth;
316
- rayQueue.elements[ rayIndex ].maxDist = 0.0;
387
+ // the bounce segment is skipped for finished paths. Inside a medium, MediumKernel walks it
388
+ if ( ! isTerminated && medium.inside ) {
389
+
390
+ rayDataStorage[ index ].walkPending = 1u;
391
+ rayDataStorage[ index ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, input.normal );
392
+ rayDataStorage[ index ].direction = scatterRec.direction;
393
+ rayDataStorage[ index ].rayStart = 0.0;
394
+
395
+ } else if ( ! isTerminated ) {
396
+
397
+ let rayIndex = atomicAdd( &queueCounters[ ${ RAY_QUEUE_LENGTH }u ], 1u );
398
+ rayQueue[ rayIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, scatterRec.direction, input.normal );
399
+ rayQueue[ rayIndex ].direction = scatterRec.direction;
400
+ rayQueue[ rayIndex ].pixelIndex = input.pixelIndex;
401
+ rayQueue[ rayIndex ].rngIndex = newBounce + input.alphaDepth + input.subsurfaceBounces;
402
+ rayQueue[ rayIndex ].seed = input.seed;
403
+ rayQueue[ rayIndex ].maxDist = 0.0;
317
404
  rayDataStorage[ index ].rayIntersectionIndex = i32( rayIndex );
318
405
 
319
- rayDataStorage[ index ].origin = rayQueue.elements[ rayIndex ].origin;
406
+ rayDataStorage[ index ].origin = rayQueue[ rayIndex ].origin;
320
407
  rayDataStorage[ index ].direction = scatterRec.direction;
408
+ rayDataStorage[ index ].rayStart = 0.0;
321
409
 
322
410
  }
323
411
 
@@ -332,14 +420,13 @@ export class MaterialKernel extends ComputeKernel {
332
420
  rayDataStorage[ index ].lightBsdf = evalRec.color;
333
421
  rayDataStorage[ index ].lightBsdfPdf = evalRec.pdf;
334
422
 
335
- let shadowIndex = atomicAdd( &shadowRayQueue.length, 1u );
336
- shadowRayQueue.elements[ shadowIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.lightDirection, input.normal );
337
- shadowRayQueue.elements[ shadowIndex ].direction = input.lightDirection;
338
- shadowRayQueue.elements[ shadowIndex ].pixelIndex = input.pixelIndex;
339
- shadowRayQueue.elements[ shadowIndex ].currentBounce = input.currentBounce;
340
- shadowRayQueue.elements[ shadowIndex ].seed = input.seed;
341
- shadowRayQueue.elements[ shadowIndex ].alphaDepth = input.alphaDepth;
342
- shadowRayQueue.elements[ shadowIndex ].maxDist = input.lightDist * ( 1.0 - ${ LIGHT_EPSILON } );
423
+ let shadowIndex = atomicAdd( &queueCounters[ ${ SHADOW_RAY_QUEUE_LENGTH }u ], 1u );
424
+ shadowRayQueue[ shadowIndex ].origin = ${ offsetRayOriginFunc }( vertexData.position.xyz, input.lightDirection, input.normal );
425
+ shadowRayQueue[ shadowIndex ].direction = input.lightDirection;
426
+ shadowRayQueue[ shadowIndex ].pixelIndex = input.pixelIndex;
427
+ shadowRayQueue[ shadowIndex ].rngIndex = input.currentBounce + input.alphaDepth + input.subsurfaceBounces;
428
+ shadowRayQueue[ shadowIndex ].seed = input.seed;
429
+ shadowRayQueue[ shadowIndex ].maxDist = input.lightDist * ( 1.0 - ${ LIGHT_EPSILON } );
343
430
  rayDataStorage[ index ].shadowRayIntersectionIndex = i32( shadowIndex );
344
431
 
345
432
  } else {