rayzee 8.6.1 → 8.8.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "8.6.1",
3
+ "version": "8.8.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -198,6 +198,8 @@ export const ENGINE_DEFAULTS = {
198
198
  // value only governs where a denoise is cheap. 8 ms measured 31 refreshes/sec at 512² at an
199
199
  // unchanged sample rate; 50 ms measured 18/sec for nothing in return.
200
200
  continuousDenoiseInterval: 8,
201
+ // While the view moves, feed OIDN the reprojected history of restarted frames, not one sample.
202
+ oidnTemporalHistory: true,
201
203
 
202
204
  enableUpscaler: false,
203
205
  upscalerScale: 2,
@@ -7,16 +7,16 @@ const log = createLogger( 'oidn' );
7
7
  // The tile alignment oidn-web's own fitTileDimension uses (its tileScheduler.js).
8
8
  const OIDN_TILE_ALIGNMENT = 16;
9
9
 
10
- let _initUNetFromURL = null;
11
- async function getInitUNetFromURL() {
10
+ let _initUNetFromBuffer = null;
11
+ async function getInitUNetFromBuffer() {
12
12
 
13
- if ( ! _initUNetFromURL ) {
13
+ if ( ! _initUNetFromBuffer ) {
14
14
 
15
- _initUNetFromURL = ( await import( 'oidn-web' ) ).initUNetFromURL;
15
+ _initUNetFromBuffer = ( await import( 'oidn-web' ) ).initUNetFromBuffer;
16
16
 
17
17
  }
18
18
 
19
- return _initUNetFromURL;
19
+ return _initUNetFromBuffer;
20
20
 
21
21
  }
22
22
 
@@ -199,7 +199,7 @@ export class OIDNDenoiser extends EventDispatcher {
199
199
 
200
200
  // WebGPU GPU-native path (no CPU readback for inputs)
201
201
  // backendParams: () => { device: GPUDevice, adapterInfo: GPUAdapterInfo|null }
202
- // getGPUTextures: () => { color: GPUTexture, albedo: GPUTexture, normal: GPUTexture }
202
+ // getGPUTextures: ( { continuous } ) => { color: GPUTexture, albedo: GPUTexture, normal: GPUTexture }
203
203
  this.backendParamsGetter = options.backendParams || null;
204
204
  this.getGPUTextures = options.getGPUTextures || null;
205
205
  this.gpuDevice = null;
@@ -263,6 +263,8 @@ export class OIDNDenoiser extends EventDispatcher {
263
263
 
264
264
  this.currentTZAUrl = null;
265
265
  this.unet = null;
266
+ // url → Promise<ArrayBuffer>, so a tier swap never downloads the model again.
267
+ this._weights = new Map();
266
268
  // A start() requested while the UNet is still loading is deferred here and fired
267
269
  // once loading finishes, instead of being silently dropped.
268
270
  this._pendingStart = false;
@@ -420,9 +422,12 @@ export class OIDNDenoiser extends EventDispatcher {
420
422
 
421
423
  this.dispatchEvent( { type: 'loading', message: 'Loading UNet denoiser...' } );
422
424
 
423
- // Dispose previous instance
424
425
  if ( this.unet ) {
425
426
 
427
+ // A tile already started still writes into the old network once its microtasks run;
428
+ // disposing under it submitted destroyed buffers.
429
+ if ( this.state.isDenoising ) this.state.abortController?.abort();
430
+ await new Promise( resolve => setTimeout( resolve, 0 ) );
426
431
  this.unet.dispose();
427
432
  this.unet = null;
428
433
 
@@ -439,8 +444,8 @@ export class OIDNDenoiser extends EventDispatcher {
439
444
 
440
445
  }
441
446
 
442
- const initFn = await getInitUNetFromURL();
443
- this.unet = await initFn( tzaUrl, backendParams, {
447
+ const initFn = await getInitUNetFromBuffer();
448
+ this.unet = await initFn( await this._fetchWeights( tzaUrl ), backendParams, {
444
449
  aux: true,
445
450
  hdr: true,
446
451
  maxTileSize: tileSize,
@@ -456,6 +461,26 @@ export class OIDNDenoiser extends EventDispatcher {
456
461
 
457
462
  }
458
463
 
464
+ _fetchWeights( url ) {
465
+
466
+ let bytes = this._weights.get( url );
467
+ if ( ! bytes ) {
468
+
469
+ bytes = fetch( url ).then( res => {
470
+
471
+ if ( ! res.ok ) throw new Error( `HTTP ${ res.status } fetching ${ url }` );
472
+ return res.arrayBuffer();
473
+
474
+ } );
475
+ bytes.catch( () => this._weights.delete( url ) );
476
+ this._weights.set( url, bytes );
477
+
478
+ }
479
+
480
+ return bytes;
481
+
482
+ }
483
+
459
484
  _generateTzaUrl() {
460
485
 
461
486
  const { oidnWeightsBaseUrl } = getAssetConfig();
@@ -614,7 +639,7 @@ export class OIDNDenoiser extends EventDispatcher {
614
639
 
615
640
  }
616
641
 
617
- const textures = this.getGPUTextures();
642
+ const textures = this.getGPUTextures( { continuous } );
618
643
  if ( ! textures?.color ) {
619
644
 
620
645
  log.warn( 'GPU textures not ready yet' );
@@ -1253,6 +1278,7 @@ export class OIDNDenoiser extends EventDispatcher {
1253
1278
 
1254
1279
  // Clear references
1255
1280
  this.unet = null;
1281
+ this._weights.clear();
1256
1282
  this.state.abortController = null;
1257
1283
 
1258
1284
  // Remove all event listeners
@@ -0,0 +1,569 @@
1
+ import { GBUFFER_MISS_THRESHOLD } from '../EngineDefaults.js';
2
+
3
+ /**
4
+ * History of the frames the path tracer restarted while the view moved, reprojected into the
5
+ * current view and handed to OIDN instead of a single fresh sample.
6
+ *
7
+ * OIDN reads spatially correlated noise as detail, so each pixel takes ONE history pixel (never a
8
+ * bilinear blend), and a history pixel several pixels picked has its length split between them.
9
+ * History length rides in albedo.w; colour.w stays the path tracer's alpha.
10
+ */
11
+
12
+ export const HISTORY_DEFAULTS = Object.freeze( {
13
+ maxLength: 8,
14
+ // Reflections do not move with the surface.
15
+ smoothLength: 2,
16
+ roughStart: 0.45,
17
+ roughEnd: 0.7,
18
+ depthTolerance: 0.05,
19
+ normalTolerance: 0.8,
20
+ // A moved object's lighting changes as it moves, so its history lags like a reflection.
21
+ movedLength: 2,
22
+ } );
23
+
24
+ const WG = 8;
25
+ const PARAMS_BYTES = 288;
26
+ const MIN_MOVED_CAPACITY = 16;
27
+ const FLAG_CLEAN_AUX = 1;
28
+ const FLAG_SPLIT_COPIES = 2;
29
+ const PICK_MOVED = 0x40000000;
30
+
31
+ const COMMON_WGSL = /* wgsl */`
32
+ struct Params {
33
+ size: vec2<u32>,
34
+ reset: u32,
35
+ flags: u32,
36
+ maxLength: f32,
37
+ smoothLength: f32,
38
+ depthTolerance: f32,
39
+ normalTolerance: f32,
40
+ roughStart: f32,
41
+ roughEnd: f32,
42
+ samples: f32,
43
+ historyScale: f32,
44
+ camWorld: mat4x4<f32>,
45
+ projInv: mat4x4<f32>,
46
+ prevViewProj: mat4x4<f32>,
47
+ prevCamPos: vec4<f32>,
48
+ moved: vec4<u32>,
49
+ movedLength: f32,
50
+ };
51
+
52
+ @group(0) @binding(0) var<uniform> P: Params;
53
+
54
+ fn finite3( v: vec3<f32> ) -> bool {
55
+ let e = bitcast<vec3<u32>>( v ) & vec3<u32>( 0x7f800000u );
56
+ return all( e != vec3<u32>( 0x7f800000u ) );
57
+ }
58
+
59
+ // [0,1]-encoded in and out, renormalised like FinalWrite's clean aux.
60
+ fn mixNormal( h: vec3<f32>, c: vec3<f32>, t: f32 ) -> vec3<f32> {
61
+ let m = mix( h * 2.0 - 1.0, c * 2.0 - 1.0, t );
62
+ let l = length( m );
63
+ return select( c, m / l * 0.5 + 0.5, l > 1e-4 );
64
+ }
65
+ `;
66
+
67
+ const SELECT_WGSL = COMMON_WGSL + /* wgsl */`
68
+ @group(0) @binding(1) var geoCur: texture_2d<f32>;
69
+ @group(0) @binding(2) var geoPrev: texture_2d<f32>;
70
+ @group(0) @binding(3) var<storage, read_write> picked: array<i32>;
71
+ @group(0) @binding(4) var<storage, read_write> copies: array<atomic<u32>>;
72
+ @group(0) @binding(5) var instanceLeaf: texture_2d<u32>;
73
+ @group(0) @binding(6) var<storage, read> movedLeaves: array<u32>;
74
+ @group(0) @binding(7) var<storage, read> movedToPrev: array<mat4x4<f32>>;
75
+
76
+ fn movedIndex( leaf: u32 ) -> i32 {
77
+
78
+ var lo = 0u;
79
+ var hi = P.moved.x;
80
+ loop {
81
+
82
+ if ( lo >= hi ) { break; }
83
+ let mid = ( lo + hi ) / 2u;
84
+ let v = movedLeaves[ mid ];
85
+ if ( v == leaf ) { return i32( mid ); }
86
+ if ( v < leaf ) { lo = mid + 1u; } else { hi = mid; }
87
+
88
+ }
89
+ return -1;
90
+
91
+ }
92
+
93
+ @compute @workgroup_size(${ WG }, ${ WG })
94
+ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
95
+
96
+ if ( gid.x >= P.size.x || gid.y >= P.size.y ) { return; }
97
+
98
+ let g = textureLoad( geoCur, vec2<i32>( gid.xy ), 0 );
99
+ var best = -1.0;
100
+ var pick = -1;
101
+ var movedFlag = 0;
102
+
103
+ if ( P.reset == 0u && g.w < ${ GBUFFER_MISS_THRESHOLD.toFixed( 1 ) } ) {
104
+
105
+ let size = vec2<f32>( P.size );
106
+ let uv = ( vec2<f32>( gid.xy ) + 0.5 ) / size;
107
+ let rc = P.projInv * vec4<f32>( uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 1.0, 1.0 );
108
+ let dir = normalize( ( P.camWorld * vec4<f32>( rc.xyz / rc.w, 0.0 ) ).xyz );
109
+ var wp = P.camWorld[ 3 ].xyz + dir * g.w;
110
+ var n = g.xyz * 2.0 - 1.0;
111
+
112
+ // A moved object's surface is looked for where the object was.
113
+ let leaf = textureLoad( instanceLeaf, vec2<i32>( gid.xy ), 0 ).x;
114
+ if ( leaf > 0u && P.moved.x > 0u ) {
115
+
116
+ let m = movedIndex( leaf - 1u );
117
+ if ( m >= 0 ) {
118
+
119
+ let toPrev = movedToPrev[ u32( m ) ];
120
+ wp = ( toPrev * vec4<f32>( wp, 1.0 ) ).xyz;
121
+ n = normalize( ( toPrev * vec4<f32>( n, 0.0 ) ).xyz );
122
+ movedFlag = ${ PICK_MOVED };
123
+
124
+ }
125
+
126
+ }
127
+
128
+ let clip = P.prevViewProj * vec4<f32>( wp, 1.0 );
129
+
130
+ if ( clip.w > 0.0 ) {
131
+
132
+ let pos = vec2<f32>( clip.x / clip.w * 0.5 + 0.5, 0.5 - clip.y / clip.w * 0.5 ) * size - 0.5;
133
+ let base = vec2<i32>( floor( pos ) );
134
+ let f = fract( pos );
135
+ let expected = length( wp - P.prevCamPos.xyz );
136
+
137
+ for ( var j = 0; j < 2; j++ ) {
138
+ for ( var i = 0; i < 2; i++ ) {
139
+
140
+ let q = base + vec2<i32>( i, j );
141
+ if ( any( q < vec2<i32>( 0 ) ) || any( q >= vec2<i32>( P.size ) ) ) { continue; }
142
+ let h = textureLoad( geoPrev, q, 0 );
143
+ if ( abs( h.w - expected ) >= P.depthTolerance * expected ) { continue; }
144
+ if ( dot( n, h.xyz * 2.0 - 1.0 ) <= P.normalTolerance ) { continue; }
145
+ let w = select( 1.0 - f.x, f.x, i == 1 ) * select( 1.0 - f.y, f.y, j == 1 );
146
+ if ( w > best ) { best = w; pick = q.y * i32( P.size.x ) + q.x; }
147
+
148
+ }
149
+ }
150
+
151
+ }
152
+
153
+ }
154
+
155
+ picked[ gid.y * P.size.x + gid.x ] = select( pick, pick | movedFlag, pick >= 0 );
156
+ if ( pick >= 0 ) { atomicAdd( &copies[ u32( pick ) ], 1u ); }
157
+
158
+ }
159
+ `;
160
+
161
+ const BLEND_WGSL = COMMON_WGSL + /* wgsl */`
162
+ @group(0) @binding(1) var curColor: texture_2d<f32>;
163
+ @group(0) @binding(2) var curAlbedo: texture_2d<f32>;
164
+ @group(0) @binding(3) var curNormal: texture_2d<f32>;
165
+ @group(0) @binding(4) var shading: texture_2d<f32>;
166
+ @group(0) @binding(5) var histColor: texture_2d<f32>;
167
+ @group(0) @binding(6) var histAlbedo: texture_2d<f32>;
168
+ @group(0) @binding(7) var histNormal: texture_2d<f32>;
169
+ @group(0) @binding(8) var<storage, read> picked: array<i32>;
170
+ @group(0) @binding(9) var<storage, read> copies: array<u32>;
171
+ @group(0) @binding(10) var outColor: texture_storage_2d<rgba32float, write>;
172
+ @group(0) @binding(11) var outAlbedo: texture_storage_2d<rgba32float, write>;
173
+ @group(0) @binding(12) var outNormal: texture_storage_2d<rgba32float, write>;
174
+
175
+ @compute @workgroup_size(${ WG }, ${ WG })
176
+ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
177
+
178
+ if ( gid.x >= P.size.x || gid.y >= P.size.y ) { return; }
179
+
180
+ let p = vec2<i32>( gid.xy );
181
+ var cur = textureLoad( curColor, p, 0 );
182
+ if ( ! finite3( cur.xyz ) ) { cur = vec4<f32>( 0.0, 0.0, 0.0, cur.w ); }
183
+ let ca = textureLoad( curAlbedo, p, 0 );
184
+ let cn = textureLoad( curNormal, p, 0 );
185
+
186
+ var len = 0.0;
187
+ var hc = cur.xyz;
188
+ var ha = ca.xyz;
189
+ var hn = cn.xyz;
190
+ let raw = picked[ gid.y * P.size.x + gid.x ];
191
+ let movedPx = raw >= 0 && ( raw & ${ PICK_MOVED } ) != 0;
192
+ let pick = select( raw, raw & ~${ PICK_MOVED }, raw >= 0 );
193
+
194
+ if ( pick >= 0 ) {
195
+
196
+ let q = vec2<i32>( pick % i32( P.size.x ), pick / i32( P.size.x ) );
197
+ let c = textureLoad( histColor, q, 0 ).xyz;
198
+
199
+ if ( finite3( c ) ) {
200
+
201
+ let a = textureLoad( histAlbedo, q, 0 );
202
+ hc = c;
203
+ ha = a.xyz;
204
+ hn = textureLoad( histNormal, q, 0 ).xyz;
205
+ len = a.w;
206
+ if ( ( P.flags & ${ FLAG_SPLIT_COPIES }u ) != 0u ) { len = len / f32( max( copies[ u32( pick ) ], 1u ) ); }
207
+
208
+ }
209
+
210
+ }
211
+
212
+ let rough = textureLoad( shading, p, 0 ).w;
213
+ var cap = mix( P.smoothLength, P.maxLength, smoothstep( P.roughStart, P.roughEnd, rough ) );
214
+ if ( movedPx ) { cap = min( cap, P.movedLength ); }
215
+ let newLen = min( len + 1.0, cap );
216
+ let t = 1.0 / newLen;
217
+
218
+ textureStore( outColor, p, vec4<f32>( mix( hc, cur.xyz, t ), cur.w ) );
219
+ textureStore( outAlbedo, p, vec4<f32>( mix( ha, ca.xyz, t ), newLen ) );
220
+ let n = select( cn.xyz, mixNormal( hn, cn.xyz, t ), ( P.flags & ${ FLAG_CLEAN_AUX }u ) != 0u );
221
+ textureStore( outNormal, p, vec4<f32>( n, cn.w ) );
222
+
223
+ }
224
+ `;
225
+
226
+ // Sample-weighted mean of history and a still accumulation.
227
+ const MERGE_WGSL = COMMON_WGSL + /* wgsl */`
228
+ @group(0) @binding(1) var accColor: texture_2d<f32>;
229
+ @group(0) @binding(2) var accAlbedo: texture_2d<f32>;
230
+ @group(0) @binding(3) var accNormal: texture_2d<f32>;
231
+ @group(0) @binding(4) var histColor: texture_2d<f32>;
232
+ @group(0) @binding(5) var histAlbedo: texture_2d<f32>;
233
+ @group(0) @binding(6) var histNormal: texture_2d<f32>;
234
+ @group(0) @binding(7) var outColor: texture_storage_2d<rgba32float, write>;
235
+ @group(0) @binding(8) var outAlbedo: texture_storage_2d<rgba32float, write>;
236
+ @group(0) @binding(9) var outNormal: texture_storage_2d<rgba32float, write>;
237
+
238
+ @compute @workgroup_size(${ WG }, ${ WG })
239
+ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
240
+
241
+ if ( gid.x >= P.size.x || gid.y >= P.size.y ) { return; }
242
+
243
+ let p = vec2<i32>( gid.xy );
244
+ let ac = textureLoad( accColor, p, 0 );
245
+ let aa = textureLoad( accAlbedo, p, 0 );
246
+ let an = textureLoad( accNormal, p, 0 );
247
+
248
+ var k = 0.0;
249
+ var hc = ac.xyz;
250
+ var ha = aa.xyz;
251
+ var hn = an.xyz;
252
+
253
+ if ( P.reset == 0u ) {
254
+
255
+ let c = textureLoad( histColor, p, 0 ).xyz;
256
+
257
+ if ( finite3( c ) ) {
258
+
259
+ let a = textureLoad( histAlbedo, p, 0 );
260
+ k = a.w * P.historyScale;
261
+ hc = c;
262
+ ha = a.xyz;
263
+ hn = textureLoad( histNormal, p, 0 ).xyz;
264
+
265
+ }
266
+
267
+ }
268
+
269
+ let t = P.samples / max( k + P.samples, 1e-6 );
270
+ textureStore( outColor, p, vec4<f32>( mix( hc, ac.xyz, t ), ac.w ) );
271
+ textureStore( outAlbedo, p, vec4<f32>( mix( ha, aa.xyz, t ), min( k + P.samples, P.maxLength ) ) );
272
+ let n = select( an.xyz, mixNormal( hn, an.xyz, t ), ( P.flags & ${ FLAG_CLEAN_AUX }u ) != 0u );
273
+ textureStore( outNormal, p, vec4<f32>( n, an.w ) );
274
+
275
+ }
276
+ `;
277
+
278
+ const texEntry = ( binding ) => ( { binding, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'unfilterable-float' } } );
279
+ const outEntry = ( binding ) => ( { binding, visibility: GPUShaderStage.COMPUTE, storageTexture: { access: 'write-only', format: 'rgba32float' } } );
280
+ const bufEntry = ( binding, type ) => ( { binding, visibility: GPUShaderStage.COMPUTE, buffer: { type } } );
281
+
282
+ export class OIDNTemporalHistory {
283
+
284
+ /** @param {GPUDevice} device */
285
+ constructor( device ) {
286
+
287
+ this.device = device;
288
+ this.settings = { ...HISTORY_DEFAULTS, cleanAux: false, splitCopies: true };
289
+ this.valid = false;
290
+ this.width = 0;
291
+ this.height = 0;
292
+
293
+ this._sets = null;
294
+ this._current = 0;
295
+ this._picked = null;
296
+ this._copies = null;
297
+ this._paramsBuffer = null;
298
+ this._pipelines = null;
299
+ this._movedLeaves = null;
300
+ this._movedToPrev = null;
301
+ this._movedCapacity = 0;
302
+ this._movedCount = 0;
303
+
304
+ this._paramData = new ArrayBuffer( PARAMS_BYTES );
305
+ this._paramU32 = new Uint32Array( this._paramData );
306
+ this._paramF32 = new Float32Array( this._paramData );
307
+ this._prevViewProj = new Float32Array( 16 );
308
+ this._prevCamPos = new Float32Array( 4 );
309
+
310
+ }
311
+
312
+ /** The history OIDN reads while the view moves, or null before the first frame. */
313
+ get textures() {
314
+
315
+ return this.valid ? this._sets[ this._current ] : null;
316
+
317
+ }
318
+
319
+ invalidate() {
320
+
321
+ this.valid = false;
322
+
323
+ }
324
+
325
+ /**
326
+ * Blends the frame just traced (a restart: one sample) into the reprojected history.
327
+ * @param {Object} src - GPUTextures { color, albedo, normal, geo, geoPrev, shading, leaf } and { width, height }
328
+ * @param {{ world: Float32Array, projInv: Float32Array, viewProj: Float32Array }} camera - of that frame
329
+ * @param {?{ count: number, leaves: Uint32Array, toPrev: Float32Array }} [moved] - TLAS leaves
330
+ * (ascending) that moved since the last frame, with each one's current→previous world matrix
331
+ */
332
+ accumulate( src, camera, moved = null ) {
333
+
334
+ if ( ! this._ensure( src.width, src.height ) ) return;
335
+
336
+ this._uploadMoved( moved );
337
+ this._writeParams( camera, ! this.valid, 0, 1 );
338
+ const from = this._sets[ this._current ];
339
+ const to = this._sets[ 1 - this._current ];
340
+ const { select, blend } = this._pipelines;
341
+
342
+ const encoder = this.device.createCommandEncoder( { label: 'oidn-history' } );
343
+ encoder.clearBuffer( this._copies );
344
+ this._dispatch( encoder, select, [
345
+ { buffer: this._paramsBuffer }, src.geo, src.geoPrev, { buffer: this._picked }, { buffer: this._copies },
346
+ src.leaf, { buffer: this._movedLeaves }, { buffer: this._movedToPrev },
347
+ ] );
348
+ this._dispatch( encoder, blend, [
349
+ { buffer: this._paramsBuffer }, src.color, src.albedo, src.normal, src.shading,
350
+ from.color, from.albedo, from.normal, { buffer: this._picked }, { buffer: this._copies },
351
+ to.color, to.albedo, to.normal,
352
+ ] );
353
+ this.device.queue.submit( [ encoder.finish() ] );
354
+
355
+ this._current = 1 - this._current;
356
+ this.valid = true;
357
+ this._rememberCamera( camera );
358
+
359
+ }
360
+
361
+ /**
362
+ * Sample-weighted mean of the history and a still accumulation of `samples` frames.
363
+ * @param {Object} src - as for accumulate; only colour, albedo and normal are read
364
+ * @param {number} samples
365
+ * @param {Object} camera - of the accumulation
366
+ * @param {Object} [options]
367
+ * @param {boolean} [options.commit] - make the result the history (a move is starting)
368
+ * @param {boolean} [options.keepHistory] - false merges nothing in: the result is the accumulation
369
+ * @param {number} [options.historyScale] - weight on the history's sample count
370
+ * @returns {?{ color: GPUTexture, albedo: GPUTexture, normal: GPUTexture }}
371
+ */
372
+ merge( src, samples, camera, { commit = false, keepHistory = true, historyScale = 1 } = {} ) {
373
+
374
+ if ( ! this._ensure( src.width, src.height ) ) return null;
375
+
376
+ this._writeParams( camera, ! this.valid || ! keepHistory, samples, historyScale );
377
+ const from = this._sets[ this._current ];
378
+ const to = this._sets[ 1 - this._current ];
379
+
380
+ const encoder = this.device.createCommandEncoder( { label: 'oidn-history-merge' } );
381
+ this._dispatch( encoder, this._pipelines.merge, [
382
+ { buffer: this._paramsBuffer }, src.color, src.albedo, src.normal,
383
+ from.color, from.albedo, from.normal, to.color, to.albedo, to.normal,
384
+ ] );
385
+ this.device.queue.submit( [ encoder.finish() ] );
386
+
387
+ if ( commit ) {
388
+
389
+ this._current = 1 - this._current;
390
+ this.valid = true;
391
+ this._rememberCamera( camera );
392
+
393
+ }
394
+
395
+ return to;
396
+
397
+ }
398
+
399
+ dispose() {
400
+
401
+ this._releaseTextures();
402
+ this._paramsBuffer?.destroy();
403
+ this._movedLeaves?.destroy();
404
+ this._movedToPrev?.destroy();
405
+ this._paramsBuffer = null;
406
+ this._movedLeaves = null;
407
+ this._movedToPrev = null;
408
+ this._movedCapacity = 0;
409
+ this._pipelines = null;
410
+
411
+ }
412
+
413
+ _ensure( width, height ) {
414
+
415
+ if ( ! width || ! height ) return false;
416
+ this._ensurePipelines();
417
+ if ( this._sets && this.width === width && this.height === height ) return true;
418
+
419
+ this._releaseTextures();
420
+
421
+ const device = this.device;
422
+ const usage = GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC;
423
+ const make = ( label ) => device.createTexture( { label, size: { width, height }, format: 'rgba32float', usage } );
424
+ this._sets = [ 0, 1 ].map( i => ( {
425
+ color: make( `oidn-history-color-${ i }` ),
426
+ albedo: make( `oidn-history-albedo-${ i }` ),
427
+ normal: make( `oidn-history-normal-${ i }` ),
428
+ } ) );
429
+ this._picked = device.createBuffer( { label: 'oidn-history-picked', size: width * height * 4, usage: GPUBufferUsage.STORAGE } );
430
+ this._copies = device.createBuffer( { label: 'oidn-history-copies', size: width * height * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST } );
431
+
432
+ this.width = width;
433
+ this.height = height;
434
+ this._current = 0;
435
+ this.valid = false;
436
+ return true;
437
+
438
+ }
439
+
440
+ _ensurePipelines() {
441
+
442
+ if ( this._pipelines ) return;
443
+
444
+ const device = this.device;
445
+ const build = ( label, code, entries ) => {
446
+
447
+ const layout = device.createBindGroupLayout( { label, entries } );
448
+ const pipeline = device.createComputePipeline( {
449
+ label,
450
+ layout: device.createPipelineLayout( { bindGroupLayouts: [ layout ] } ),
451
+ compute: { module: device.createShaderModule( { label, code } ), entryPoint: 'main' },
452
+ } );
453
+ return { pipeline, layout };
454
+
455
+ };
456
+
457
+ const uniform = bufEntry( 0, 'uniform' );
458
+ this._pipelines = {
459
+ select: build( 'oidn-history-select', SELECT_WGSL, [
460
+ uniform, texEntry( 1 ), texEntry( 2 ), bufEntry( 3, 'storage' ), bufEntry( 4, 'storage' ),
461
+ { binding: 5, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'uint' } },
462
+ bufEntry( 6, 'read-only-storage' ), bufEntry( 7, 'read-only-storage' ),
463
+ ] ),
464
+ blend: build( 'oidn-history-blend', BLEND_WGSL, [
465
+ uniform, ...[ 1, 2, 3, 4, 5, 6, 7 ].map( texEntry ),
466
+ bufEntry( 8, 'read-only-storage' ), bufEntry( 9, 'read-only-storage' ),
467
+ outEntry( 10 ), outEntry( 11 ), outEntry( 12 ),
468
+ ] ),
469
+ merge: build( 'oidn-history-merge', MERGE_WGSL, [
470
+ uniform, ...[ 1, 2, 3, 4, 5, 6 ].map( texEntry ), outEntry( 7 ), outEntry( 8 ), outEntry( 9 ),
471
+ ] ),
472
+ };
473
+
474
+ this._paramsBuffer = device.createBuffer( {
475
+ label: 'oidn-history-params',
476
+ size: PARAMS_BYTES,
477
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
478
+ } );
479
+ this._growMoved( MIN_MOVED_CAPACITY );
480
+
481
+ }
482
+
483
+ _growMoved( capacity ) {
484
+
485
+ this._movedLeaves?.destroy();
486
+ this._movedToPrev?.destroy();
487
+ const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
488
+ this._movedLeaves = this.device.createBuffer( { label: 'oidn-history-moved-leaves', size: capacity * 4, usage } );
489
+ this._movedToPrev = this.device.createBuffer( { label: 'oidn-history-moved-matrices', size: capacity * 64, usage } );
490
+ this._movedCapacity = capacity;
491
+
492
+ }
493
+
494
+ _uploadMoved( moved ) {
495
+
496
+ this._movedCount = moved?.count ?? 0;
497
+ if ( ! this._movedCount ) return;
498
+ if ( this._movedCount > this._movedCapacity ) this._growMoved( Math.max( this._movedCount, this._movedCapacity * 2 ) );
499
+ this.device.queue.writeBuffer( this._movedLeaves, 0, moved.leaves, 0, this._movedCount );
500
+ this.device.queue.writeBuffer( this._movedToPrev, 0, moved.toPrev, 0, this._movedCount * 16 );
501
+
502
+ }
503
+
504
+ // Resources in binding order; GPUTextures get a view.
505
+ _dispatch( encoder, { pipeline, layout }, resources ) {
506
+
507
+ const entries = resources.map( ( r, binding ) => ( {
508
+ binding,
509
+ resource: r.buffer ? r : r.createView(),
510
+ } ) );
511
+ const pass = encoder.beginComputePass();
512
+ pass.setPipeline( pipeline );
513
+ pass.setBindGroup( 0, this.device.createBindGroup( { layout, entries } ) );
514
+ pass.dispatchWorkgroups( Math.ceil( this.width / WG ), Math.ceil( this.height / WG ) );
515
+ pass.end();
516
+
517
+ }
518
+
519
+ _writeParams( camera, reset, samples, historyScale ) {
520
+
521
+ const s = this.settings;
522
+ const u = this._paramU32;
523
+ const f = this._paramF32;
524
+ u[ 0 ] = this.width;
525
+ u[ 1 ] = this.height;
526
+ u[ 2 ] = reset ? 1 : 0;
527
+ u[ 3 ] = ( s.cleanAux ? FLAG_CLEAN_AUX : 0 ) | ( s.splitCopies ? FLAG_SPLIT_COPIES : 0 );
528
+ f[ 4 ] = s.maxLength;
529
+ f[ 5 ] = s.smoothLength;
530
+ f[ 6 ] = s.depthTolerance;
531
+ f[ 7 ] = s.normalTolerance;
532
+ f[ 8 ] = s.roughStart;
533
+ f[ 9 ] = s.roughEnd;
534
+ f[ 10 ] = samples;
535
+ f[ 11 ] = historyScale;
536
+ f.set( camera.world, 12 );
537
+ f.set( camera.projInv, 28 );
538
+ f.set( this._prevViewProj, 44 );
539
+ f.set( this._prevCamPos, 60 );
540
+ u[ 64 ] = this._movedCount;
541
+ f[ 68 ] = s.movedLength;
542
+ this.device.queue.writeBuffer( this._paramsBuffer, 0, this._paramData );
543
+
544
+ }
545
+
546
+ _rememberCamera( camera ) {
547
+
548
+ this._prevViewProj.set( camera.viewProj );
549
+ this._prevCamPos[ 0 ] = camera.world[ 12 ];
550
+ this._prevCamPos[ 1 ] = camera.world[ 13 ];
551
+ this._prevCamPos[ 2 ] = camera.world[ 14 ];
552
+
553
+ }
554
+
555
+ _releaseTextures() {
556
+
557
+ this._sets?.forEach( set => Object.values( set ).forEach( t => t.destroy() ) );
558
+ this._picked?.destroy();
559
+ this._copies?.destroy();
560
+ this._sets = null;
561
+ this._picked = null;
562
+ this._copies = null;
563
+ this.width = 0;
564
+ this.height = 0;
565
+ this.valid = false;
566
+
567
+ }
568
+
569
+ }