rayzee 8.7.0 → 8.8.1
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/README.md +35 -6
- package/dist/rayzee.es.js +3370 -2796
- package/dist/rayzee.umd.js +248 -10
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -0
- package/src/Passes/OIDNDenoiser.js +36 -10
- package/src/Passes/OIDNTemporalHistory.js +569 -0
- package/src/PathTracerApp.js +41 -6
- package/src/Stages/NormalDepth.js +94 -3
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +4 -0
- package/src/managers/DenoisingManager.js +254 -5
- package/src/managers/LightManager.js +5 -0
- package/src/managers/TransformManager.js +38 -20
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -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
|
|
11
|
-
async function
|
|
10
|
+
let _initUNetFromBuffer = null;
|
|
11
|
+
async function getInitUNetFromBuffer() {
|
|
12
12
|
|
|
13
|
-
if ( !
|
|
13
|
+
if ( ! _initUNetFromBuffer ) {
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
_initUNetFromBuffer = ( await import( 'oidn-web' ) ).initUNetFromBuffer;
|
|
16
16
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
return
|
|
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
|
|
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
|
+
}
|