three-gpu-pathtracer 0.0.24 → 0.0.26

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 (73) hide show
  1. package/README.md +53 -530
  2. package/build/index.module.js +262 -50
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +261 -48
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +29 -11
  7. package/src/core/WebGLPathTracer.js +8 -0
  8. package/src/core/utils/sceneUpdateUtils.js +8 -12
  9. package/src/detectors/PrecisionMaterial.js +9 -9
  10. package/src/index.js +1 -0
  11. package/src/objects/PhysicalSpotLight.js +2 -0
  12. package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
  13. package/src/textures/BlueNoiseTexture.js +6 -6
  14. package/src/textures/ProceduralEquirectTexture.js +8 -7
  15. package/src/textures/turquinMetal.png +0 -0
  16. package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
  17. package/src/uniforms/FloatAttributeTextureArray.js +11 -11
  18. package/src/uniforms/MaterialsTexture.js +9 -9
  19. package/src/webgpu/API.md +762 -0
  20. package/src/webgpu/AtlasTexture.js +471 -0
  21. package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
  22. package/src/webgpu/EquirectBackgroundInfo.js +97 -0
  23. package/src/webgpu/EquirectHdrInfoNode.js +151 -0
  24. package/src/webgpu/LightsInfoNode.js +224 -0
  25. package/src/webgpu/MegaKernelPathTracer.js +277 -0
  26. package/src/webgpu/PathTracerBackend.js +216 -0
  27. package/src/webgpu/TurquinTexture.js +169 -0
  28. package/src/webgpu/WaveFrontPathTracer.js +586 -0
  29. package/src/webgpu/WebGPUPathTracer.js +1302 -0
  30. package/src/webgpu/compute/ComputeKernel.js +92 -0
  31. package/src/webgpu/compute/CopyBufferKernel.js +40 -0
  32. package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
  33. package/src/webgpu/compute/SampleDebugKernel.js +51 -0
  34. package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
  35. package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
  36. package/src/webgpu/compute/ZeroOutKernel.js +31 -0
  37. package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
  38. package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
  39. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
  40. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
  41. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
  42. package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
  43. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
  44. package/src/webgpu/compute/wavefront/structs.js +155 -0
  45. package/src/webgpu/constants.js +62 -0
  46. package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
  47. package/src/webgpu/index.d.ts +176 -0
  48. package/src/webgpu/index.js +11 -0
  49. package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
  50. package/src/webgpu/materials/GraphMaterial.js +331 -0
  51. package/src/webgpu/materials/PathtracingMaterial.js +89 -0
  52. package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
  53. package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
  54. package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
  55. package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
  56. package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
  57. package/src/webgpu/nodes/eon.wgsl.js +262 -0
  58. package/src/webgpu/nodes/ggx.wgsl.js +190 -0
  59. package/src/webgpu/nodes/lights.wgsl.js +203 -0
  60. package/src/webgpu/nodes/material.wgsl.js +990 -0
  61. package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
  62. package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
  63. package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
  64. package/src/webgpu/nodes/random.wgsl.js +20 -0
  65. package/src/webgpu/nodes/reset.wgsl.js +21 -0
  66. package/src/webgpu/nodes/sampling.wgsl.js +193 -0
  67. package/src/webgpu/nodes/sheen.wgsl.js +148 -0
  68. package/src/webgpu/nodes/structs.wgsl.js +272 -0
  69. package/src/webgpu/nodes/utils.wgsl.js +366 -0
  70. package/src/webgpu/shims/ArrayCameraShim.js +74 -0
  71. package/src/webgpu/shims/EquirectCameraShim.js +37 -0
  72. package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
  73. package/src/webgpu/upscale/FSRUpscaler.js +108 -0
@@ -0,0 +1,111 @@
1
+ import { StorageBufferAttribute, StorageTexture, StructTypeNode } from 'three/webgpu';
2
+ import { storage, textureStore, globalId } from 'three/tsl';
3
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
4
+ import { ComputeKernel } from './ComputeKernel.js';
5
+ import { SAMPLE_COUNT_MASK, SAMPLE_DISPATCHED_FLAG } from '../constants.js';
6
+
7
+ // field order of the sample counter buffer, matching the struct below
8
+ export const SAMPLE_COUNTER_MIN = 0;
9
+ export const SAMPLE_COUNTER_MAX = 1;
10
+ export const SAMPLE_COUNTER_TOTAL_LO = 2;
11
+ export const SAMPLE_COUNTER_TOTAL_HI = 3;
12
+ export const SAMPLE_COUNTER_PIXEL_COUNT = 4;
13
+ export const SAMPLE_COUNTER_LENGTH = 5;
14
+
15
+ export const U32_RANGE = 4294967296;
16
+
17
+ // Reduction target for the sample counts. The total outgrows 32 bits, so it is split across
18
+ // "totalLo" and "totalHi" and reassembled on read.
19
+ export const sampleCountersStruct = new StructTypeNode( {
20
+ minSamples: { type: 'uint', atomic: true },
21
+ maxSamples: { type: 'uint', atomic: true },
22
+ totalLo: { type: 'uint', atomic: true },
23
+ totalHi: { type: 'uint', atomic: true },
24
+ pixelCount: { type: 'uint', atomic: true },
25
+ }, 'SampleCounters' );
26
+
27
+ // Clears the counters ahead of the tally. "minSamples" starts at the u32 max so the first
28
+ // "atomicMin" replaces it.
29
+ export class PrimeSampleCountersKernel extends ComputeKernel {
30
+
31
+ constructor() {
32
+
33
+ const params = {
34
+ counters: storage( new StorageBufferAttribute( 1, 1 ), sampleCountersStruct ),
35
+ };
36
+
37
+ const fn = wgslTagFn/* wgsl */`
38
+ fn compute() -> void {
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 );
45
+
46
+ }
47
+ `;
48
+
49
+ super( fn( params ) );
50
+
51
+ this.defineUniformAccessors( params );
52
+
53
+ }
54
+
55
+ }
56
+
57
+ // Reduces the sample count target to a min, max, and total so the CPU reads back a few values
58
+ // instead of the whole texture.
59
+ export class TallySampleCountsKernel extends ComputeKernel {
60
+
61
+ constructor() {
62
+
63
+ const params = {
64
+ globalId: globalId,
65
+ counters: storage( new StorageBufferAttribute( 1, 1 ), sampleCountersStruct ),
66
+ sampleCountTarget: textureStore( new StorageTexture( 1, 1 ) ).toReadOnly(),
67
+ };
68
+
69
+ const fn = wgslTagFn/* wgsl */`
70
+ fn compute( globalId: vec3u ) -> void {
71
+
72
+ let targetDimensions = textureDimensions( ${ params.sampleCountTarget } );
73
+ if ( globalId.x >= targetDimensions.x || globalId.y >= targetDimensions.y ) {
74
+
75
+ return;
76
+
77
+ }
78
+
79
+ // pixels without a camera ray aren't part of the image, so skip rather than count zero
80
+ let combinedField = textureLoad( ${ params.sampleCountTarget }, globalId.xy ).r;
81
+ if ( ( combinedField & ${ SAMPLE_DISPATCHED_FLAG }u ) == 0u ) {
82
+
83
+ return;
84
+
85
+ }
86
+
87
+ let samples = combinedField & ${ SAMPLE_COUNT_MASK }u;
88
+
89
+ atomicMin( &${ params.counters }.minSamples, samples );
90
+ atomicMax( &${ params.counters }.maxSamples, samples );
91
+ atomicAdd( &${ params.counters }.pixelCount, 1u );
92
+
93
+ // the sum overflowed if it came out smaller than the value already there, so carry
94
+ // the wrap into "totalHi"
95
+ let previousTotal = atomicAdd( &${ params.counters }.totalLo, samples );
96
+ if ( previousTotal + samples < previousTotal ) {
97
+
98
+ atomicAdd( &${ params.counters }.totalHi, 1u );
99
+
100
+ }
101
+
102
+ }
103
+ `;
104
+
105
+ super( fn( params ) );
106
+
107
+ this.defineUniformAccessors( params );
108
+
109
+ }
110
+
111
+ }
@@ -0,0 +1,35 @@
1
+ import { IndirectStorageBufferAttribute } from 'three/webgpu';
2
+ import { ComputeKernel } from './ComputeKernel.js';
3
+ import { storage, globalId } from 'three/tsl';
4
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+
6
+ export class ZeroOutBufferKernel extends ComputeKernel {
7
+
8
+ constructor( options = {} ) {
9
+
10
+ const {
11
+ type = 'u32',
12
+ } = options;
13
+
14
+ const params = {
15
+ globalId: globalId,
16
+ outputTarget: storage( new IndirectStorageBufferAttribute( 1, 1 ), type ),
17
+ };
18
+
19
+ const fn = wgslTagFn/* wgsl */`
20
+ fn compute( globalId: vec3u ) -> void {
21
+
22
+ ${ params.outputTarget }[ globalId.x ] = 0;
23
+
24
+ }
25
+ `;
26
+
27
+ super( fn( params ) );
28
+
29
+ this.defineUniformAccessors( {
30
+ target: params.outputTarget,
31
+ } );
32
+
33
+ }
34
+
35
+ }
@@ -0,0 +1,31 @@
1
+ import { StorageTexture } from 'three/webgpu';
2
+ import { ComputeKernel } from './ComputeKernel.js';
3
+ import { textureStore, globalId } from 'three/tsl';
4
+ import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+
6
+ export class ZeroOutKernel extends ComputeKernel {
7
+
8
+ constructor() {
9
+
10
+ const params = {
11
+ globalId: globalId,
12
+ outputTarget: textureStore( new StorageTexture( 1, 1 ) ).toWriteOnly(),
13
+ };
14
+
15
+ const fn = wgslTagFn/* wgsl */`
16
+ fn compute( globalId: vec3u ) -> void {
17
+
18
+ textureStore( ${ params.outputTarget }, globalId.xy, vec4( 0, 0, 0, 1 ) );
19
+
20
+ }
21
+ `;
22
+
23
+ super( fn( params ) );
24
+
25
+ this.defineUniformAccessors( {
26
+ target: params.outputTarget,
27
+ } );
28
+
29
+ }
30
+
31
+ }
@@ -0,0 +1,349 @@
1
+ import { StorageBufferAttribute, StorageTexture } from 'three/webgpu';
2
+ import { ComputeKernel } from '../ComputeKernel.js';
3
+ import { uniform, storage, textureStore, globalId } from 'three/tsl';
4
+ import { proxy, proxyFn, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
+ import { misHeuristicFn, weightedAlphaBlendFn } from '../../nodes/sampling.wgsl.js';
6
+ import { clampPathContributionFunc, isTerminatingScatterFunc } from '../../nodes/utils.wgsl.js';
7
+ import { TRANSMISSIVE_BACKGROUND_ENVIRONMENT, TRANSMISSIVE_BACKGROUND_OVERLAY, TRANSMISSIVE_BACKGROUND_TRANSPARENT } from '../../constants.js';
8
+ import {
9
+ rngInit, rand2, rand3,
10
+ RNG_INDEX_BACKGROUND_SAMPLE,
11
+ RNG_INDEX_DIRECT_LIGHT_SAMPLE,
12
+ } from '../../nodes/random.wgsl.js';
13
+ import { ENVIRONMENT_LIGHT_TYPE, LIGHT_FAR_DISTANCE, isMISWeightLightFn } from '../../nodes/lights.wgsl.js';
14
+ import { lightRecordStruct, scatterRecordStruct } from '../../nodes/structs.wgsl.js';
15
+ import { rayDataStruct, intersectionResultStruct } from './structs.js';
16
+ import { SAMPLE_COUNT_MASK, SAMPLE_DISPATCHED_FLAG } from '../../constants.js';
17
+
18
+ // Path logic over the persistent slot pool: resolves the previous frame's shadow and bounce trace
19
+ // results, accumulates emission and NEE / forward MIS contributions, terminates finished paths into
20
+ // the output, and samples the next NEE light for MaterialKernel. Touches no BVH data.
21
+ export class LogicKernel extends ComputeKernel {
22
+
23
+ constructor( ) {
24
+
25
+ const params = {
26
+ envInfo: { value: null },
27
+ backgroundInfo: { value: null },
28
+ lightsInfo: { value: null },
29
+
30
+ // 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(),
34
+
35
+ // settings
36
+ misEnabled: uniform( 1, 'uint' ),
37
+ rayCount: uniform( 0, 'uint' ),
38
+ maxBounces: uniform( 5, 'uint' ),
39
+ clampDirect: uniform( 0 ),
40
+ clampIndirect: uniform( 10 ),
41
+ transmissiveBackground: uniform( TRANSMISSIVE_BACKGROUND_OVERLAY ),
42
+
43
+ rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
44
+ rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
45
+ shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
46
+
47
+ globalId: globalId,
48
+ };
49
+
50
+ // environment + background resources pulled off their providers (embedded functions)
51
+ const envTotalSumNode = proxy( 'envInfo.value.totalSumNode', params );
52
+ const envIntensityNode = proxy( 'envInfo.value.intensityNode', params );
53
+ const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
54
+ const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
55
+ const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
56
+ const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
57
+
58
+ // analytic scene lights pulled off the lightsInfo provider (LightsInfoNode)
59
+ const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
60
+ const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
61
+ const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
62
+
63
+ const fn = wgslTagFn/* wgsl */`
64
+
65
+ fn compute(
66
+ // settings
67
+ misEnabled: u32,
68
+ rayCount: u32,
69
+ maxBounces: u32,
70
+ clampDirect: f32,
71
+ clampIndirect: f32,
72
+ transmissiveBackground: u32,
73
+
74
+ globalId: vec3u
75
+ ) -> void {
76
+
77
+ let rayDataStorage = &${ params.rayDataStorage };
78
+ let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
79
+ let shadowRayIntersectionsStorage = &${ params.shadowRayIntersectionsStorage };
80
+
81
+ // bound by "rayCount" rather than the pool length. See MaterialKernel
82
+ let index = globalId.x;
83
+ if ( index >= rayCount ) {
84
+
85
+ return;
86
+
87
+ }
88
+
89
+ // skip slots that have never spawned a ray
90
+ let input = ( *rayDataStorage )[ index ];
91
+ if ( input.rayIntersectionIndex < 0 ) {
92
+
93
+ return;
94
+
95
+ }
96
+
97
+ let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
98
+ ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
99
+
100
+ // one-sample NEE selection normalization (lights + env), matched with the megakernel
101
+ let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
102
+ let lightsCount = ${ lightsCountNode };
103
+ var lightsDenom = f32( lightsCount );
104
+ if ( envActive ) {
105
+
106
+ lightsDenom += 1.0;
107
+
108
+ }
109
+
110
+ var resultColor = input.resultColor;
111
+ var throughputColor = input.throughputColor;
112
+
113
+ // resolve the previous surface's NEE shadow ray (pre-scatter throughput). The index
114
+ // is negative when no shadow ray was enqueued last frame
115
+ if ( input.shadowRayIntersectionIndex >= 0 && input.lightPdf > 0.0 ) {
116
+
117
+ let shadowHit = ( *shadowRayIntersectionsStorage )[ u32( input.shadowRayIntersectionIndex ) ];
118
+ let occluded = shadowHit.objectIndex >= 0;
119
+ if ( ! occluded ) {
120
+
121
+ // env + area lights are also bsdf-sampled, so MIS-weight them; punctual take full weight
122
+ let misWeight = select( 1.0, ${ misHeuristicFn }( input.lightPdf, input.lightBsdfPdf ), ${ isMISWeightLightFn }( input.lightType ) );
123
+ let directLight = throughputColor * input.lightEmission * input.lightBsdf * misWeight / input.lightPdf;
124
+ let contribution = ${ clampPathContributionFunc }( directLight, input.currentBounce, clampDirect, clampIndirect );
125
+ resultColor += vec4f( contribution, 0.0 );
126
+
127
+ }
128
+
129
+ }
130
+
131
+ // emission gathered at the previous surface (pre-scatter throughput). Found light clamps
132
+ // at the depth of the segment that found it, as in Cycles, so one less than the current
133
+ let emission = ${ clampPathContributionFunc }( throughputColor * input.emission, max( input.currentBounce, 1u ) - 1u, clampDirect, clampIndirect );
134
+ resultColor += vec4f( emission, 0.0 );
135
+
136
+ // reconstruct the scatter record staged by MaterialKernel
137
+ var scatterRec: ${ scatterRecordStruct };
138
+ scatterRec.color = input.scatterColor;
139
+ scatterRec.pdf = input.scatterPdf;
140
+
141
+ // the bounce limit, russian roulette, and terminating scatter checks all ran in
142
+ // MaterialKernel, which stages a zeroed pdf and skips the bounce trace when they fire
143
+ var isTerminated = all( throughputColor == vec3f( 0.0 ) ) || input.currentBounce >= maxBounces || ${ isTerminatingScatterFunc }( scatterRec );
144
+
145
+ if ( ! isTerminated ) {
146
+
147
+ // apply the scatter across the traced segment
148
+ throughputColor *= scatterRec.color / scatterRec.pdf;
149
+
150
+ let hitResult = ( *rayIntersectionsStorage )[ u32( input.rayIntersectionIndex ) ];
151
+ let didHit = hitResult.objectIndex >= 0;
152
+ let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
153
+
154
+ // forward hits: a bsdf-sampled segment that lands on a area light. MIS-weighted
155
+ // only when NEE is also sampling the lights. The camera segment is skipped.
156
+ if ( input.currentBounce > 0u ) {
157
+
158
+ for ( var li = 0u; li < lightsCount; li ++ ) {
159
+
160
+ var lightRec: ${ lightRecordStruct };
161
+ if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
162
+
163
+ var misWeight = 1.0;
164
+ if ( misEnabled != 0u ) {
165
+
166
+ let lightPdf = lightRec.pdf / lightsDenom;
167
+ misWeight = ${ misHeuristicFn }( input.scatterPdf, lightPdf );
168
+
169
+ }
170
+
171
+ let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
172
+ resultColor += vec4f( lightHit, 0.0 );
173
+
174
+ }
175
+
176
+ }
177
+
178
+ }
179
+
180
+ if ( didHit ) {
181
+
182
+ // stage the hit for MaterialKernel
183
+ rayDataStorage[ index ].barycoord = hitResult.barycoord;
184
+ rayDataStorage[ index ].normal = hitResult.normal;
185
+ rayDataStorage[ index ].side = hitResult.side;
186
+ rayDataStorage[ index ].indices = hitResult.indices;
187
+ rayDataStorage[ index ].objectIndex = hitResult.objectIndex;
188
+ rayDataStorage[ index ].dist = hitResult.dist;
189
+
190
+ // next event estimation: pick one light or the environment with a single sample.
191
+ // MaterialKernel evaluates the bsdf and enqueues the shadow ray.
192
+ // TODO: importance-sample the selection by light intensity and solid angle
193
+ var lightPdf = 0.0;
194
+ if ( misEnabled != 0u && lightsDenom > 0.0 ) {
195
+
196
+ let ruv = ${ rand3 }( ${ RNG_INDEX_DIRECT_LIGHT_SAMPLE } );
197
+ let lightIndex = min( u32( ruv.x * lightsDenom ), u32( lightsDenom ) - 1u );
198
+ var lightRec: ${ lightRecordStruct };
199
+ if ( envActive && lightIndex == lightsCount ) {
200
+
201
+ // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
202
+ let envSample = ${ sampleEnvDir }( ruv.yz );
203
+ lightRec.direction = envSample.direction;
204
+ lightRec.emission = envSample.color;
205
+ lightRec.pdf = envSample.pdf;
206
+ lightRec.dist = ${ LIGHT_FAR_DISTANCE };
207
+ lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
208
+
209
+ } else {
210
+
211
+ lightRec = ${ randomLightSampleFn }( lightIndex, hitResult.position, ruv.yz );
212
+
213
+ }
214
+
215
+ lightPdf = lightRec.pdf / lightsDenom;
216
+ rayDataStorage[ index ].lightDirection = lightRec.direction;
217
+ rayDataStorage[ index ].lightEmission = lightRec.emission;
218
+ rayDataStorage[ index ].lightDist = lightRec.dist;
219
+ rayDataStorage[ index ].lightType = lightRec.lightType;
220
+
221
+ }
222
+
223
+ rayDataStorage[ index ].lightPdf = lightPdf;
224
+
225
+ } else {
226
+
227
+ // the segment escaped the scene: gather the environment for opaque paths, or
228
+ // the background for camera segments and fully transmissive paths
229
+ if ( input.currentBounce > 0u && input.isFullyTransmissive == 0u ) {
230
+
231
+ var misWeight = 1.0;
232
+ if ( misEnabled != 0u && envActive ) {
233
+
234
+ // match the env pdf scaling used by the NEE selection so the two estimators balance
235
+ let envPdf = ${ getEnvDirPdf }( input.direction ) / lightsDenom;
236
+ misWeight = ${ misHeuristicFn }( input.scatterPdf, envPdf );
237
+
238
+ }
239
+
240
+ let environment = ${ sampleEnvColor }( input.direction ).rgb * throughputColor * misWeight;
241
+ let contribution = ${ clampPathContributionFunc }( environment, input.currentBounce, clampDirect, clampIndirect );
242
+ resultColor += vec4f( contribution, 0.0 );
243
+
244
+ } else {
245
+
246
+ // hit the background
247
+ // support multiple transparent background blending techniques
248
+ let rng = ${ rand2 }( ${ RNG_INDEX_BACKGROUND_SAMPLE } );
249
+ let bg = ${ sampleBackground }( input.direction, rng );
250
+ if ( input.currentBounce == 0u ) {
251
+
252
+ // sample the background directly if this is the primary ray
253
+ let background = ${ clampPathContributionFunc }( bg.a * bg.rgb, input.currentBounce, clampDirect, clampIndirect );
254
+ resultColor = vec4f( background, bg.a );
255
+
256
+ } else {
257
+
258
+ // transmissive ray handling
259
+ let env = ${ sampleEnvColor }( input.direction );
260
+ let avg = saturate( dot( throughputColor, vec3f( 1.0 / 3.0 ) ) );
261
+ let transparency = ( 1.0 - bg.a ) * avg;
262
+
263
+ var misWeight = 1.0;
264
+ if ( misEnabled != 0u && envActive ) {
265
+
266
+ let envPdf = ${ getEnvDirPdf }( input.direction );
267
+ misWeight = ${ misHeuristicFn }( input.scatterPdf, envPdf );
268
+
269
+ }
270
+
271
+ if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_ENVIRONMENT }u ) {
272
+
273
+ // display the env map through transmissive surfaces
274
+ let background = ${ clampPathContributionFunc }( env.rgb * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
275
+ resultColor = vec4f(
276
+ resultColor.rgb + background,
277
+ 1.0,
278
+ );
279
+
280
+ } else if ( transmissiveBackground == ${ TRANSMISSIVE_BACKGROUND_TRANSPARENT }u ) {
281
+
282
+ // fade the background by the throughput color average
283
+ let background = ${ clampPathContributionFunc }( bg.a * bg.rgb * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
284
+ resultColor = vec4f(
285
+ resultColor.rgb + background,
286
+ 1.0 - transparency,
287
+ );
288
+
289
+ } else {
290
+
291
+ // fade the background by the throughput color average, mixing in env lighting
292
+ var light = mix( env.rgb, bg.rgb, bg.a ) * misWeight;
293
+ let background = ${ clampPathContributionFunc }( light * throughputColor, input.currentBounce, clampDirect, clampIndirect );
294
+ resultColor = vec4f(
295
+ resultColor.rgb + background,
296
+ 1.0 - transparency,
297
+ );
298
+
299
+ }
300
+
301
+ }
302
+
303
+ }
304
+
305
+ isTerminated = true;
306
+
307
+ }
308
+
309
+ }
310
+
311
+ // the color rows are stored top down to match a rasterized render target
312
+ let colorIndex = vec2u( indexUV.x, textureDimensions( ${ params.outputTarget } ).y - 1u - indexUV.y );
313
+ let storedSamples = textureLoad( ${ params.sampleCountTarget }, indexUV ).r & ${ SAMPLE_COUNT_MASK }u;
314
+
315
+ if ( isTerminated ) {
316
+
317
+ // blend the finished sample into the output and free the slot for a new camera ray
318
+ let sampleCount = storedSamples + 1;
319
+ let prevColor = textureLoad( ${ params.prevOutputTarget }, colorIndex );
320
+ let blendedColor = ${ weightedAlphaBlendFn }( prevColor, resultColor, 1.0 / f32( sampleCount ) );
321
+ textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ${ SAMPLE_DISPATCHED_FLAG }u | sampleCount ) );
322
+ textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
323
+
324
+ rayDataStorage[ index ].objectIndex = - 1;
325
+
326
+ } else {
327
+
328
+ // show the path so far, which the first sample overwrites
329
+ if ( storedSamples == 0u ) {
330
+
331
+ textureStore( ${ params.outputTarget }, colorIndex, resultColor );
332
+
333
+ }
334
+
335
+ rayDataStorage[ index ].resultColor = resultColor;
336
+ rayDataStorage[ index ].throughputColor = throughputColor;
337
+
338
+ }
339
+
340
+ }
341
+ `;
342
+
343
+ super( fn( params ) );
344
+
345
+ this.defineUniformAccessors( params );
346
+
347
+ }
348
+
349
+ }