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
@@ -2,7 +2,7 @@ import { StorageBufferAttribute } from 'three/webgpu';
2
2
  import { uniform, storage, globalId } from 'three/tsl';
3
3
  import { ComputeKernel } from '../ComputeKernel.js';
4
4
  import { wgslTagFn } from 'three-mesh-bvh/webgpu';
5
- import { rayDataStruct, pixelQueueStruct } from './structs.js';
5
+ import { rayDataStruct, PIXEL_QUEUE_ELEMENT_COUNT, PIXEL_QUEUE_HEADER } from './structs.js';
6
6
 
7
7
  // Moves pixels between a range of path slots and the queue when the pool is resized. Slots added
8
8
  // by a grow take a pixel from the queue tail and start idle, as MaterialKernel expects. Slots
@@ -14,7 +14,7 @@ export class ResetSlotsKernel extends ComputeKernel {
14
14
 
15
15
  const params = {
16
16
  rayDataStorage: storage( new StorageBufferAttribute( 1, 1 ), rayDataStruct ),
17
- pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), pixelQueueStruct ),
17
+ pixelQueue: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ).toAtomic(),
18
18
  start: uniform( 0, 'uint' ),
19
19
  end: uniform( 0, 'uint' ),
20
20
  addingSlots: uniform( 0, 'uint' ),
@@ -37,14 +37,14 @@ export class ResetSlotsKernel extends ComputeKernel {
37
37
  if ( addingSlots == 0u ) {
38
38
 
39
39
  // slots being removed hand their pixel back before the pool is discarded
40
- let queueIndex = atomicAdd( &pixelQueue.elementCount, 1u );
41
- atomicStore( &pixelQueue.elements[ queueIndex ], rayDataStorage[ index ].pixelIndex );
40
+ let queueIndex = atomicAdd( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
41
+ atomicStore( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + queueIndex ], rayDataStorage[ index ].pixelIndex );
42
42
 
43
43
  } else {
44
44
 
45
45
  // the pool never exceeds the pixel count, so the queue holds enough for every new slot
46
- let previousCount = atomicSub( &pixelQueue.elementCount, 1u );
47
- rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue.elements[ previousCount - 1u ] );
46
+ let previousCount = atomicSub( &pixelQueue[ ${ PIXEL_QUEUE_ELEMENT_COUNT }u ], 1u );
47
+ rayDataStorage[ index ].pixelIndex = atomicLoad( &pixelQueue[ ${ PIXEL_QUEUE_HEADER }u + previousCount - 1u ] );
48
48
  rayDataStorage[ index ].resultColor = vec4f( 0.0 );
49
49
  rayDataStorage[ index ].throughputColor = vec3f( 0.0 );
50
50
  rayDataStorage[ index ].objectIndex = - 1;
@@ -1,9 +1,9 @@
1
1
  import { StorageBufferAttribute } from 'three/webgpu';
2
2
  import { ComputeKernel } from '../ComputeKernel.js';
3
3
  import { storage, globalId } from 'three/tsl';
4
- import { proxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
4
+ import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
5
  import { rngInit } from '../../nodes/random.wgsl.js';
6
- import { rayQueueStruct, intersectionResultStruct } from './structs.js';
6
+ import { traceQueuedRayStruct, intersectionResultStruct, RAY_QUEUE_LENGTH } from './structs.js';
7
7
 
8
8
  // Pure BVH traversal over the queued bounce rays: one thread per queued ray, writing a compact
9
9
  // intersection result at the ray's queue index for LogicKernel to consume next frame.
@@ -12,34 +12,36 @@ export class TraceRayKernel extends ComputeKernel {
12
12
  constructor( ) {
13
13
 
14
14
  const params = {
15
- bvhData: { value: null },
16
15
 
17
- rayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueStruct ),
16
+ rayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
17
+ queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
18
18
  rayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
19
19
 
20
20
  globalId: globalId,
21
21
  };
22
22
 
23
- const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
24
- const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params );
23
+ const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
24
+ const raycastFirstHitFn = contextProxy( 'bvhData.fns.raycastFirstHit' );
25
25
 
26
26
  const fn = wgslTagFn /* wgsl */`
27
27
 
28
28
  fn compute( globalId: vec3u ) -> void {
29
29
 
30
30
  let rayQueue = &${ params.rayQueue };
31
+ let queueCounters = &${ params.queueCounters };
31
32
  let rayIntersectionsStorage = &${ params.rayIntersectionsStorage };
32
33
 
33
34
  let index = globalId.x;
34
- if ( index >= rayQueue.length ) {
35
+ if ( index >= queueCounters[ ${ RAY_QUEUE_LENGTH }u ] ) {
35
36
 
36
37
  return;
37
38
 
38
39
  }
39
40
 
40
- let queuedRay = ( *rayQueue ).elements[ index ];
41
+ // whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
42
+ let queuedRay = ${ params.rayQueue }[ index ];
41
43
  let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
42
- ${ rngInit }( indexUV, queuedRay.seed, queuedRay.currentBounce + queuedRay.alphaDepth );
44
+ ${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
43
45
 
44
46
  let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
45
47
  var hitResult: ${ raycastOutput };
@@ -1,59 +1,54 @@
1
1
  import { StorageBufferAttribute } from 'three/webgpu';
2
2
  import { ComputeKernel } from '../ComputeKernel.js';
3
3
  import { storage, globalId } from 'three/tsl';
4
- import { proxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
4
+ import { contextProxy, wgslTagFn } from 'three-mesh-bvh/webgpu';
5
5
  import { rngInit } from '../../nodes/random.wgsl.js';
6
- import { rayQueueStruct, intersectionResultStruct } from './structs.js';
6
+ import { traceQueuedRayStruct, shadowTraceResultStruct, SHADOW_RAY_QUEUE_LENGTH } from './structs.js';
7
7
 
8
- // Pure BVH traversal over the queued shadow rays. Uses the same first-hit traversal as the bounce
9
- // rays ( no dedicated any-hit traversal exists yet ); LogicKernel decides occlusion by comparing the
10
- // hit distance against the light distance.
8
+ // Pure BVH traversal over the queued shadow rays. Uses an any-hit traversal without face culling that
9
+ // passes partially transparent surfaces, accumulating the light they block.
11
10
  export class TraceShadowRayKernel extends ComputeKernel {
12
11
 
13
12
  constructor( ) {
14
13
 
15
14
  const params = {
16
- bvhData: { value: null },
17
15
 
18
- shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), rayQueueStruct ),
19
- shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), intersectionResultStruct ),
16
+ shadowRayQueue: storage( new StorageBufferAttribute( 1, 1 ), traceQueuedRayStruct ),
17
+ queueCounters: storage( new StorageBufferAttribute( 1, 1 ), 'uint' ),
18
+ shadowRayIntersectionsStorage: storage( new StorageBufferAttribute( 1, 1 ), shadowTraceResultStruct ),
20
19
 
21
20
  globalId: globalId,
22
21
  };
23
22
 
24
- const raycastOutput = proxy( 'bvhData.value.fns.raycastFirstHit.outputType', params );
25
- const raycastFirstHitFn = proxy( 'bvhData.value.fns.raycastFirstHit', params );
23
+ const raycastOutput = contextProxy( 'bvhData.fns.raycastFirstHit.outputType' );
24
+ const raycastShadowFn = contextProxy( 'bvhData.fns.raycastShadow' );
26
25
 
27
26
  const fn = wgslTagFn /* wgsl */`
28
27
 
29
28
  fn compute( globalId: vec3u ) -> void {
30
29
 
31
30
  let shadowRayQueue = &${ params.shadowRayQueue };
31
+ let queueCounters = &${ params.queueCounters };
32
32
  let shadowRayIntersectionsStorage = &${ params.shadowRayIntersectionsStorage };
33
33
 
34
34
  let index = globalId.x;
35
- if ( index >= shadowRayQueue.length ) {
35
+ if ( index >= queueCounters[ ${ SHADOW_RAY_QUEUE_LENGTH }u ] ) {
36
36
 
37
37
  return;
38
38
 
39
39
  }
40
40
 
41
- let queuedRay = ( *shadowRayQueue ).elements[ index ];
41
+ // whole structs are read from the buffer, not the pointer alias, which Safari fails to compile
42
+ let queuedRay = ${ params.shadowRayQueue }[ index ];
42
43
  let indexUV = vec2u( queuedRay.pixelIndex >> 16, queuedRay.pixelIndex & 0xFFFF );
43
- ${ rngInit }( indexUV, queuedRay.seed, queuedRay.currentBounce + queuedRay.alphaDepth );
44
+ ${ rngInit }( indexUV, queuedRay.seed, queuedRay.rngIndex );
44
45
 
45
46
  let ray = Ray( queuedRay.origin, queuedRay.direction, queuedRay.maxDist );
46
47
  var hitResult: ${ raycastOutput };
47
- if ( ${ raycastFirstHitFn }( ray, &hitResult ) ) {
48
-
49
- shadowRayIntersectionsStorage[ index ].objectIndex = i32( hitResult.objectIndex );
50
- shadowRayIntersectionsStorage[ index ].dist = hitResult.dist;
51
-
52
- } else {
53
-
54
- shadowRayIntersectionsStorage[ index ].objectIndex = - 1;
55
-
56
- }
48
+ let occluded = ${ raycastShadowFn }( ray, &hitResult );
49
+ shadowRayIntersectionsStorage[ index ].objectIndex = select( - 1, i32( hitResult.objectIndex ), occluded );
50
+ shadowRayIntersectionsStorage[ index ].skippedOcclusion = hitResult.skippedOcclusion;
51
+ shadowRayIntersectionsStorage[ index ].occlusion = hitResult.occlusion;
57
52
 
58
53
  }
59
54
  `;
@@ -1,4 +1,5 @@
1
1
  import { StructTypeNode } from 'three/webgpu';
2
+ import { mediumStateStruct } from '../../nodes/medium.wgsl.js';
2
3
 
3
4
  // StructTypeNode that force-emits dependency structs. TSL can't discover a struct referenced only via
4
5
  // a type string, so we build each dependency first, which registers its definition ahead of this struct.
@@ -28,7 +29,7 @@ class DependentStructTypeNode extends StructTypeNode {
28
29
 
29
30
  // Persistent per-path-slot state. One slot per in-flight path, holding everything needed to resolve
30
31
  // the previous frame's trace results and stage the next bounce.
31
- export const rayDataStruct = new StructTypeNode( {
32
+ const rayDataMembers = {
32
33
 
33
34
  throughputColor: 'vec3f',
34
35
  currentBounce: 'uint',
@@ -76,8 +77,10 @@ export const rayDataStruct = new StructTypeNode( {
76
77
  // the traced segment length, for the transmission attenuation applied by MaterialKernel
77
78
  dist: 'float',
78
79
 
79
- // alpha test pass throughs, counted separately from the bounce count
80
+ // alpha test pass throughs, counted separately from the bounce count, and how far along the ray they
81
+ // moved the start of its traversal. The ray keeps the origin it scattered from
80
82
  alphaDepth: 'uint',
83
+ rayStart: 'float',
81
84
 
82
85
  // the camera segment's maximum trace distance, carried across alpha pass throughs
83
86
  maxDist: 'float',
@@ -86,7 +89,30 @@ export const rayDataStruct = new StructTypeNode( {
86
89
  // first dispersive interaction
87
90
  dispersionWavelength: 'float',
88
91
 
89
- }, 'RayData' );
92
+ // the first scatter's albedo and normal, 8 bits per channel, for the denoiser's auxiliary buffers
93
+ auxAlbedo: 'uint',
94
+ auxNormal: 'uint',
95
+
96
+ // scatter events taken inside media, counted separately from the bounce count
97
+ subsurfaceBounces: 'uint',
98
+
99
+ // whether MediumKernel walks the path's next segment, cleared once the walk reaches a surface or ends
100
+ walkPending: 'uint',
101
+
102
+ // whether the staged segment starts at a medium scatter event, where no light is sampled
103
+ mediumScattered: 'uint',
104
+
105
+ };
106
+
107
+ // The path's medium, nested at the end to share the ray data buffer under the storage buffer limit. Sized
108
+ // separately since nested structs aren't.
109
+ export const rayDataStruct = new DependentStructTypeNode( {
110
+ ...rayDataMembers,
111
+ medium: mediumStateStruct.name,
112
+ }, 'RayData', [ mediumStateStruct ] );
113
+
114
+ const rayDataLength = new StructTypeNode( rayDataMembers ).getLength();
115
+ rayDataStruct.getLength = () => rayDataLength + mediumStateStruct.getLength();
90
116
 
91
117
  // A ray queued for BVH traversal by the trace kernels. A "maxDist" of zero traces unbounded.
92
118
  export const traceQueuedRayStruct = new StructTypeNode( {
@@ -95,12 +121,14 @@ export const traceQueuedRayStruct = new StructTypeNode( {
95
121
  pixelIndex: 'uint',
96
122
 
97
123
  direction: 'vec3f',
98
- currentBounce: 'uint',
124
+
125
+ // the traversal's random sequence index: bounces, alpha pass throughs and subsurface bounces, as in the megakernel
126
+ rngIndex: 'uint',
99
127
 
100
128
  seed: 'uint',
101
- alphaDepth: 'uint',
102
129
  maxDist: 'float',
103
130
  _alignment0: 'uint',
131
+ _alignment1: 'uint',
104
132
 
105
133
  }, 'TraceQueuedRay' );
106
134
 
@@ -118,38 +146,38 @@ export const intersectionResultStruct = new StructTypeNode( {
118
146
  side: 'float',
119
147
 
120
148
  indices: 'vec3u',
121
- _alignment0: 'uint',
122
149
 
123
150
  }, 'TraceResult' );
124
151
 
125
- // Queue wrappers that keep an append-only length counter in a header ahead of the elements. The
126
- // atomic variant is bound where rays are pushed with atomicAdd; the plain one where the length is
127
- // only read or reset. getLength returns just the header since the trailing array is runtime-sized.
128
- export const rayQueueStruct = new DependentStructTypeNode( {
129
- length: 'uint',
130
- elements: `array<${ traceQueuedRayStruct.name }>`,
131
- }, 'RayQueue', [ traceQueuedRayStruct ] );
132
- rayQueueStruct.getLength = () => 4;
133
-
134
- export const rayQueueAtomicStruct = new DependentStructTypeNode( {
135
- length: { type: 'uint', atomic: true },
136
- elements: `array<${ traceQueuedRayStruct.name }>`,
137
- }, 'RayQueue', [ traceQueuedRayStruct ] );
138
- rayQueueAtomicStruct.getLength = () => 4;
152
+ // Shadow ray trace result, written by TraceShadowRayKernel at the ray's queue index and consumed by
153
+ // LogicKernel the following frame. objectIndex >= 0 encodes an occluded light.
154
+ export const shadowTraceResultStruct = new StructTypeNode( {
155
+
156
+ objectIndex: 'int',
157
+
158
+ // the share of bsdf rays the skipped faces would stop, whose light weight moves to the light sample
159
+ skippedOcclusion: 'float',
160
+
161
+ // the fraction of light blocked by the partially transparent surfaces passed
162
+ occlusion: 'float',
163
+
164
+ }, 'ShadowTraceResult' );
165
+
166
+ // The ray and shadow ray queues are plain arrays of queued rays, with their append-only lengths in a
167
+ // separate counters buffer: storage buffers are always bound as arrays, which can't hold a struct
168
+ // ending in a runtime-sized array.
169
+ export const RAY_QUEUE_LENGTH = 0;
170
+ export const SHADOW_RAY_QUEUE_LENGTH = 1;
171
+ export const QUEUE_COUNTER_COUNT = 2;
172
+
173
+ // The path slots walking through a medium this pass, for MediumKernel. Its length leads the same u32
174
+ // array, ahead of the elements, as MediumKernel is at its storage buffer limit.
175
+ export const MEDIUM_QUEUE_LENGTH = 0;
176
+ export const MEDIUM_QUEUE_HEADER = 1;
139
177
 
140
178
  // Round-robin queue of pixel indices waiting for a free path slot when the output resolution exceeds
141
- // the ray data pool. Sized to the pixel count so retiring slots can always return their pixel. The
142
- // non-atomic variant is used for contention-free initialization.
143
- export const pixelQueueStruct = new DependentStructTypeNode( {
144
- current: { type: 'uint', atomic: true },
145
- elementCount: { type: 'uint', atomic: true },
146
- elements: 'array<atomic<u32>>',
147
- }, 'PixelQueue' );
148
- pixelQueueStruct.getLength = () => 2;
149
-
150
- export const pixelQueueNonAtomicStruct = new DependentStructTypeNode( {
151
- current: 'uint',
152
- elementCount: 'uint',
153
- elements: 'array<u32>',
154
- }, 'PixelQueue' );
155
- pixelQueueNonAtomicStruct.getLength = () => 2;
179
+ // the ray data pool, sized to the pixel count so retiring slots can always return their pixel. Its
180
+ // counters lead the same u32 array, ahead of the elements.
181
+ export const PIXEL_QUEUE_CURRENT = 0;
182
+ export const PIXEL_QUEUE_ELEMENT_COUNT = 1;
183
+ export const PIXEL_QUEUE_HEADER = 2;
@@ -13,7 +13,7 @@ import * as bluedither from './nodes/rand/bluedither.wgsl.js';
13
13
  export const RANDOM_PCG = pcg;
14
14
 
15
15
  /**
16
- * Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
16
+ * Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`. The default.
17
17
  * @section Random Strategies
18
18
  * @type {Object}
19
19
  */
@@ -21,7 +21,7 @@ export const RANDOM_SOBOL = sobol;
21
21
 
22
22
  /**
23
23
  * The same Sobol sequence for every pixel, offset by a blue noise dither so early frames read as
24
- * smooth grain rather than clumped noise. The default.
24
+ * smooth grain rather than clumped noise.
25
25
  * @section Random Strategies
26
26
  * @type {Object}
27
27
  */
@@ -1,8 +1,20 @@
1
- import { ExternalTexture, NearestFilter, NoToneMapping, RenderTarget, UnsignedByteType } from 'three/webgpu';
2
- import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
1
+ import { EventDispatcher, ExternalTexture } from 'three/webgpu';
2
+ import { UnpackAuxKernel } from '../compute/UnpackAuxKernel.js';
3
+ import { createAuxImageTarget } from '../compute/targets.js';
3
4
 
4
- /** @import { Camera, Scene, Texture, WebGPURenderer } from 'three/webgpu' */
5
- /** @import { DynamicTileSetting } from 'oidn-web' */
5
+ /** @import { StorageArrayTexture, Texture, WebGPURenderer } from 'three/webgpu' */
6
+ /** @import { DynamicTileSetting } from 'oidn-webgpu' */
7
+
8
+ /**
9
+ * Fired when a pass started by `update` fails, such as the weights failing to download, with the
10
+ * failure as the event's `error`. No other pass starts until a reset.
11
+ * @event OIDNDenoiser#error
12
+ */
13
+
14
+ /**
15
+ * Fired as each tile of a pass finishes, with the fraction of the pass done as `progress`.
16
+ * @event OIDNDenoiser#progress
17
+ */
6
18
 
7
19
  /**
8
20
  * Runs Open Image Denoise over a path traced image. Pass one to
@@ -12,14 +24,14 @@ import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
12
24
  * nor the network files become a dependency.
13
25
  *
14
26
  * ```js
15
- * import { initUNetFromURL } from 'oidn-web';
27
+ * import { initUNetFromURL } from 'oidn-webgpu';
16
28
  * pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
17
29
  * ```
18
30
  *
19
31
  * Weights come from the oidn-weights repository, where the "_small" and "_large" variants trade
20
32
  * quality against download size and per tile cost.
21
33
  */
22
- export class OIDNDenoiser {
34
+ export class OIDNDenoiser extends EventDispatcher {
23
35
 
24
36
  /**
25
37
  * The denoised result, or null until the first tile has been produced.
@@ -69,6 +81,8 @@ export class OIDNDenoiser {
69
81
  */
70
82
  constructor( options = {} ) {
71
83
 
84
+ super();
85
+
72
86
  const {
73
87
  initUNetFromURL,
74
88
  auxWeightsUrl,
@@ -80,7 +94,7 @@ export class OIDNDenoiser {
80
94
 
81
95
  if ( ! initUNetFromURL ) {
82
96
 
83
- throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-web" must be provided.' );
97
+ throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-webgpu" must be provided.' );
84
98
 
85
99
  }
86
100
 
@@ -88,27 +102,32 @@ export class OIDNDenoiser {
88
102
  this.auxWeightsUrl = auxWeightsUrl;
89
103
  this.colorWeightsUrl = colorWeightsUrl;
90
104
 
91
- // `false` falls back to the color only model, which is blurrier but skips a scene render
105
+ // `false` falls back to the color only model, which is blurrier but needs no auxiliary buffers
92
106
  this.useAuxiliaryBuffers = useAuxiliaryBuffers;
93
107
 
94
- // tiling, passed through to oidn-web. Null keeps its defaults
108
+ // tiling, passed through to oidn-webgpu. Null keeps its defaults
95
109
  this.maxTileSize = maxTileSize;
96
110
  this.dynamicTile = dynamicTile;
97
111
 
98
112
  this.renderer = null;
99
- this.scene = null;
100
- this.camera = null;
101
113
 
102
114
  this._texture = null;
103
115
  this._running = false;
104
116
  this._complete = false;
105
117
  this._abort = null;
106
118
  this._requestId = 0;
119
+
120
+ // set when a pass started by "update" fails, so the render loop doesn't start another until a reset
121
+ this._failed = false;
107
122
  this._unets = { aux: null, color: null };
108
123
 
109
124
  this._resultPipeline = null;
110
125
  this._rawTexture = null;
111
- this._auxTarget = null;
126
+
127
+ // the albedo and normal unpacked from the path tracer's auxiliary target for a pass
128
+ this._unpackAuxKernel = new UnpackAuxKernel().setWorkgroupSize( 8, 8, 1 );
129
+ this._albedoTarget = createAuxImageTarget( 'Denoiser Albedo' );
130
+ this._normalTarget = createAuxImageTarget( 'Denoiser Normal' );
112
131
 
113
132
  }
114
133
 
@@ -122,41 +141,40 @@ export class OIDNDenoiser {
122
141
  }
123
142
 
124
143
  /**
125
- * @param {Scene} scene
126
- * @param {Camera} camera
127
- */
128
- setScene( scene, camera ) {
129
-
130
- this.scene = scene;
131
- this.camera = camera;
132
-
133
- }
134
-
135
- /**
136
- * Renders the auxiliary buffers and starts a pass. Safe to call every frame.
144
+ * Unpacks the auxiliary buffers the path tracer accumulated and starts a pass. Safe to call
145
+ * every frame.
137
146
  *
138
147
  * @param {Texture} target - The path traced result, in linear HDR.
148
+ * @param {StorageArrayTexture|null} aux - The packed auxiliary buffers, see "aux.wgsl.js".
139
149
  * @returns {Texture|null}
140
150
  */
141
- update( target ) {
151
+ update( target, aux = null ) {
142
152
 
143
- if ( this._complete || this._running ) {
153
+ if ( this._complete || this._running || this._failed ) {
144
154
 
145
155
  return this._texture;
146
156
 
147
157
  }
148
158
 
159
+ // TODO: denoise the alpha for transparent backgrounds. oidn-webgpu's "denoiseAlpha" runs as a
160
+ // second pass, but its "_updatePasses" doesn't rebuild its shaders when the option changes
149
161
  let albedo = null;
150
162
  let normal = null;
151
- if ( this.useAuxiliaryBuffers ) {
163
+ if ( this.useAuxiliaryBuffers && aux ) {
152
164
 
153
- const auxTarget = this._renderAuxiliaryBuffers( target.width, target.height );
154
- albedo = auxTarget.textures[ 0 ];
155
- normal = auxTarget.textures[ 1 ];
165
+ this._unpackAux( aux );
166
+ albedo = this._albedoTarget;
167
+ normal = this._normalTarget;
156
168
 
157
169
  }
158
170
 
159
- this.denoise( target, albedo, normal );
171
+ // a failure, such as the weights failing to download, is reported once rather than every frame
172
+ this.denoise( target, albedo, normal ).catch( error => {
173
+
174
+ this._failed = true;
175
+ this.dispatchEvent( { type: 'error', error } );
176
+
177
+ } );
160
178
 
161
179
  return this._texture;
162
180
 
@@ -213,14 +231,22 @@ export class OIDNDenoiser {
213
231
  const inputs = {
214
232
  color: { data: backend.get( color ).texture, width, height },
215
233
 
234
+ // pace the tiles to display frames so the path tracer keeps rendering between them
235
+ scheduling: 'animation-frame',
236
+
216
237
  // the output is seeded with the raw color, so copying per tile shows a progressive wipe
217
- progress: output => this._copyOutputToTexture( output, width, height ),
218
- done: output => {
238
+ progress: ( output, tileData, tile, tileIndex, tileCount ) => {
219
239
 
220
240
  this._copyOutputToTexture( output, width, height );
241
+ this.dispatchEvent( { type: 'progress', progress: ( tileIndex + 1 ) / tileCount } );
242
+
243
+ },
244
+ done: output => {
245
+
221
246
  this._running = false;
222
247
  this._complete = true;
223
248
  this._abort = null;
249
+ this._disposeAuxImages();
224
250
 
225
251
  },
226
252
  };
@@ -254,17 +280,18 @@ export class OIDNDenoiser {
254
280
  this._rawTexture?.destroy();
255
281
  this._rawTexture = null;
256
282
 
283
+ this._disposeAuxImages();
284
+
257
285
  this._running = false;
258
286
  this._complete = false;
287
+ this._failed = false;
259
288
 
260
289
  }
261
290
 
262
291
  dispose() {
263
292
 
264
293
  this.reset();
265
-
266
- this._auxTarget?.dispose();
267
- this._auxTarget = null;
294
+ this._unpackAuxKernel.dispose();
268
295
 
269
296
  // the networks hold the weights on the GPU, and a load may still be in flight
270
297
  for ( const key in this._unets ) {
@@ -276,52 +303,28 @@ export class OIDNDenoiser {
276
303
 
277
304
  }
278
305
 
279
- // Rasterizes the albedo and normal buffers that guide the filter. oidn-web takes normals
280
- // mapped into [0,1], with (0.5, 0.5, 1) as a flat normal.
281
- _renderAuxiliaryBuffers( width, height ) {
306
+ // Unpacks the path tracer's auxiliary target into albedo and normal textures, since oidn-webgpu
307
+ // reads every input as the same kind as the color texture
308
+ _unpackAux( aux ) {
282
309
 
283
- const { renderer, scene, camera } = this;
310
+ const { renderer, _unpackAuxKernel, _albedoTarget, _normalTarget } = this;
311
+ const { width, height } = aux.image;
284
312
 
285
- if ( ! this._auxTarget ) {
313
+ _albedoTarget.setSize( width, height );
314
+ _normalTarget.setSize( width, height );
286
315
 
287
- // Eight bits per channel since both buffers hold [0,1]. Multisampled so the rasterized
288
- // silhouettes match the jittered path traced ones.
289
- this._auxTarget = new RenderTarget( 1, 1, {
290
- count: 2,
291
- type: UnsignedByteType,
292
- minFilter: NearestFilter,
293
- magFilter: NearestFilter,
294
- samples: 4,
295
- } );
296
-
297
- // the MRT keys are matched against these names
298
- this._auxTarget.textures[ 0 ].name = 'output';
299
- this._auxTarget.textures[ 1 ].name = 'normal';
300
-
301
- }
316
+ _unpackAuxKernel.auxTarget = aux;
317
+ _unpackAuxKernel.albedoTarget = _albedoTarget;
318
+ _unpackAuxKernel.normalTarget = _normalTarget;
319
+ renderer.compute( _unpackAuxKernel.kernel, _unpackAuxKernel.getDispatchSize( width, height ) );
302
320
 
303
- const auxTarget = this._auxTarget;
304
- auxTarget.setSize( width, height );
305
-
306
- const originalTarget = renderer.getRenderTarget();
307
- const originalMRT = renderer.getMRT();
308
- const originalToneMapping = renderer.toneMapping;
309
-
310
- // the buffers are data rather than an image, so they must not be tone mapped
311
- renderer.toneMapping = NoToneMapping;
312
- renderer.setRenderTarget( auxTarget );
313
- renderer.setMRT( mrt( {
314
- output: diffuseColor,
315
- normal: vec4( normalView.mul( 0.5 ).add( 0.5 ), 1.0 ),
316
- } ) );
317
-
318
- renderer.render( scene, camera );
321
+ }
319
322
 
320
- renderer.setMRT( originalMRT );
321
- renderer.setRenderTarget( originalTarget );
322
- renderer.toneMapping = originalToneMapping;
323
+ // the unpacked images only live for a pass
324
+ _disposeAuxImages() {
323
325
 
324
- return auxTarget;
326
+ this._albedoTarget.dispose();
327
+ this._normalTarget.dispose();
325
328
 
326
329
  }
327
330
 
@@ -346,7 +349,7 @@ export class OIDNDenoiser {
346
349
  if ( this.maxTileSize !== null ) options.maxTileSize = this.maxTileSize;
347
350
  if ( this.dynamicTile !== null ) options.dynamicTile = this.dynamicTile;
348
351
 
349
- return this.initUNetFromURL( url, { device, adapterInfo: device.adapterInfo }, options );
352
+ return this.initUNetFromURL( url, { device }, options );
350
353
 
351
354
  } )();
352
355
 
@@ -358,7 +361,7 @@ export class OIDNDenoiser {
358
361
 
359
362
  // Copies the denoiser's packed output buffer into a sampleable texture, using a compute pass
360
363
  // since copyBufferToTexture requires 256 byte row alignment.
361
- // TODO: remove the copy once oidn-web can write into a caller-provided storage texture.
364
+ // TODO: remove the copy once oidn-webgpu can write into a caller-provided storage texture.
362
365
  _copyOutputToTexture( output, width, height ) {
363
366
 
364
367
  const device = this.renderer.backend.device;