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
@@ -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
+ }
@@ -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
  };
@@ -53,12 +59,17 @@ export class LogicKernel extends ComputeKernel {
53
59
  const sampleEnvColor = proxy( 'envInfo.value.sampleColor', params );
54
60
  const sampleEnvDir = proxy( 'envInfo.value.sampleDir', params );
55
61
  const getEnvDirPdf = proxy( 'envInfo.value.getDirPdf', params );
62
+ const getEnvWeight = proxy( 'envInfo.value.getWeight', params );
56
63
  const sampleBackground = proxy( 'backgroundInfo.value.sampleColor', params );
57
64
 
58
65
  // analytic scene lights pulled off the lightsInfo provider (LightsInfoNode)
59
66
  const lightsCountNode = proxy( 'lightsInfo.value.countNode', params );
60
67
  const randomLightSampleFn = proxyFn( 'lightsInfo.value.randomLightSample', params );
61
68
  const intersectLightAtIndexFn = proxyFn( 'lightsInfo.value.intersectLightAtIndex', params );
69
+ const getLightWeightFn = proxyFn( 'lightsInfo.value.getLightWeight', params );
70
+ const getLightsWeightFn = proxyFn( 'lightsInfo.value.getLightsWeight', params );
71
+ const selectLightFn = proxyFn( 'lightsInfo.value.selectLight', params );
72
+ const isLightVisibleToCameraFn = proxyFn( 'lightsInfo.value.isLightVisibleToCamera', params );
62
73
 
63
74
  const fn = wgslTagFn/* wgsl */`
64
75
 
@@ -70,6 +81,7 @@ export class LogicKernel extends ComputeKernel {
70
81
  clampDirect: f32,
71
82
  clampIndirect: f32,
72
83
  transmissiveBackground: u32,
84
+ accumulateAux: u32,
73
85
 
74
86
  globalId: vec3u
75
87
  ) -> void {
@@ -87,7 +99,8 @@ export class LogicKernel extends ComputeKernel {
87
99
  }
88
100
 
89
101
  // skip slots that have never spawned a ray
90
- 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 ];
91
104
  if ( input.rayIntersectionIndex < 0 ) {
92
105
 
93
106
  return;
@@ -95,32 +108,40 @@ export class LogicKernel extends ComputeKernel {
95
108
  }
96
109
 
97
110
  let indexUV = vec2u( input.pixelIndex >> 16, input.pixelIndex & 0xFFFF );
98
- ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth );
111
+ ${ rngInit }( indexUV, input.seed, input.currentBounce + input.alphaDepth + input.subsurfaceBounces );
99
112
 
100
- // one-sample NEE selection normalization (lights + env), matched with the megakernel
113
+ // one-sample NEE chooses between the analytic lights and the environment, matched with
114
+ // the megakernel
101
115
  let envActive = ${ envTotalSumNode } > 0.0 && ${ envIntensityNode } > 0.0;
102
116
  let lightsCount = ${ lightsCountNode };
103
- var lightsDenom = f32( lightsCount );
117
+ var envWeight = 0.0;
104
118
  if ( envActive ) {
105
119
 
106
- lightsDenom += 1.0;
120
+ envWeight = ${ getEnvWeight }();
107
121
 
108
122
  }
109
123
 
110
124
  var resultColor = input.resultColor;
111
125
  var throughputColor = input.throughputColor;
112
126
 
127
+ // added last, since a camera ray miss overwrites the pixel with the background
128
+ var lightHits = vec3f( 0.0 );
129
+ var cameraHitLight = false;
130
+
113
131
  // resolve the previous surface's NEE shadow ray (pre-scatter throughput). The index
114
132
  // is negative when no shadow ray was enqueued last frame
115
133
  if ( input.shadowRayIntersectionIndex >= 0 && input.lightPdf > 0.0 ) {
116
134
 
117
- let shadowHit = ( *shadowRayIntersectionsStorage )[ u32( input.shadowRayIntersectionIndex ) ];
135
+ let shadowHit = ${ params.shadowRayIntersectionsStorage }[ u32( input.shadowRayIntersectionIndex ) ];
118
136
  let occluded = shadowHit.objectIndex >= 0;
119
137
  if ( ! occluded ) {
120
138
 
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;
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;
124
145
  let contribution = ${ clampPathContributionFunc }( directLight, input.currentBounce, clampDirect, clampIndirect );
125
146
  resultColor += vec4f( contribution, 0.0 );
126
147
 
@@ -142,37 +163,64 @@ export class LogicKernel extends ComputeKernel {
142
163
  // MaterialKernel, which stages a zeroed pdf and skips the bounce trace when they fire
143
164
  var isTerminated = all( throughputColor == vec3f( 0.0 ) ) || input.currentBounce >= maxBounces || ${ isTerminatingScatterFunc }( scatterRec );
144
165
 
166
+ // apply the scatter across the traced segment, including the weight of a medium walk
145
167
  if ( ! isTerminated ) {
146
168
 
147
- // apply the scatter across the traced segment
148
169
  throughputColor *= scatterRec.color / scatterRec.pdf;
149
170
 
150
- 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 ) ];
151
187
  let didHit = hitResult.objectIndex >= 0;
152
188
  let surfaceDist = select( ${ LIGHT_FAR_DISTANCE }, hitResult.dist, didHit );
153
189
 
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 ) {
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;
193
+ var lightTotalWeight = 0.0;
194
+ if ( isMISWeighted ) {
157
195
 
158
- for ( var li = 0u; li < lightsCount; li ++ ) {
196
+ lightTotalWeight = ${ getLightsWeightFn }( input.origin ) + envWeight;
159
197
 
160
- var lightRec: ${ lightRecordStruct };
161
- if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist < surfaceDist ) {
198
+ }
162
199
 
163
- var misWeight = 1.0;
164
- if ( misEnabled != 0u ) {
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
202
+ for ( var li = 0u; li < lightsCount; li ++ ) {
165
203
 
166
- let lightPdf = lightRec.pdf / lightsDenom;
167
- misWeight = ${ misHeuristicFn }( input.scatterPdf, lightPdf );
204
+ if ( input.currentBounce == 0u && ! ${ isLightVisibleToCameraFn }( li ) ) {
168
205
 
169
- }
206
+ continue;
170
207
 
171
- let lightHit = ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
172
- resultColor += vec4f( lightHit, 0.0 );
208
+ }
209
+
210
+ var lightRec: ${ lightRecordStruct };
211
+ if ( ${ intersectLightAtIndexFn }( input.origin, input.direction, li, &lightRec ) && lightRec.dist > input.rayStart && lightRec.dist < input.rayStart + surfaceDist ) {
212
+
213
+ var misWeight = 1.0;
214
+ if ( isMISWeighted ) {
215
+
216
+ let selectionPdf = ${ getLightWeightFn }( li, input.origin ) / lightTotalWeight;
217
+ misWeight = ${ misHeuristicFn }( input.scatterPdf, lightRec.pdf * selectionPdf );
173
218
 
174
219
  }
175
220
 
221
+ lightHits += ${ clampPathContributionFunc }( lightRec.emission * throughputColor * misWeight, input.currentBounce, clampDirect, clampIndirect );
222
+ cameraHitLight = cameraHitLight || input.currentBounce == 0u;
223
+
176
224
  }
177
225
 
178
226
  }
@@ -187,36 +235,40 @@ export class LogicKernel extends ComputeKernel {
187
235
  rayDataStorage[ index ].objectIndex = hitResult.objectIndex;
188
236
  rayDataStorage[ index ].dist = hitResult.dist;
189
237
 
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
238
+ // next event estimation: choose one light or the environment by its estimated
239
+ // contribution. MaterialKernel evaluates the bsdf and enqueues the shadow ray.
193
240
  var lightPdf = 0.0;
194
- if ( misEnabled != 0u && lightsDenom > 0.0 ) {
241
+ if ( misEnabled != 0u ) {
195
242
 
196
243
  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 };
244
+ var selectionPdf = 0.0;
245
+ let lightIndex = ${ selectLightFn }( hitResult.position, envWeight, ruv.x, &selectionPdf );
246
+ if ( selectionPdf > 0.0 ) {
208
247
 
209
- } else {
248
+ var lightRec: ${ lightRecordStruct };
249
+ if ( envActive && lightIndex == lightsCount ) {
210
250
 
211
- lightRec = ${ randomLightSampleFn }( lightIndex, hitResult.position, ruv.yz );
251
+ // the environment, sampled from its CDF, as a light of kind ENVIRONMENT
252
+ let envSample = ${ sampleEnvDir }( ruv.yz );
253
+ lightRec.direction = envSample.direction;
254
+ lightRec.emission = envSample.color;
255
+ lightRec.pdf = envSample.pdf;
256
+ lightRec.dist = ${ LIGHT_FAR_DISTANCE };
257
+ lightRec.lightType = ${ ENVIRONMENT_LIGHT_TYPE };
212
258
 
213
- }
259
+ } else {
214
260
 
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;
261
+ lightRec = ${ randomLightSampleFn }( lightIndex, hitResult.position, ruv.yz );
262
+
263
+ }
264
+
265
+ lightPdf = lightRec.pdf * selectionPdf;
266
+ rayDataStorage[ index ].lightDirection = lightRec.direction;
267
+ rayDataStorage[ index ].lightEmission = lightRec.emission;
268
+ rayDataStorage[ index ].lightDist = lightRec.dist;
269
+ rayDataStorage[ index ].lightType = lightRec.lightType;
270
+
271
+ }
220
272
 
221
273
  }
222
274
 
@@ -229,10 +281,10 @@ export class LogicKernel extends ComputeKernel {
229
281
  if ( input.currentBounce > 0u && input.isFullyTransmissive == 0u ) {
230
282
 
231
283
  var misWeight = 1.0;
232
- if ( misEnabled != 0u && envActive ) {
284
+ if ( isMISWeighted && envActive ) {
233
285
 
234
286
  // match the env pdf scaling used by the NEE selection so the two estimators balance
235
- let envPdf = ${ getEnvDirPdf }( input.direction ) / lightsDenom;
287
+ let envPdf = ${ getEnvDirPdf }( input.direction ) * envWeight / lightTotalWeight;
236
288
  misWeight = ${ misHeuristicFn }( input.scatterPdf, envPdf );
237
289
 
238
290
  }
@@ -308,6 +360,14 @@ export class LogicKernel extends ComputeKernel {
308
360
 
309
361
  }
310
362
 
363
+ // visible lights are opaque against a transparent background
364
+ resultColor += vec4f( lightHits, 0.0 );
365
+ if ( cameraHitLight ) {
366
+
367
+ resultColor.a = 1.0;
368
+
369
+ }
370
+
311
371
  // the color rows are stored top down to match a rasterized render target
312
372
  let colorIndex = vec2u( indexUV.x, textureDimensions( ${ params.outputTarget } ).y - 1u - indexUV.y );
313
373
  let storedSamples = textureLoad( ${ params.sampleCountTarget }, indexUV ).r & ${ SAMPLE_COUNT_MASK }u;
@@ -321,6 +381,27 @@ export class LogicKernel extends ComputeKernel {
321
381
  textureStore( ${ params.sampleCountTarget }, indexUV, vec4( ${ SAMPLE_DISPATCHED_FLAG }u | sampleCount ) );
322
382
  textureStore( ${ params.outputTarget }, colorIndex, blendedColor );
323
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
+
324
405
  rayDataStorage[ index ].objectIndex = - 1;
325
406
 
326
407
  } else {