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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-gpu-pathtracer",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "description": "Path tracing renderer and utilities for three.js built on top of three-mesh-bvh.",
5
5
  "module": "src/index.js",
6
6
  "main": "build/index.umd.cjs",
@@ -45,7 +45,7 @@
45
45
  "@monogrid/gainmap-js": "^3.0.5",
46
46
  "@pmndrs/upscaler": "^0.3.0",
47
47
  "@types/node": "^24.3.1",
48
- "@types/three": "^0.185.1",
48
+ "@types/three": "^0.186.0",
49
49
  "@typescript-eslint/eslint-plugin": "^8.40.0",
50
50
  "@vitejs/plugin-basic-ssl": "^2.3.0",
51
51
  "@webgpu/types": "^0.1.72",
@@ -56,22 +56,22 @@
56
56
  "jsdoc": "^4.0.5",
57
57
  "mediabunny": "^1.50.9",
58
58
  "node-fetch": "^3.2.9",
59
- "oidn-web": "^0.4.0",
59
+ "oidn-webgpu": "^0.6.0",
60
60
  "pixelmatch": "^5.3.0",
61
61
  "pngjs": "^6.0.0",
62
62
  "process": "^0.11.10",
63
63
  "puppeteer": "^24.42.0",
64
64
  "rollup": "^2.70.0",
65
65
  "simple-git": "^3.10.0",
66
- "three": "^0.185.1",
67
- "three-mesh-bvh": "^0.9.15",
66
+ "three": "^0.187.0",
67
+ "three-mesh-bvh": "^0.9.17",
68
68
  "typescript": "^5.9.2",
69
69
  "vite": "^8.2.2",
70
70
  "yargs": "^17.5.1"
71
71
  },
72
72
  "peerDependencies": {
73
- "three": ">=0.185.0",
74
- "three-mesh-bvh": ">=0.9.15",
73
+ "three": ">=0.187.0",
74
+ "three-mesh-bvh": ">=0.9.17",
75
75
  "xatlas-web": "^0.1.0"
76
76
  },
77
77
  "scripts": {
@@ -177,7 +177,7 @@ export class PathTracingSceneGenerator {
177
177
  // generate the geometry
178
178
  const result = staticGeometryGenerator.generate( geometry );
179
179
  const materials = result.materials;
180
- let needsMaterialIndexUpdate = result.changeType !== NO_CHANGE || this._materialUuids === null || this._materialUuids.length !== length;
180
+ let needsMaterialIndexUpdate = result.changeType !== NO_CHANGE || this._materialUuids === null || this._materialUuids.length !== materials.length;
181
181
  if ( ! needsMaterialIndexUpdate ) {
182
182
 
183
183
  for ( let i = 0, length = materials.length; i < length; i ++ ) {
@@ -1,4 +1,4 @@
1
- import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, Source, RepeatWrapping, RGBAFormat, FloatType, ClampToEdgeWrapping } from 'three';
1
+ import { DataTexture, RedFormat, LinearFilter, DataUtils, HalfFloatType, TextureSource, RepeatWrapping, RGBAFormat, FloatType, ClampToEdgeWrapping } from 'three';
2
2
  import { toHalfFloatArray } from '../utils/TextureUtils.js';
3
3
 
4
4
  function binarySearchFindClosestIndexOf( array, targetValue, offset = 0, count = array.length ) {
@@ -42,7 +42,7 @@ function colorToLuminance( r, g, b ) {
42
42
  function preprocessEnvMap( envMap, targetType = HalfFloatType ) {
43
43
 
44
44
  const map = envMap.clone();
45
- map.source = new Source( { ...map.image } );
45
+ map.source = new TextureSource( { ...map.image } );
46
46
  const { width, height, data } = map.image;
47
47
 
48
48
  // TODO: is there a simple way to avoid cloning and adjusting the env map data here?
@@ -311,6 +311,8 @@ export class EquirectHdrInfoUniform {
311
311
 
312
312
  this.totalSum = totalSumValue;
313
313
  this.map = map;
314
+ this.cdfMarginal = cdfMarginal;
315
+ this.cdfConditional = cdfConditional;
314
316
 
315
317
  }
316
318
 
package/src/webgpu/API.md CHANGED
@@ -17,7 +17,7 @@ Uncorrelated pseudo random samples. Converges slowest but never shows structured
17
17
  RANDOM_SOBOL: Object
18
18
  ```
19
19
 
20
- Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
20
+ Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`. The default.
21
21
 
22
22
  ### RANDOM_BLUE_DITHER
23
23
 
@@ -26,7 +26,7 @@ RANDOM_BLUE_DITHER: Object
26
26
  ```
27
27
 
28
28
  The same Sobol sequence for every pixel, offset by a blue noise dither so early frames read as
29
- smooth grain rather than clumped noise. The default.
29
+ smooth grain rather than clumped noise.
30
30
 
31
31
  ## Transmissive Background Modes
32
32
 
@@ -143,7 +143,7 @@ Runs Open Image Denoise over a path traced image. Pass one to
143
143
  nor the network files become a dependency.
144
144
 
145
145
  ```js
146
- import { initUNetFromURL } from 'oidn-web';
146
+ import { initUNetFromURL } from 'oidn-webgpu';
147
147
  pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
148
148
  ```
149
149
 
@@ -151,6 +151,17 @@ Weights come from the oidn-weights repository, where the "_small" and "_large" v
151
151
  quality against download size and per tile cost.
152
152
 
153
153
 
154
+ ### events
155
+
156
+ ```js
157
+ // Fired when a pass started by `update` fails, such as the weights failing to download, with the
158
+ // failure as the event's `error`. No other pass starts until a reset.
159
+ { type: 'error' }
160
+
161
+ // Fired as each tile of a pass finishes, with the fraction of the pass done as `progress`.
162
+ { type: 'progress' }
163
+ ```
164
+
154
165
  ### .texture
155
166
 
156
167
  ```js
@@ -210,20 +221,17 @@ init( renderer: WebGPURenderer ): void
210
221
  ```
211
222
 
212
223
 
213
- ### .setScene
214
-
215
- ```js
216
- setScene( scene: Scene, camera: Camera ): void
217
- ```
218
-
219
-
220
224
  ### .update
221
225
 
222
226
  ```js
223
- update( target: Texture ): Texture | null
227
+ update(
228
+ target: Texture,
229
+ aux = null: StorageArrayTexture | null
230
+ ): Texture | null
224
231
  ```
225
232
 
226
- Renders the auxiliary buffers and starts a pass. Safe to call every frame.
233
+ Unpacks the auxiliary buffers the path tracer accumulated and starts a pass. Safe to call
234
+ every frame.
227
235
 
228
236
 
229
237
  ### .denoise
@@ -261,6 +269,19 @@ the environment afterward require the matching `update*` function. Any change th
261
269
  the accumulated image restarts it.
262
270
 
263
271
 
272
+ ### events
273
+
274
+ ```js
275
+ // Fired once every pixel has reached `maxSamples` and an attached denoiser has finished. Fires
276
+ // again after a reset, or after `maxSamples` is raised, once the render has caught up.
277
+ { type: 'complete' }
278
+
279
+ // Fired as the render advances. `stage` is "building", "compiling", "rendering" or "denoising", and
280
+ // `progress` is the fraction of it done, or null when unknown. "rendering" also gives the sample
281
+ // counts as `samples`, reported a few times a second.
282
+ { type: 'progress' }
283
+ ```
284
+
264
285
  ### .maxBounces
265
286
 
266
287
  ```js
@@ -277,20 +298,40 @@ resolve more indirect light at the cost of speed.
277
298
  frameBudget: number = 250000
278
299
  ```
279
300
 
280
- Number of path slots dispatched per [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call,
301
+ Number of path steps dispatched per [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call,
281
302
  independent of resolution. Raising it trades frame rate for convergence speed.
282
303
 
283
304
 
305
+ ### .frameTime
306
+
307
+ ```js
308
+ readonly frameTime: number
309
+ ```
310
+
311
+ GPU time in milliseconds of a recent [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call. Only measured
312
+ when the renderer is created with `trackTimestamp: true`.
313
+
314
+
284
315
  ### .maxTransparentBounces
285
316
 
286
317
  ```js
287
- maxTransparentBounces: number = 5
318
+ maxTransparentBounces: number = 15
288
319
  ```
289
320
 
290
321
  Maximum number of alpha tested surfaces a ray can pass through. Counted separately from
291
322
  [WebGPUPathTracer#maxBounces](WebGPUPathTracer#maxBounces) so foliage and cutouts cannot exhaust the bounce budget.
292
323
 
293
324
 
325
+ ### .maxSubsurfaceBounces
326
+
327
+ ```js
328
+ maxSubsurfaceBounces: number = 256
329
+ ```
330
+
331
+ Maximum number of scatter events a path can take inside a subsurface scattering volume. Counted
332
+ separately from [WebGPUPathTracer#maxBounces](WebGPUPathTracer#maxBounces). Paths that reach it end, darkening dense volumes.
333
+
334
+
294
335
  ### .maxSamples
295
336
 
296
337
  ```js
@@ -303,6 +344,16 @@ render has stopped, so it has no effect while this is `0`.
303
344
  > [!NOTE]
304
345
  > `0` renders indefinitely.
305
346
 
347
+ ### .complete
348
+
349
+ ```js
350
+ complete: boolean
351
+ ```
352
+
353
+ Whether every pixel has reached [WebGPUPathTracer#maxSamples](WebGPUPathTracer#maxSamples) and an attached denoiser
354
+ has finished, as of the last measurement. Always false while rendering indefinitely.
355
+
356
+
306
357
  ### .transmissiveBackground
307
358
 
308
359
  ```js
@@ -395,6 +446,17 @@ readonly lowResMode: boolean
395
446
  Whether the tracer is currently rendering the low resolution preview.
396
447
 
397
448
 
449
+ ### .compiling
450
+
451
+ ```js
452
+ readonly compiling: boolean
453
+ ```
454
+
455
+ Whether the kernels are compiling, such as after the scene or material changes. Nothing new
456
+ renders until they finish and the canvas keeps the last frame, so an app may draw its own
457
+ preview in the meantime.
458
+
459
+
398
460
  ### .textureAtlas
399
461
 
400
462
  ```js
@@ -410,13 +472,13 @@ The atlas every scene texture is packed into for the kernels to sample.
410
472
  minSamples: number = 1
411
473
  ```
412
474
 
413
- Samples every pixel must reach before the full resolution render is faded in.
475
+ Average samples per pixel the full resolution render must reach before it is faded in.
414
476
 
415
477
 
416
478
  ### .renderDelay
417
479
 
418
480
  ```js
419
- renderDelay: number = 500
481
+ renderDelay: number = 16
420
482
  ```
421
483
 
422
484
  Milliseconds to show the low resolution preview for after a reset.
@@ -425,7 +487,7 @@ Milliseconds to show the low resolution preview for after a reset.
425
487
  ### .fadeDuration
426
488
 
427
489
  ```js
428
- fadeDuration: number = 500
490
+ fadeDuration: number = 250
429
491
  ```
430
492
 
431
493
  Milliseconds taken to cross fade from the preview to the full render.
@@ -446,7 +508,39 @@ Whether to render a low resolution preview while the camera is moving.
446
508
  lowResScale: number = 0.1
447
509
  ```
448
510
 
449
- Resolution of the low resolution preview, as a fraction of the render size.
511
+ Resolution of the low resolution preview, as a fraction of the render size. Adjusted when
512
+ [WebGPUPathTracer#adaptiveDynamicRes](WebGPUPathTracer#adaptiveDynamicRes) is set. The preview never has more pixels than a
513
+ frame of [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) can render.
514
+
515
+
516
+ ### .adaptiveDynamicRes
517
+
518
+ ```js
519
+ adaptiveDynamicRes: boolean = true
520
+ ```
521
+
522
+ Whether [WebGPUPathTracer#lowResScale](WebGPUPathTracer#lowResScale) is adjusted to the pixels a frame of
523
+ [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) can render.
524
+
525
+
526
+ ### .adaptiveFrameBudget
527
+
528
+ ```js
529
+ adaptiveFrameBudget: boolean = true
530
+ ```
531
+
532
+ Whether [WebGPUPathTracer#frameBudget](WebGPUPathTracer#frameBudget) is adjusted to keep each
533
+ [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) call near [WebGPUPathTracer#targetFrameTime](WebGPUPathTracer#targetFrameTime) of
534
+ GPU time. Requires a renderer created with `trackTimestamp: true`.
535
+
536
+
537
+ ### .targetFrameTime
538
+
539
+ ```js
540
+ targetFrameTime: number = 16
541
+ ```
542
+
543
+ GPU time in milliseconds per frame targeted by [WebGPUPathTracer#adaptiveFrameBudget](WebGPUPathTracer#adaptiveFrameBudget).
450
544
 
451
545
 
452
546
  ### .renderScale
@@ -469,6 +563,27 @@ Whether to track the canvas size automatically. Set to `false` to drive the rend
469
563
  size with [WebGPUPathTracer#setSize](WebGPUPathTracer#setSize).
470
564
 
471
565
 
566
+ ### .renderToCanvas
567
+
568
+ ```js
569
+ renderToCanvas: boolean = true
570
+ ```
571
+
572
+ Whether [WebGPUPathTracer#renderSample](WebGPUPathTracer#renderSample) draws the result to the current render
573
+ target, the canvas by default. Set to `false` to only accumulate, as when baking to a
574
+ texture read through [WebGPUPathTracer#target](WebGPUPathTracer#target).
575
+
576
+
577
+ ### .asyncCompile
578
+
579
+ ```js
580
+ asyncCompile: boolean = false
581
+ ```
582
+
583
+ Whether to compile the kernels asynchronously so a compile doesn't stall the page. Nothing
584
+ new renders while [WebGPUPathTracer#compiling](WebGPUPathTracer#compiling) is true.
585
+
586
+
472
587
  ### .generateMissingAttributes
473
588
 
474
589
  ```js
@@ -539,7 +654,8 @@ setDenoiser( denoiser: OIDNDenoiser | null ): void
539
654
  ```
540
655
 
541
656
  Attaches a denoiser, run once the render settles and displayed in place of the raw image.
542
- Settings live on the instance. Pass null to remove it.
657
+ Settings live on the instance. Pass null to remove it. The render restarts when the auxiliary
658
+ buffers it needs are added or dropped.
543
659
 
544
660
 
545
661
  ### .setUpscaler
@@ -564,6 +680,28 @@ against. Call this once, then use the `update*` functions for later changes.
564
680
  > [!NOTE]
565
681
  > Geometry BVHs are built synchronously, so this blocks for large scenes.
566
682
 
683
+ ### .setBVHWorker
684
+
685
+ ```js
686
+ setBVHWorker( WorkerConstructor: function ): void
687
+ ```
688
+
689
+ Sets the worker class [WebGPUPathTracer#setSceneAsync](WebGPUPathTracer#setSceneAsync) builds the mesh geometry BVHs
690
+ with, such as `GenerateMeshBVHWorker` from three-mesh-bvh. Several are created and disposed
691
+ per call so geometries build in parallel.
692
+
693
+
694
+ ### .setSceneAsync
695
+
696
+ ```js
697
+ async setSceneAsync( scene: Scene, camera: Camera ): Promise<void>
698
+ ```
699
+
700
+ Like [WebGPUPathTracer#setScene](WebGPUPathTracer#setScene) but builds the mesh geometry BVHs in the workers set
701
+ with [WebGPUPathTracer#setBVHWorker](WebGPUPathTracer#setBVHWorker) so the page is not blocked. Skinned meshes are
702
+ still built synchronously.
703
+
704
+
567
705
  ### .setRandom
568
706
 
569
707
  ```js
@@ -708,6 +846,16 @@ Draws a heatmap of the per pixel sample counts, for spotting pixels that converg
708
846
  > Measures on every call and draws with the previous result, since the readback lands a
709
847
  > frame later.
710
848
 
849
+ ### .renderAuxBuffer
850
+
851
+ ```js
852
+ renderAuxBuffer( buffer = 'albedo': 'albedo' | 'normal' ): void
853
+ ```
854
+
855
+ Draws the albedo or normal auxiliary buffer accumulated for the denoiser, for checking what
856
+ it is given. Nothing is drawn until a denoiser that uses them is attached.
857
+
858
+
711
859
  ### .dispose
712
860
 
713
861
  ```js
@@ -9,7 +9,6 @@ import {
9
9
  Matrix3,
10
10
  } from 'three/webgpu';
11
11
 
12
- const MAX_TEXTURE_SIZE = 4096;
13
12
  const _prevClearColor = new Color();
14
13
  const _prevMatrix = new Matrix3();
15
14
 
@@ -243,9 +242,9 @@ export class AtlasTexture {
243
242
 
244
243
  }
245
244
 
246
- // calculate the maximum dimension of the atlas
245
+ // calculate the maximum dimension of the atlas, up to what the device allows
247
246
  let maxDim = textures.reduce( ( v, t ) => Math.max( v, t.width, t.height ), 1 );
248
- maxDim = Math.min( MAX_TEXTURE_SIZE, maxDim );
247
+ maxDim = Math.min( renderer.backend.device.limits.maxTextureDimension2D, maxDim );
249
248
 
250
249
  const width = maxDim;
251
250
  const height = maxDim;
@@ -1,20 +1,9 @@
1
- import { Matrix3 } from 'three/webgpu';
2
- import { texture, sampler, uniform, wgslFn, PI } 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,29 +146,22 @@ 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 };
111
-
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 {
119
-
120
- result.pdf = 0.0;
121
-
122
- }
164
+ result.pdf = ${ getTexelPdfFn }( texel, uv.y );
123
165
 
124
166
  return result;
125
167
 
@@ -147,11 +189,10 @@ export class EquirectHdrInfoNode extends EquirectHdrInfoUniform {
147
189
 
148
190
  let rotatedDir = ${ rotationNode } * direction;
149
191
  let mapUv = ${ equirectDirectionToUvFn }( rotatedDir );
150
- let color = textureSampleLevel( ${ mapNode }, ${ mapSampler }, mapUv, 0 ).rgb;
151
- let lum = ${ luminanceFn }( color );
152
192
  let resolution = textureDimensions( ${ mapNode } );
193
+ let texel = min( vec2u( mapUv * vec2f( resolution ) ), resolution - 1u );
153
194
 
154
- return ${ equirectLuminancePdfFn }( lum, ${ totalSumNode }, resolution );
195
+ return ${ getTexelPdfFn }( texel, mapUv.y );
155
196
 
156
197
  }
157
198
  `;
@@ -36,7 +36,7 @@ export class LightsInfoNode extends LightsInfoUniformStruct {
36
36
  // ies profiles packed into an atlas alongside their placement rects
37
37
  this.iesAtlas = new AtlasTexture( { type: HalfFloatType } );
38
38
  this.iesProfilesNode = texture( this.iesAtlas.texture );
39
- this.iesInfoNode = uniformArray( this.iesAtlas.textureInfo, 'uvec4' );
39
+ this.iesInfoNode = uniformArray( this.iesAtlas.textureInfo, 'uvec4' ).setName( 'iesInfo' );
40
40
 
41
41
  this._initFns();
42
42
 
@@ -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 );