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
@@ -1,20 +1,9 @@
1
- import { Matrix3 } from 'three/webgpu';
2
- import { texture, sampler, uniform, wgslFn } from 'three/tsl';
1
+ import { Matrix3, StorageBufferAttribute } from 'three/webgpu';
2
+ import { texture, sampler, uniform, storage, PI } from 'three/tsl';
3
3
  import { EquirectHdrInfoUniform } from '../uniforms/EquirectHdrInfoUniform.js';
4
4
  import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
- import { constants, environmentSampleStruct } from './nodes/structs.wgsl.js';
6
- import { equirectDirectionToUvFn, equirectUvToDirectionFn, luminanceFn } from './nodes/sampling.wgsl.js';
7
-
8
- // totalWeight includes sin( theta ), which cancels in the solid-angle conversion.
9
- const equirectLuminancePdfFn = wgslFn( /* wgsl */ `
10
-
11
- fn equirectLuminancePdf( luminance: f32, totalWeight: f32, resolution: vec2u ) -> f32 {
12
-
13
- return f32( resolution.x * resolution.y ) * luminance / ( 2.0 * PI * PI * totalWeight );
14
-
15
- }
16
-
17
- `, [ constants ] );
5
+ import { environmentSampleStruct } from './nodes/structs.wgsl.js';
6
+ import { equirectDirectionToUvFn, equirectUvToDirectionFn } from './nodes/sampling.wgsl.js';
18
7
 
19
8
  export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
20
9
 
@@ -22,13 +11,10 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
22
11
 
23
12
  super();
24
13
 
25
- // environment map + importance-sampling CDF textures, each with a sampler
14
+ // environment map with a sampler, and the importance sampling CDFs for the default 1x1 map
26
15
  this.mapNode = texture( this.map );
27
16
  this.mapSampler = sampler( this.mapNode );
28
- this.marginalNode = texture( this.marginalWeights );
29
- this.marginalSampler = sampler( this.marginalNode );
30
- this.conditionalNode = texture( this.conditionalWeights );
31
- this.conditionalSampler = sampler( this.conditionalNode );
17
+ this.cdfNode = storage( new StorageBufferAttribute( new Float32Array( [ 0, 1, 0, 1 ] ), 1 ), 'float' ).toReadOnly();
32
18
 
33
19
  // scalar parameters that assemble into the EnvironmentInfo struct in the shader
34
20
  this.rotationNode = uniform( new Matrix3() );
@@ -53,17 +39,35 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
53
39
 
54
40
  const {
55
41
  mapNode,
56
- marginalNode,
57
- conditionalNode,
42
+ cdfNode,
58
43
  totalSumNode,
59
44
  } = this;
60
45
 
61
46
  // refresh values in place on the existing nodes so no rebuild is required
62
47
  mapNode.value = this.map;
63
- marginalNode.value = this.marginalWeights;
64
- conditionalNode.value = this.conditionalWeights;
65
48
  totalSumNode.value = this.totalSum;
66
49
 
50
+ // pack the row CDF and then each row's pixel CDF into one buffer, each led by a zero so a
51
+ // pixel's range is two neighboring entries
52
+ const { width, height } = this.map.image;
53
+ const { cdfMarginal, cdfConditional } = this;
54
+ const cdf = new Float32Array( height + 1 + height * ( width + 1 ) );
55
+ cdf.set( cdfMarginal, 1 );
56
+ for ( let y = 0; y < height; y ++ ) {
57
+
58
+ cdf.set( cdfConditional.subarray( y * width, y * width + width ), height + 1 + y * ( width + 1 ) + 1 );
59
+
60
+ }
61
+
62
+ cdfNode.value = new StorageBufferAttribute( cdf, 1 );
63
+
64
+ }
65
+
66
+ dispose() {
67
+
68
+ super.dispose();
69
+ this.cdfNode.value.dispose();
70
+
67
71
  }
68
72
 
69
73
  _initFns() {
@@ -71,15 +75,60 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
71
75
  const {
72
76
  mapNode,
73
77
  mapSampler,
74
- marginalNode,
75
- marginalSampler,
76
- conditionalNode,
77
- conditionalSampler,
78
+ cdfNode,
78
79
  totalSumNode,
79
80
  rotationNode,
80
81
  intensityNode,
81
82
  } = this;
82
83
 
84
+ // Binary searches the "count" pixel ranges from "offset" for the one holding "r", writing where
85
+ // "r" falls within it to "fraction". See:
86
+ // https://pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations#Piecewise-Constant2DDistributions
87
+ // https://github.com/blender/cycles/blob/main/src/kernel/light/background.h
88
+ const searchCdfFn = wgslTagFn/* wgsl */`
89
+ fn searchEnvCdf( r: f32, offset: u32, count: u32, fraction: ptr<function, f32> ) -> u32 {
90
+
91
+ var lower = 0u;
92
+ var upper = count - 1u;
93
+ while ( lower < upper ) {
94
+
95
+ let mid = ( lower + upper ) >> 1u;
96
+ if ( ${ cdfNode }[ offset + mid + 1u ] <= r ) {
97
+
98
+ lower = mid + 1u;
99
+
100
+ } else {
101
+
102
+ upper = mid;
103
+
104
+ }
105
+
106
+ }
107
+
108
+ let start = ${ cdfNode }[ offset + lower ];
109
+ let end = ${ cdfNode }[ offset + lower + 1u ];
110
+ *fraction = select( 0.5, saturate( ( r - start ) / ( end - start ) ), end > start );
111
+ return lower;
112
+
113
+ }
114
+ `;
115
+
116
+ // the solid angle pdf of a direction at "v" in the given pixel, which is sampled uniformly over its area
117
+ const getTexelPdfFn = wgslTagFn/* wgsl */`
118
+ fn getEnvTexelPdf( texel: vec2u, v: f32 ) -> f32 {
119
+
120
+ let resolution = textureDimensions( ${ mapNode } );
121
+ let rowOffset = resolution.y + 1u + texel.y * ( resolution.x + 1u );
122
+ let rowPdf = ${ cdfNode }[ texel.y + 1u ] - ${ cdfNode }[ texel.y ];
123
+ let pixelPdf = ${ cdfNode }[ rowOffset + texel.x + 1u ] - ${ cdfNode }[ rowOffset + texel.x ];
124
+
125
+ // the equirect projection stretches each pixel by 1 / sin( theta ) over the sphere
126
+ let sinTheta = sin( ${ PI } * v );
127
+ return select( 0.0, rowPdf * pixelPdf * f32( resolution.x * resolution.y ) / ( 2.0 * ${ PI } * ${ PI } * sinTheta ), sinTheta > 0.0 );
128
+
129
+ }
130
+ `;
131
+
83
132
  this.sampleColor = wgslTagFn/* wgsl */`
84
133
  fn sampleEnv( direction: vec3f ) -> vec4f {
85
134
 
@@ -97,31 +146,34 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
97
146
 
98
147
  var result: ${ environmentSampleStruct };
99
148
 
100
- // walk the CDF: marginal picks the row (v), conditional picks the column (u)
101
- let v = textureSampleLevel( ${ marginalNode }, ${ marginalSampler }, vec2f( r.x, 0.0 ), 0 ).x;
102
- let u = textureSampleLevel( ${ conditionalNode }, ${ conditionalSampler }, vec2f( r.y, v ), 0 ).x;
103
- let uv = vec2f( u, v );
104
- let totalSum = ${ totalSumNode };
149
+ // search the CDFs: marginal picks the row (v), conditional picks the column (u), and the
150
+ // fractions place the sample within the pixel
151
+ let resolution = textureDimensions( ${ mapNode } );
152
+ var rowFraction = 0.0;
153
+ var columnFraction = 0.0;
154
+ let row = ${ searchCdfFn }( r.x, 0u, resolution.y, &rowFraction );
155
+ let column = ${ searchCdfFn }( r.y, resolution.y + 1u + row * ( resolution.x + 1u ), resolution.x, &columnFraction );
156
+ let texel = vec2u( column, row );
157
+ let uv = ( vec2f( texel ) + vec2f( columnFraction, rowFraction ) ) / vec2f( resolution );
105
158
 
106
159
  let direction = ${ equirectUvToDirectionFn }( uv );
107
160
  let color = textureSampleLevel( ${ mapNode }, ${ mapSampler }, uv, 0 ).rgb;
108
161
 
109
162
  result.direction = transpose( ${ rotationNode } ) * direction;
110
163
  result.color = color * ${ intensityNode };
164
+ result.pdf = ${ getTexelPdfFn }( texel, uv.y );
111
165
 
112
- if ( totalSum != 0.0 ) {
113
-
114
- let lum = ${ luminanceFn }( color );
115
- let resolution = textureDimensions( ${ mapNode } );
116
- result.pdf = ${ equirectLuminancePdfFn }( lum, totalSum, resolution );
117
-
118
- } else {
166
+ return result;
119
167
 
120
- result.pdf = 0.0;
168
+ }
169
+ `;
121
170
 
122
- }
171
+ // the environment's average irradiance, for weighing it against the lights
172
+ this.getWeight = wgslTagFn/* wgsl */`
173
+ fn getEnvWeight() -> f32 {
123
174
 
124
- return result;
175
+ let resolution = textureDimensions( ${ mapNode } );
176
+ return ${ intensityNode } * ${ PI } * ${ PI } * ${ totalSumNode } / ( 2.0 * f32( resolution.x * resolution.y ) );
125
177
 
126
178
  }
127
179
  `;
@@ -137,11 +189,10 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
137
189
 
138
190
  let rotatedDir = ${ rotationNode } * direction;
139
191
  let mapUv = ${ equirectDirectionToUvFn }( rotatedDir );
140
- let color = textureSampleLevel( ${ mapNode }, ${ mapSampler }, mapUv, 0 ).rgb;
141
- let lum = ${ luminanceFn }( color );
142
192
  let resolution = textureDimensions( ${ mapNode } );
193
+ let texel = min( vec2u( mapUv * vec2f( resolution ) ), resolution - 1u );
143
194
 
144
- return ${ equirectLuminancePdfFn }( lum, ${ totalSumNode }, resolution );
195
+ return ${ getTexelPdfFn }( texel, mapUv.y );
145
196
 
146
197
  }
147
198
  `;
@@ -5,6 +5,7 @@ import { AtlasTexture } from './AtlasTexture.js';
5
5
  import { LightsInfoUniformStruct } from '../uniforms/LightsInfoUniformStruct.js';
6
6
  import { lightStruct, lightRecordStruct } from './nodes/structs.wgsl.js';
7
7
  import { sampleTexelFunc } from './nodes/utils.wgsl.js';
8
+ import { luminanceFn } from './nodes/sampling.wgsl.js';
8
9
  import {
9
10
  RECT_AREA_LIGHT_TYPE,
10
11
  CIRC_AREA_LIGHT_TYPE,
@@ -17,6 +18,8 @@ import {
17
18
  randomAreaLightSampleFn,
18
19
  randomSpotLightSampleFn,
19
20
  getSpotAttenuationFn,
21
+ getDistanceAttenuationFn,
22
+ getSpotLightNearestFn,
20
23
  } from './nodes/lights.wgsl.js';
21
24
 
22
25
  export class LightsInfoNode extends LightsInfoUniformStruct {
@@ -33,7 +36,7 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
33
36
  // ies profiles packed into an atlas alongside their placement rects
34
37
  this.iesAtlas = new AtlasTexture( { type: HalfFloatType } );
35
38
  this.iesProfilesNode = texture( this.iesAtlas.texture );
36
- this.iesInfoNode = uniformArray( this.iesAtlas.textureInfo, 'uvec4' );
39
+ this.iesInfoNode = uniformArray( this.iesAtlas.textureInfo, 'uvec4' ).setName( 'iesInfo' );
37
40
 
38
41
  this._initFns();
39
42
 
@@ -65,12 +68,13 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
65
68
  const src = this.tex.image.data;
66
69
  this.buffer.array.set( src.subarray( 0, count * stride ) );
67
70
 
68
- // rewrite the int fields ( lightType, iesProfile ) as i32 bits
71
+ // rewrite the int fields ( lightType, visibleToCameraRays, iesProfile ) as i32 bits
69
72
  const intView = new Int32Array( this.buffer.array.buffer );
70
73
  for ( let i = 0; i < count; i ++ ) {
71
74
 
72
75
  const base = i * stride;
73
76
  intView[ base + 3 ] = Math.round( src[ base + 3 ] );
77
+ intView[ base + 11 ] = Math.round( src[ base + 11 ] );
74
78
  intView[ base + 21 ] = Math.round( src[ base + 21 ] );
75
79
 
76
80
  }
@@ -84,12 +88,146 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
84
88
 
85
89
  _initFns() {
86
90
 
87
- const { bufferNode, iesProfilesNode, iesInfoNode } = this;
91
+ const { bufferNode, countNode, iesProfilesNode, iesInfoNode } = this;
88
92
 
89
93
  // profiles are sampled out of an atlas so filtering must resolve tile-relative wrapping
90
94
  const sampleIesTexelFn = sampleTexelFunc( iesInfoNode, iesProfilesNode, 'sampleIesTexel' );
91
95
 
92
- // uniformly pick a light and sample it
96
+ this.isLightVisibleToCamera = wgslTagFn/* wgsl */`
97
+ fn isLightVisibleToCamera( index: u32 ) -> bool {
98
+
99
+ return ${ bufferNode }[ index ].visibleToCameraRays != 0;
100
+
101
+ }
102
+ `;
103
+
104
+ // A cheap estimate of how much light reaches a position from a light, ignoring occlusion and
105
+ // the surface orientation, used to choose which light to sample.
106
+ // TODO: weigh lights below the surface's horizon to zero using its normal, skipped for
107
+ // transmissive surfaces. Forward hit MIS would then need the previous vertex's normal too.
108
+ this.getLightWeight = wgslTagFn/* wgsl */`
109
+ fn getLightWeight( index: u32, position: vec3f ) -> f32 {
110
+
111
+ let light = ${ bufferNode }[ index ];
112
+ let power = ${ luminanceFn }( light.color * light.intensity );
113
+ if ( light.lightType == ${ SPOT_LIGHT_TYPE } ) {
114
+
115
+ // the most any point on the spot's disc lights the position. Ies profiles are not
116
+ // evaluated here
117
+ let nearest = ${ getSpotLightNearestFn }( light, position );
118
+ var attenuation = 1.0;
119
+ if ( light.iesProfile < 0 ) {
120
+
121
+ attenuation = ${ getSpotAttenuationFn }( light.coneCos, light.penumbraCos, nearest.x );
122
+
123
+ }
124
+
125
+ return power * attenuation * ${ getDistanceAttenuationFn }( nearest.y, light.distance, light.decay );
126
+
127
+ } else if ( light.lightType == ${ POINT_LIGHT_TYPE } ) {
128
+
129
+ // the point light's world position is packed into the u slot
130
+ let dist = length( light.u - position );
131
+ return power * ${ getDistanceAttenuationFn }( dist, light.distance, light.decay );
132
+
133
+ } else if ( light.lightType == ${ DIR_LIGHT_TYPE } ) {
134
+
135
+ return power;
136
+
137
+ } else {
138
+
139
+ let toLight = light.position - position;
140
+ let distSq = dot( toLight, toLight );
141
+ if ( distSq == 0.0 ) {
142
+
143
+ return 0.0;
144
+
145
+ }
146
+
147
+ // area lights only emit from their front face, and the distance is held above the size
148
+ // of the light so positions on or near it do not dominate
149
+ let normal = normalize( cross( light.u, light.v ) );
150
+ let cosTheta = dot( toLight, normal ) * inverseSqrt( distSq );
151
+ return power * light.area * max( cosTheta, 0.0 ) / max( distSq, light.area );
152
+
153
+ }
154
+
155
+ }
156
+ `;
157
+
158
+ this.getLightsWeight = wgslTagFn/* wgsl */`
159
+ fn getLightsWeight( position: vec3f ) -> f32 {
160
+
161
+ var total = 0.0;
162
+ for ( var i = 0u; i < ${ countNode }; i ++ ) {
163
+
164
+ total += ${ this.getLightWeight }( i, position );
165
+
166
+ }
167
+
168
+ return total;
169
+
170
+ }
171
+ `;
172
+
173
+ // Chooses a light, or the environment when the returned index equals the light count, in
174
+ // proportion to the estimated weights. The pdf is zero when nothing can light the position.
175
+ this.selectLight = wgslTagFn/* wgsl */`
176
+ fn selectLight( position: vec3f, envWeight: f32, r: f32, selectionPdf: ptr<function, f32> ) -> u32 {
177
+
178
+ let count = ${ countNode };
179
+ let totalWeight = ${ this.getLightsWeight }( position ) + envWeight;
180
+ if ( totalWeight <= 0.0 ) {
181
+
182
+ *selectionPdf = 0.0;
183
+ return 0u;
184
+
185
+ }
186
+
187
+ let targetWeight = r * totalWeight;
188
+ var accumulated = 0.0;
189
+ var lastWeighted = 0u;
190
+ var index = count;
191
+ for ( var i = 0u; i < count; i ++ ) {
192
+
193
+ let lightWeight = ${ this.getLightWeight }( i, position );
194
+ accumulated += lightWeight;
195
+ if ( lightWeight > 0.0 ) {
196
+
197
+ lastWeighted = i;
198
+
199
+ }
200
+
201
+ if ( targetWeight < accumulated ) {
202
+
203
+ index = i;
204
+ break;
205
+
206
+ }
207
+
208
+ }
209
+
210
+ // rounding can carry the target past the last light when there is no environment
211
+ if ( index == count && envWeight <= 0.0 ) {
212
+
213
+ index = lastWeighted;
214
+
215
+ }
216
+
217
+ var weight = envWeight;
218
+ if ( index < count ) {
219
+
220
+ weight = ${ this.getLightWeight }( index, position );
221
+
222
+ }
223
+
224
+ *selectionPdf = weight / totalWeight;
225
+ return index;
226
+
227
+ }
228
+ `;
229
+
230
+ // sample the light at the given index
93
231
  this.randomLightSample = wgslTagFn/* wgsl */`
94
232
  fn randomLightSample( lightIndex: u32, rayOrigin: vec3f, ruv: vec2f ) -> ${ lightRecordStruct } {
95
233
 
@@ -69,7 +69,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
69
69
 
70
70
  setBVHData( bvhData ) {
71
71
 
72
- this.kernel.bvhData = bvhData;
72
+ this.kernel.context.bvhData = bvhData;
73
73
  this.rebuild();
74
74
 
75
75
  }
@@ -179,6 +179,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
179
179
  kernel,
180
180
  maxBounces,
181
181
  maxTransparentBounces,
182
+ maxSubsurfaceBounces,
182
183
 
183
184
  outputTarget,
184
185
  sampleCountTarget,
@@ -193,6 +194,10 @@ export class MegaKernelPathTracer extends PathTracerBackend {
193
194
 
194
195
  kernel.maxBounces = maxBounces;
195
196
  kernel.maxTransparentBounces = maxTransparentBounces;
197
+ kernel.maxSubsurfaceBounces = maxSubsurfaceBounces;
198
+
199
+ // compile the kernel before its first dispatch
200
+ yield* this.compileKernels( [ kernel ] );
196
201
 
197
202
  // number of tile cycles that have finished, ie. the sample count every pixel has reached
198
203
  let completedSamples = 0;
@@ -201,6 +206,8 @@ export class MegaKernelPathTracer extends PathTracerBackend {
201
206
 
202
207
  // skip the tile loop entirely once every pixel is finished
203
208
  kernel.maxSamples = this.maxSamples;
209
+ kernel.auxTarget = this.auxTarget ?? this._auxPlaceholder;
210
+ kernel.accumulateAux = this.auxTarget ? 1 : 0;
204
211
  if ( this.maxSamples !== 0 && completedSamples >= this.maxSamples ) {
205
212
 
206
213
  yield;
@@ -210,7 +217,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
210
217
 
211
218
  // split the frame into a near square grid of tiles no larger than the budget
212
219
  this.getSize( _size );
213
- const budgetTiles = Math.max( 1, Math.ceil( _size.x * _size.y / this.frameBudget ) );
220
+ const budgetTiles = lowResMode ? 1 : Math.max( 1, Math.ceil( _size.x * _size.y / this.frameBudget ) );
214
221
  tiles.x = Math.ceil( Math.sqrt( budgetTiles ) );
215
222
  tiles.y = Math.ceil( budgetTiles / tiles.x );
216
223
 
@@ -245,7 +252,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
245
252
  for ( let y = 0; y < tiles.y; y ++ ) {
246
253
 
247
254
  kernel.offset.set( x, y ).multiply( tileSize );
248
- renderer.compute( kernel.kernel, dispatchSize );
255
+ this.dispatch( kernel, dispatchSize );
249
256
 
250
257
  // in low res mode the tiles past the first fall outside the target and do no work
251
258
  completedTiles = Math.min( completedTiles + 1, tileCount );
@@ -271,6 +278,7 @@ export class MegaKernelPathTracer extends PathTracerBackend {
271
278
  // TODO: dispose of all buffers
272
279
  this.envInfo.dispose();
273
280
  this.lightsInfo.dispose();
281
+ this.kernel.dispose();
274
282
 
275
283
  }
276
284