three-gpu-pathtracer 0.0.23 → 0.0.25

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 (83) hide show
  1. package/README.md +53 -528
  2. package/build/index.module.js +319 -66
  3. package/build/index.module.js.map +1 -1
  4. package/build/index.umd.cjs +318 -64
  5. package/build/index.umd.cjs.map +1 -1
  6. package/package.json +35 -18
  7. package/src/core/PathTracingSceneGenerator.js +21 -2
  8. package/src/core/WebGLPathTracer.js +16 -7
  9. package/src/core/utils/mergeGeometries.js +14 -1
  10. package/src/core/utils/sceneUpdateUtils.js +8 -12
  11. package/src/detectors/PrecisionMaterial.js +9 -9
  12. package/src/index.d.ts +2 -1
  13. package/src/index.js +1 -0
  14. package/src/materials/pathtracing/PhysicalPathTracingMaterial.js +2 -1
  15. package/src/materials/pathtracing/glsl/attenuate_hit_function.glsl.js +2 -1
  16. package/src/materials/pathtracing/glsl/camera_util_functions.glsl.js +1 -1
  17. package/src/materials/pathtracing/glsl/get_surface_record_function.glsl.js +2 -1
  18. package/src/materials/surface/AmbientOcclusionMaterial.js +2 -0
  19. package/src/objects/PhysicalSpotLight.js +2 -0
  20. package/src/shader/bsdf/bsdf_functions.glsl.js +0 -1
  21. package/src/shader/bvh/inside_fog_volume_function.glsl.js +1 -1
  22. package/src/shader/structs/material_struct.glsl.js +3 -1
  23. package/src/textures/BlueNoiseTexture.js +6 -6
  24. package/src/textures/ProceduralEquirectTexture.js +8 -7
  25. package/src/textures/turquinMetal.png +0 -0
  26. package/src/uniforms/EquirectHdrInfoUniform.js +9 -4
  27. package/src/uniforms/FloatAttributeTextureArray.js +11 -11
  28. package/src/uniforms/MaterialsTexture.js +13 -10
  29. package/src/webgpu/API.md +762 -0
  30. package/src/webgpu/AtlasTexture.js +471 -0
  31. package/src/webgpu/BlurredEnvMapGenerator.js +129 -0
  32. package/src/webgpu/EquirectBackgroundInfo.js +97 -0
  33. package/src/webgpu/EquirectHdrInfoNode.js +151 -0
  34. package/src/webgpu/LightsInfoNode.js +224 -0
  35. package/src/webgpu/MegaKernelPathTracer.js +277 -0
  36. package/src/webgpu/PathTracerBackend.js +216 -0
  37. package/src/webgpu/TurquinTexture.js +169 -0
  38. package/src/webgpu/WaveFrontPathTracer.js +586 -0
  39. package/src/webgpu/WebGPUPathTracer.js +1302 -0
  40. package/src/webgpu/compute/ComputeKernel.js +92 -0
  41. package/src/webgpu/compute/CopyBufferKernel.js +40 -0
  42. package/src/webgpu/compute/PathTracerMegaKernel.js +470 -0
  43. package/src/webgpu/compute/SampleDebugKernel.js +51 -0
  44. package/src/webgpu/compute/TallySampleCountsKernel.js +111 -0
  45. package/src/webgpu/compute/ZeroOutBufferKernel.js +35 -0
  46. package/src/webgpu/compute/ZeroOutKernel.js +31 -0
  47. package/src/webgpu/compute/wavefront/LogicKernel.js +349 -0
  48. package/src/webgpu/compute/wavefront/MaterialKernel.js +371 -0
  49. package/src/webgpu/compute/wavefront/PopulatePixelIndicesKernel.js +72 -0
  50. package/src/webgpu/compute/wavefront/QueueLengthToDispatchKernel.js +38 -0
  51. package/src/webgpu/compute/wavefront/ResetSlotsKernel.js +66 -0
  52. package/src/webgpu/compute/wavefront/TraceRayKernel.js +71 -0
  53. package/src/webgpu/compute/wavefront/TraceShadowRayKernel.js +67 -0
  54. package/src/webgpu/compute/wavefront/structs.js +155 -0
  55. package/src/webgpu/constants.js +62 -0
  56. package/src/webgpu/denoise/OIDNDenoiser.js +431 -0
  57. package/src/webgpu/index.d.ts +176 -0
  58. package/src/webgpu/index.js +11 -0
  59. package/src/webgpu/materials/GltfCompliantMaterial.js +608 -0
  60. package/src/webgpu/materials/GraphMaterial.js +331 -0
  61. package/src/webgpu/materials/PathtracingMaterial.js +89 -0
  62. package/src/webgpu/materials/RenderToScreenMaterial.js +150 -0
  63. package/src/webgpu/materials/debug/AtlasDebugMaterial.js +41 -0
  64. package/src/webgpu/materials/debug/SampleDensityMaterial.js +61 -0
  65. package/src/webgpu/nodes/PathtracerBVHComputeData.js +1037 -0
  66. package/src/webgpu/nodes/debugBounds.wgsl.js +228 -0
  67. package/src/webgpu/nodes/eon.wgsl.js +262 -0
  68. package/src/webgpu/nodes/ggx.wgsl.js +190 -0
  69. package/src/webgpu/nodes/lights.wgsl.js +203 -0
  70. package/src/webgpu/nodes/material.wgsl.js +990 -0
  71. package/src/webgpu/nodes/rand/bluedither.wgsl.js +92 -0
  72. package/src/webgpu/nodes/rand/pcg.wgsl.js +81 -0
  73. package/src/webgpu/nodes/rand/sobol.wgsl.js +287 -0
  74. package/src/webgpu/nodes/random.wgsl.js +20 -0
  75. package/src/webgpu/nodes/reset.wgsl.js +21 -0
  76. package/src/webgpu/nodes/sampling.wgsl.js +193 -0
  77. package/src/webgpu/nodes/sheen.wgsl.js +148 -0
  78. package/src/webgpu/nodes/structs.wgsl.js +272 -0
  79. package/src/webgpu/nodes/utils.wgsl.js +366 -0
  80. package/src/webgpu/shims/ArrayCameraShim.js +74 -0
  81. package/src/webgpu/shims/EquirectCameraShim.js +37 -0
  82. package/src/webgpu/shims/PhysicalCameraShim.js +151 -0
  83. package/src/webgpu/upscale/FSRUpscaler.js +108 -0
@@ -0,0 +1,155 @@
1
+ import { StructTypeNode } from 'three/webgpu';
2
+
3
+ // StructTypeNode that force-emits dependency structs. TSL can't discover a struct referenced only via
4
+ // a type string, so we build each dependency first, which registers its definition ahead of this struct.
5
+ // TODO: remove this once TSL resolves struct dependencies from string member types itself.
6
+ class DependentStructTypeNode extends StructTypeNode {
7
+
8
+ constructor( members, name, dependencies = [] ) {
9
+
10
+ super( members, name );
11
+ this.dependencies = dependencies;
12
+
13
+ }
14
+
15
+ setup( builder ) {
16
+
17
+ for ( const dep of this.dependencies ) {
18
+
19
+ dep.build( builder );
20
+
21
+ }
22
+
23
+ super.setup( builder );
24
+
25
+ }
26
+
27
+ }
28
+
29
+ // Persistent per-path-slot state. One slot per in-flight path, holding everything needed to resolve
30
+ // the previous frame's trace results and stage the next bounce.
31
+ export const rayDataStruct = new StructTypeNode( {
32
+
33
+ throughputColor: 'vec3f',
34
+ currentBounce: 'uint',
35
+
36
+ resultColor: 'vec4f',
37
+
38
+ emission: 'vec3f',
39
+ scatterPdf: 'float',
40
+
41
+ scatterColor: 'vec3f',
42
+
43
+ // the smallest scatter pdf seen along the path, for the glossy filter
44
+ minPdf: 'float',
45
+
46
+ origin: 'vec3f',
47
+
48
+ // whether every scatter so far has been transmissive, for the background modes
49
+ isFullyTransmissive: 'uint',
50
+
51
+ direction: 'vec3f',
52
+ side: 'float',
53
+
54
+ normal: 'vec3f',
55
+ objectIndex: 'int',
56
+
57
+ barycoord: 'vec3f',
58
+ pixelIndex: 'uint',
59
+
60
+ indices: 'vec3u',
61
+ seed: 'uint',
62
+
63
+ lightDirection: 'vec3f',
64
+ lightPdf: 'float',
65
+
66
+ lightEmission: 'vec3f',
67
+ lightDist: 'float',
68
+
69
+ lightBsdf: 'vec3f',
70
+ lightBsdfPdf: 'float',
71
+
72
+ rayIntersectionIndex: 'int',
73
+ shadowRayIntersectionIndex: 'int',
74
+ lightType: 'int',
75
+
76
+ // the traced segment length, for the transmission attenuation applied by MaterialKernel
77
+ dist: 'float',
78
+
79
+ // alpha test pass throughs, counted separately from the bounce count
80
+ alphaDepth: 'uint',
81
+
82
+ // the camera segment's maximum trace distance, carried across alpha pass throughs
83
+ maxDist: 'float',
84
+
85
+ // the path's hero wavelength, negative until its reconstruction weight is applied at the
86
+ // first dispersive interaction
87
+ dispersionWavelength: 'float',
88
+
89
+ }, 'RayData' );
90
+
91
+ // A ray queued for BVH traversal by the trace kernels. A "maxDist" of zero traces unbounded.
92
+ export const traceQueuedRayStruct = new StructTypeNode( {
93
+
94
+ origin: 'vec3f',
95
+ pixelIndex: 'uint',
96
+
97
+ direction: 'vec3f',
98
+ currentBounce: 'uint',
99
+
100
+ seed: 'uint',
101
+ alphaDepth: 'uint',
102
+ maxDist: 'float',
103
+ _alignment0: 'uint',
104
+
105
+ }, 'TraceQueuedRay' );
106
+
107
+ // Compact trace result, written by the trace kernels at the ray's queue index and consumed by
108
+ // LogicKernel the following frame. objectIndex < 0 encodes a miss.
109
+ export const intersectionResultStruct = new StructTypeNode( {
110
+
111
+ barycoord: 'vec3f',
112
+ objectIndex: 'int',
113
+
114
+ position: 'vec3f',
115
+ dist: 'float',
116
+
117
+ normal: 'vec3f',
118
+ side: 'float',
119
+
120
+ indices: 'vec3u',
121
+ _alignment0: 'uint',
122
+
123
+ }, 'TraceResult' );
124
+
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;
139
+
140
+ // 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;
@@ -0,0 +1,62 @@
1
+ import * as pcg from './nodes/rand/pcg.wgsl.js';
2
+ import * as sobol from './nodes/rand/sobol.wgsl.js';
3
+ import * as bluedither from './nodes/rand/bluedither.wgsl.js';
4
+
5
+ // Sampling strategies the kernels draw random numbers from. Pass one to
6
+ // "WebGPUPathTracer.setRandom".
7
+
8
+ /**
9
+ * Uncorrelated pseudo random samples. Converges slowest but never shows structured artifacts.
10
+ * @section Random Strategies
11
+ * @type {Object}
12
+ */
13
+ export const RANDOM_PCG = pcg;
14
+
15
+ /**
16
+ * Sobol low discrepancy samples, scrambled per pixel. Converges faster than `RANDOM_PCG`.
17
+ * @section Random Strategies
18
+ * @type {Object}
19
+ */
20
+ export const RANDOM_SOBOL = sobol;
21
+
22
+ /**
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.
25
+ * @section Random Strategies
26
+ * @type {Object}
27
+ */
28
+ export const RANDOM_BLUE_DITHER = bluedither;
29
+
30
+ // How rays that have only passed through transmissive surfaces treat the background on a miss.
31
+ // Assign one to "WebGPUPathTracer.transmissiveBackground".
32
+
33
+ /**
34
+ * Sample the environment map, so glass shows what is behind it.
35
+ * @section Transmissive Background Modes
36
+ * @type {number}
37
+ */
38
+ export const TRANSMISSIVE_BACKGROUND_ENVIRONMENT = 0;
39
+
40
+ /**
41
+ * Set the opacity from the transmitted light intensity, tinted by the environment lighting.
42
+ * @section Transmissive Background Modes
43
+ * @type {number}
44
+ */
45
+ export const TRANSMISSIVE_BACKGROUND_OVERLAY = 1;
46
+
47
+ /**
48
+ * Attenuate the background by the transmitted light, so glass renders against transparency.
49
+ * @section Transmissive Background Modes
50
+ * @type {number}
51
+ */
52
+ export const TRANSMISSIVE_BACKGROUND_TRANSPARENT = 2;
53
+
54
+ // Layout of the r32uint "sample count" target: two flag bits then a 30 bit count.
55
+
56
+ // this pixel already has a ray on the queue
57
+ export const SAMPLE_ACTIVE_FLAG = 0x80000000;
58
+
59
+ // this pixel has had a camera ray at least once
60
+ export const SAMPLE_DISPATCHED_FLAG = 0x40000000;
61
+
62
+ export const SAMPLE_COUNT_MASK = 0x3FFFFFFF;
@@ -0,0 +1,431 @@
1
+ import { ExternalTexture, NearestFilter, NoToneMapping, RenderTarget, UnsignedByteType } from 'three/webgpu';
2
+ import { diffuseColor, mrt, normalView, vec4 } from 'three/tsl';
3
+
4
+ /** @import { Camera, Scene, Texture, WebGPURenderer } from 'three/webgpu' */
5
+ /** @import { DynamicTileSetting } from 'oidn-web' */
6
+
7
+ /**
8
+ * Runs Open Image Denoise over a path traced image. Pass one to
9
+ * "WebGPUPathTracer.setDenoiser", or drive it directly with "denoise".
10
+ *
11
+ * "initUNetFromURL" and the weights are passed in rather than imported so neither the library
12
+ * nor the network files become a dependency.
13
+ *
14
+ * ```js
15
+ * import { initUNetFromURL } from 'oidn-web';
16
+ * pathTracer.setDenoiser( new OIDNDenoiser( { initUNetFromURL, auxWeightsUrl } ) );
17
+ * ```
18
+ *
19
+ * Weights come from the oidn-weights repository, where the "_small" and "_large" variants trade
20
+ * quality against download size and per tile cost.
21
+ */
22
+ export class OIDNDenoiser {
23
+
24
+ /**
25
+ * The denoised result, or null until the first tile has been produced.
26
+ *
27
+ * @type {ExternalTexture|null}
28
+ */
29
+ get texture() {
30
+
31
+ return this._texture;
32
+
33
+ }
34
+
35
+ /**
36
+ * Whether a pass has finished. Stays true until reset.
37
+ *
38
+ * @type {boolean}
39
+ */
40
+ get complete() {
41
+
42
+ return this._complete;
43
+
44
+ }
45
+
46
+ /**
47
+ * Whether a pass is running. The work is spread over several frames.
48
+ *
49
+ * @type {boolean}
50
+ */
51
+ get running() {
52
+
53
+ return this._running;
54
+
55
+ }
56
+
57
+ /**
58
+ * Every field below can also be assigned after construction.
59
+ *
60
+ * @param {Object} options
61
+ * @param {Function} options.initUNetFromURL
62
+ * @param {string} options.auxWeightsUrl - Weights for the guided model.
63
+ * @param {string} [options.colorWeightsUrl] - Weights for the color only model, needed only
64
+ * when `useAuxiliaryBuffers` is `false`.
65
+ * @param {boolean} [options.useAuxiliaryBuffers]
66
+ * @param {number|null} [options.maxTileSize]
67
+ * @param {DynamicTileSetting|null} [options.dynamicTile] - `false` pins every tile to
68
+ * `maxTileSize`. An object tunes the adaptive sizing.
69
+ */
70
+ constructor( options = {} ) {
71
+
72
+ const {
73
+ initUNetFromURL,
74
+ auxWeightsUrl,
75
+ colorWeightsUrl,
76
+ useAuxiliaryBuffers = true,
77
+ maxTileSize = null,
78
+ dynamicTile = null,
79
+ } = options;
80
+
81
+ if ( ! initUNetFromURL ) {
82
+
83
+ throw new Error( 'OIDNDenoiser: "initUNetFromURL" from "oidn-web" must be provided.' );
84
+
85
+ }
86
+
87
+ this.initUNetFromURL = initUNetFromURL;
88
+ this.auxWeightsUrl = auxWeightsUrl;
89
+ this.colorWeightsUrl = colorWeightsUrl;
90
+
91
+ // `false` falls back to the color only model, which is blurrier but skips a scene render
92
+ this.useAuxiliaryBuffers = useAuxiliaryBuffers;
93
+
94
+ // tiling, passed through to oidn-web. Null keeps its defaults
95
+ this.maxTileSize = maxTileSize;
96
+ this.dynamicTile = dynamicTile;
97
+
98
+ this.renderer = null;
99
+ this.scene = null;
100
+ this.camera = null;
101
+
102
+ this._texture = null;
103
+ this._running = false;
104
+ this._complete = false;
105
+ this._abort = null;
106
+ this._requestId = 0;
107
+ this._unets = { aux: null, color: null };
108
+
109
+ this._resultPipeline = null;
110
+ this._rawTexture = null;
111
+ this._auxTarget = null;
112
+
113
+ }
114
+
115
+ /**
116
+ * @param {WebGPURenderer} renderer
117
+ */
118
+ init( renderer ) {
119
+
120
+ this.renderer = renderer;
121
+
122
+ }
123
+
124
+ /**
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.
137
+ *
138
+ * @param {Texture} target - The path traced result, in linear HDR.
139
+ * @returns {Texture|null}
140
+ */
141
+ update( target ) {
142
+
143
+ if ( this._complete || this._running ) {
144
+
145
+ return this._texture;
146
+
147
+ }
148
+
149
+ let albedo = null;
150
+ let normal = null;
151
+ if ( this.useAuxiliaryBuffers ) {
152
+
153
+ const auxTarget = this._renderAuxiliaryBuffers( target.width, target.height );
154
+ albedo = auxTarget.textures[ 0 ];
155
+ normal = auxTarget.textures[ 1 ];
156
+
157
+ }
158
+
159
+ this.denoise( target, albedo, normal );
160
+
161
+ return this._texture;
162
+
163
+ }
164
+
165
+ /**
166
+ * Runs a pass unless one is already running or finished. The optional albedo and normal
167
+ * buffers guide the filter and select the guided model. Both hold [0,1] values, with normals
168
+ * mapped so a flat normal is (0.5, 0.5, 1).
169
+ *
170
+ * @param {Texture} color - The path traced result, in linear HDR.
171
+ * @param {Texture|null} albedo
172
+ * @param {Texture|null} normal
173
+ */
174
+ async denoise( color, albedo = null, normal = null ) {
175
+
176
+ if ( this._complete || this._running ) {
177
+
178
+ return;
179
+
180
+ }
181
+
182
+ this._running = true;
183
+
184
+ const useAux = Boolean( albedo && normal );
185
+ const requestId = ++ this._requestId;
186
+
187
+ let unet;
188
+ try {
189
+
190
+ unet = await this._initUNet( useAux );
191
+
192
+ } catch ( error ) {
193
+
194
+ // leaving "running" set would stall every later pass
195
+ this._running = false;
196
+ throw error;
197
+
198
+ }
199
+
200
+ // bail if a reset or a newer call took over while the weights downloaded
201
+ if ( requestId !== this._requestId || ! this._running ) {
202
+
203
+ return;
204
+
205
+ }
206
+
207
+ // three only holds a WebGPU texture for a three texture once it has been initialized
208
+ const { renderer } = this;
209
+ const backend = renderer.backend;
210
+ renderer.initTexture( color );
211
+
212
+ const { width, height } = color;
213
+ const inputs = {
214
+ color: { data: backend.get( color ).texture, width, height },
215
+
216
+ // 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 => {
219
+
220
+ this._copyOutputToTexture( output, width, height );
221
+ this._running = false;
222
+ this._complete = true;
223
+ this._abort = null;
224
+
225
+ },
226
+ };
227
+
228
+ // the guided model needs both buffers, and the color only model must not be given them
229
+ if ( useAux ) {
230
+
231
+ renderer.initTexture( albedo );
232
+ renderer.initTexture( normal );
233
+ inputs.albedo = { data: backend.get( albedo ).texture, width, height };
234
+ inputs.normal = { data: backend.get( normal ).texture, width, height };
235
+
236
+ }
237
+
238
+ this._abort = unet.tileExecute( inputs );
239
+
240
+ }
241
+
242
+ /**
243
+ * Drops any running pass and the last result. Call it whenever the image changes, such as when
244
+ * the camera moves.
245
+ */
246
+ reset() {
247
+
248
+ this._abort?.();
249
+ this._abort = null;
250
+
251
+ this._texture?.dispose();
252
+ this._texture = null;
253
+
254
+ this._rawTexture?.destroy();
255
+ this._rawTexture = null;
256
+
257
+ this._running = false;
258
+ this._complete = false;
259
+
260
+ }
261
+
262
+ dispose() {
263
+
264
+ this.reset();
265
+
266
+ this._auxTarget?.dispose();
267
+ this._auxTarget = null;
268
+
269
+ // the networks hold the weights on the GPU, and a load may still be in flight
270
+ for ( const key in this._unets ) {
271
+
272
+ this._unets[ key ]?.then( unet => unet.dispose() );
273
+ this._unets[ key ] = null;
274
+
275
+ }
276
+
277
+ }
278
+
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 ) {
282
+
283
+ const { renderer, scene, camera } = this;
284
+
285
+ if ( ! this._auxTarget ) {
286
+
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
+ }
302
+
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 );
319
+
320
+ renderer.setMRT( originalMRT );
321
+ renderer.setRenderTarget( originalTarget );
322
+ renderer.toneMapping = originalToneMapping;
323
+
324
+ return auxTarget;
325
+
326
+ }
327
+
328
+ // loads each model once, with concurrent calls sharing the same promise
329
+ _initUNet( aux ) {
330
+
331
+ const key = aux ? 'aux' : 'color';
332
+ if ( ! this._unets[ key ] ) {
333
+
334
+ this._unets[ key ] = ( async () => {
335
+
336
+ const device = this.renderer.backend.device;
337
+ const url = aux ? this.auxWeightsUrl : this.colorWeightsUrl;
338
+
339
+ if ( ! url ) {
340
+
341
+ throw new Error( `OIDNDenoiser: no ${ aux ? 'auxWeightsUrl' : 'colorWeightsUrl' } was provided.` );
342
+
343
+ }
344
+
345
+ const options = { aux, hdr: true };
346
+ if ( this.maxTileSize !== null ) options.maxTileSize = this.maxTileSize;
347
+ if ( this.dynamicTile !== null ) options.dynamicTile = this.dynamicTile;
348
+
349
+ return this.initUNetFromURL( url, { device, adapterInfo: device.adapterInfo }, options );
350
+
351
+ } )();
352
+
353
+ }
354
+
355
+ return this._unets[ key ];
356
+
357
+ }
358
+
359
+ // Copies the denoiser's packed output buffer into a sampleable texture, using a compute pass
360
+ // since copyBufferToTexture requires 256 byte row alignment.
361
+ // TODO: remove the copy once oidn-web can write into a caller-provided storage texture.
362
+ _copyOutputToTexture( output, width, height ) {
363
+
364
+ const device = this.renderer.backend.device;
365
+
366
+ if ( ! this._resultPipeline ) {
367
+
368
+ const module = device.createShaderModule( {
369
+ code: /* wgsl */`
370
+ @group( 0 ) @binding( 0 ) var<storage, read> src : array<vec4f>;
371
+ @group( 0 ) @binding( 1 ) var dst : texture_storage_2d<rgba16float, write>;
372
+
373
+ @compute @workgroup_size( 8, 8 )
374
+ fn main( @builtin( global_invocation_id ) gid : vec3u ) {
375
+
376
+ let dims = textureDimensions( dst );
377
+ if ( gid.x >= dims.x || gid.y >= dims.y ) {
378
+
379
+ return;
380
+
381
+ }
382
+
383
+ textureStore( dst, gid.xy, src[ gid.y * dims.x + gid.x ] );
384
+
385
+ }
386
+ `,
387
+ } );
388
+
389
+ this._resultPipeline = device.createComputePipeline( {
390
+ layout: 'auto',
391
+ compute: { module, entryPoint: 'main' },
392
+ } );
393
+
394
+ }
395
+
396
+ if ( ! this._rawTexture || this._rawTexture.width !== width || this._rawTexture.height !== height ) {
397
+
398
+ this._rawTexture?.destroy();
399
+ this._rawTexture = device.createTexture( {
400
+ size: [ width, height ],
401
+ format: 'rgba16float',
402
+ usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
403
+ } );
404
+
405
+ // ExternalTexture lets three sample a gpu texture it did not create. It has no image
406
+ // behind it, so its size is set explicitly or "texture.width" reports zero.
407
+ this._texture?.dispose();
408
+ this._texture = new ExternalTexture( this._rawTexture );
409
+ this._texture.image = { width, height };
410
+
411
+ }
412
+
413
+ const bindGroup = device.createBindGroup( {
414
+ layout: this._resultPipeline.getBindGroupLayout( 0 ),
415
+ entries: [
416
+ { binding: 0, resource: { buffer: output.data } },
417
+ { binding: 1, resource: this._rawTexture.createView() },
418
+ ],
419
+ } );
420
+
421
+ const encoder = device.createCommandEncoder();
422
+ const pass = encoder.beginComputePass();
423
+ pass.setPipeline( this._resultPipeline );
424
+ pass.setBindGroup( 0, bindGroup );
425
+ pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
426
+ pass.end();
427
+ device.queue.submit( [ encoder.finish() ] );
428
+
429
+ }
430
+
431
+ }