rayzee 5.8.1 → 5.8.3
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/dist/rayzee.es.js +81 -83
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +2 -2
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/Passes/OIDNDenoiser.js +7 -7
- package/src/Processor/ToneMapCPU.js +12 -1
- package/src/Stages/ASVGF.js +62 -248
package/src/Stages/ASVGF.js
CHANGED
|
@@ -1,58 +1,19 @@
|
|
|
1
|
-
import { Fn,
|
|
1
|
+
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
2
|
If, dot, max, min, abs, mix,
|
|
3
3
|
textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
5
|
import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter } from 'three';
|
|
6
6
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
7
|
import { createRenderTargetHelper } from '../Processor/createRenderTargetHelper.js';
|
|
8
|
-
import { luminance
|
|
9
|
-
|
|
10
|
-
// ── wgslFn helpers ──────────────────────────────────────────
|
|
8
|
+
import { luminance } from '../TSL/Common.js';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
11
|
+
* WebGPU ASVGF Stage — temporal denoising via motion-vector reprojection
|
|
12
|
+
* + 3×3 colour-distance disocclusion rejection. Ping-pong StorageTextures.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
const gradientAdaptiveAlpha = /*@__PURE__*/ wgslFn( `
|
|
20
|
-
fn gradientAdaptiveAlpha(
|
|
21
|
-
gradient: f32,
|
|
22
|
-
baseAlpha: f32,
|
|
23
|
-
scale: f32,
|
|
24
|
-
gMin: f32,
|
|
25
|
-
gMax: f32
|
|
26
|
-
) -> f32 {
|
|
27
|
-
|
|
28
|
-
let remapped = clamp( ( gradient - gMin ) / max( gMax - gMin, 0.001 ), 0.0, 1.0 );
|
|
29
|
-
return clamp( baseAlpha + ( 1.0 - baseAlpha ) * remapped * scale, baseAlpha, 1.0 );
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
` );
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* WebGPU ASVGF Stage (Compute Shader)
|
|
36
|
-
*
|
|
37
|
-
* Adaptive Spatio-Temporal Variance-Guided Filtering for real-time denoising.
|
|
38
|
-
* Two compute passes per frame with ping-pong StorageTextures.
|
|
39
|
-
*
|
|
40
|
-
* Algorithm:
|
|
41
|
-
* 1. Gradient (compute): Cooperative tile load → 3×3 brightest search
|
|
42
|
-
* → motion reprojection → normalized luminance gradient
|
|
43
|
-
* 2. Temporal accumulation (compute): Motion validity check
|
|
44
|
-
* → normal/depth edge-stopping → 3×3 variance clipping
|
|
45
|
-
* → gradient-adaptive alpha → temporal blend + history tracking
|
|
46
|
-
* → write current ND to prevND for next frame
|
|
47
|
-
*
|
|
48
|
-
* Execution: PER_CYCLE
|
|
49
|
-
*
|
|
50
|
-
* Events listened:
|
|
51
|
-
* asvgf:reset, asvgf:setTemporal, asvgf:updateParameters,
|
|
52
|
-
* camera:moved, pipeline:reset
|
|
53
|
-
*
|
|
54
|
-
* Textures published: asvgf:output, asvgf:temporalColor
|
|
55
|
-
* Textures read: pathtracer:color, pathtracer:normalDepth, motionVector:screenSpace
|
|
14
|
+
* Events: asvgf:reset, asvgf:setTemporal, asvgf:updateParameters
|
|
15
|
+
* Publishes: asvgf:output, asvgf:temporalColor
|
|
16
|
+
* Reads: pathtracer:color, motionVector:screenSpace
|
|
56
17
|
*/
|
|
57
18
|
export class ASVGF extends RenderStage {
|
|
58
19
|
|
|
@@ -66,41 +27,25 @@ export class ASVGF extends RenderStage {
|
|
|
66
27
|
this.renderer = renderer;
|
|
67
28
|
this.debugContainer = options.debugContainer || null;
|
|
68
29
|
|
|
69
|
-
// Parameters
|
|
70
30
|
this.temporalAlpha = uniform( options.temporalAlpha ?? 0.1 );
|
|
71
|
-
this.gradientScale = uniform( options.gradientScale ?? 2.0 );
|
|
72
|
-
this.gradientMin = uniform( options.gradientMin ?? 0.01 );
|
|
73
|
-
this.gradientMax = uniform( options.gradientMax ?? 0.5 );
|
|
74
31
|
this.phiColor = uniform( options.phiColor ?? 10.0 );
|
|
75
|
-
this.phiNormal = uniform( options.phiNormal ?? 128.0 );
|
|
76
|
-
this.phiDepth = uniform( options.phiDepth ?? 1.0 );
|
|
77
32
|
this.maxAccumFrames = uniform( options.maxAccumFrames ?? 32.0 );
|
|
78
33
|
this.varianceClip = uniform( options.varianceClip ?? 1.0 );
|
|
79
34
|
|
|
80
|
-
// Resolution
|
|
81
35
|
this.resW = uniform( options.width || 1 );
|
|
82
36
|
this.resH = uniform( options.height || 1 );
|
|
83
37
|
|
|
84
|
-
// Temporal control
|
|
85
38
|
this.temporalEnabled = true;
|
|
86
|
-
this.temporalEnabledU = uniform( 1.0 );
|
|
39
|
+
this.temporalEnabledU = uniform( 1.0 );
|
|
87
40
|
|
|
88
|
-
// Input texture nodes (context textures — always regular Textures)
|
|
89
41
|
this._colorTexNode = new TextureNode();
|
|
90
|
-
this._normalDepthTexNode = new TextureNode();
|
|
91
42
|
this._motionTexNode = new TextureNode();
|
|
92
|
-
|
|
93
|
-
// Read-side TextureNode wrappers (pre-compile with EmptyTexture,
|
|
94
|
-
// then set to StorageTextures at runtime)
|
|
95
43
|
this._readTemporalTexNode = new TextureNode();
|
|
96
|
-
this._readPrevNDTexNode = new TextureNode();
|
|
97
|
-
this._gradientReadTexNode = new TextureNode();
|
|
98
44
|
|
|
99
|
-
// Ping-pong StorageTextures
|
|
100
45
|
const w = options.width || 1;
|
|
101
46
|
const h = options.height || 1;
|
|
102
47
|
|
|
103
|
-
// LinearFilter for textureLoad codegen
|
|
48
|
+
// LinearFilter required for textureLoad codegen.
|
|
104
49
|
this._temporalTexA = new StorageTexture( w, h );
|
|
105
50
|
this._temporalTexA.type = HalfFloatType;
|
|
106
51
|
this._temporalTexA.format = RGBAFormat;
|
|
@@ -113,18 +58,6 @@ export class ASVGF extends RenderStage {
|
|
|
113
58
|
this._temporalTexB.minFilter = LinearFilter;
|
|
114
59
|
this._temporalTexB.magFilter = LinearFilter;
|
|
115
60
|
|
|
116
|
-
this._prevNDTexA = new StorageTexture( w, h );
|
|
117
|
-
this._prevNDTexA.type = HalfFloatType;
|
|
118
|
-
this._prevNDTexA.format = RGBAFormat;
|
|
119
|
-
this._prevNDTexA.minFilter = LinearFilter;
|
|
120
|
-
this._prevNDTexA.magFilter = LinearFilter;
|
|
121
|
-
|
|
122
|
-
this._prevNDTexB = new StorageTexture( w, h );
|
|
123
|
-
this._prevNDTexB.type = HalfFloatType;
|
|
124
|
-
this._prevNDTexB.format = RGBAFormat;
|
|
125
|
-
this._prevNDTexB.minFilter = LinearFilter;
|
|
126
|
-
this._prevNDTexB.magFilter = LinearFilter;
|
|
127
|
-
|
|
128
61
|
this._gradientStorageTex = new StorageTexture( w, h );
|
|
129
62
|
this._gradientStorageTex.type = HalfFloatType;
|
|
130
63
|
this._gradientStorageTex.format = RGBAFormat;
|
|
@@ -134,16 +67,12 @@ export class ASVGF extends RenderStage {
|
|
|
134
67
|
this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
|
|
135
68
|
this._compiled = false;
|
|
136
69
|
|
|
137
|
-
// Dispatch dimensions
|
|
138
70
|
this._dispatchX = Math.ceil( w / 8 );
|
|
139
71
|
this._dispatchY = Math.ceil( h / 8 );
|
|
140
72
|
|
|
141
|
-
// Build compute nodes
|
|
142
73
|
this._buildGradientCompute();
|
|
143
74
|
this._buildTemporalCompute();
|
|
144
75
|
|
|
145
|
-
// ── Heatmap debug visualization (compute shader) ──
|
|
146
|
-
|
|
147
76
|
this.showHeatmap = false;
|
|
148
77
|
this.debugMode = uniform( 0, 'int' );
|
|
149
78
|
|
|
@@ -162,7 +91,7 @@ export class ASVGF extends RenderStage {
|
|
|
162
91
|
stencilBuffer: false
|
|
163
92
|
} );
|
|
164
93
|
|
|
165
|
-
// Separate
|
|
94
|
+
// Separate from temporal-pass nodes to avoid binding interference.
|
|
166
95
|
this._heatmapRawColorTexNode = new TextureNode();
|
|
167
96
|
this._heatmapColorTexNode = new TextureNode();
|
|
168
97
|
this._heatmapTemporalTexNode = new TextureNode();
|
|
@@ -187,18 +116,9 @@ export class ASVGF extends RenderStage {
|
|
|
187
116
|
|
|
188
117
|
}
|
|
189
118
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Build gradient compute node with shared memory 3×3 brightest search.
|
|
196
|
-
*
|
|
197
|
-
* Workgroup [8,8,1] loads a 10×10 luminance tile into shared memory.
|
|
198
|
-
* Each thread finds the brightest pixel in its 3×3 neighbourhood,
|
|
199
|
-
* reads the motion vector there, reprojects, and computes the
|
|
200
|
-
* normalized luminance gradient against the previous frame.
|
|
201
|
-
*/
|
|
119
|
+
// Temporal-luminance gradient (heatmap mode 5 only).
|
|
120
|
+
// 10×10 shared tile; each thread does a 3×3 brightest search, reprojects,
|
|
121
|
+
// and emits the normalized luminance delta against the previous frame.
|
|
202
122
|
_buildGradientCompute() {
|
|
203
123
|
|
|
204
124
|
const colorTex = this._colorTexNode;
|
|
@@ -225,9 +145,7 @@ export class ASVGF extends RenderStage {
|
|
|
225
145
|
const tileOriginX = int( workgroupId.x ).mul( WG_SIZE ).sub( 1 );
|
|
226
146
|
const tileOriginY = int( workgroupId.y ).mul( WG_SIZE ).sub( 1 );
|
|
227
147
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
// Load #1: all 64 threads load positions 0-63
|
|
148
|
+
// All 64 threads load positions 0–63; threads 0–35 then load 64–99.
|
|
231
149
|
const sx1 = linearIdx.mod( TILE_W );
|
|
232
150
|
const sy1 = linearIdx.div( TILE_W );
|
|
233
151
|
const gx1 = tileOriginX.add( int( sx1 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
@@ -235,7 +153,6 @@ export class ASVGF extends RenderStage {
|
|
|
235
153
|
const sColor1 = textureLoad( colorTex, ivec2( gx1, gy1 ) ).xyz;
|
|
236
154
|
sharedLum.element( linearIdx ).assign( luminance( sColor1 ) );
|
|
237
155
|
|
|
238
|
-
// Load #2: threads 0-35 load positions 64-99
|
|
239
156
|
If( linearIdx.lessThan( uint( EXTRA_LOAD ) ), () => {
|
|
240
157
|
|
|
241
158
|
const idx2 = linearIdx.add( uint( WG_THREADS ) );
|
|
@@ -250,14 +167,11 @@ export class ASVGF extends RenderStage {
|
|
|
250
167
|
|
|
251
168
|
workgroupBarrier();
|
|
252
169
|
|
|
253
|
-
// ── Per-thread gradient computation ──────────────
|
|
254
|
-
|
|
255
170
|
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( lx ) );
|
|
256
171
|
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( ly ) );
|
|
257
172
|
|
|
258
173
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
259
174
|
|
|
260
|
-
// Find brightest in 3×3 from shared memory
|
|
261
175
|
const bestLum = float( - 1.0 ).toVar();
|
|
262
176
|
const bestDx = int( 0 ).toVar();
|
|
263
177
|
const bestDy = int( 0 ).toVar();
|
|
@@ -282,22 +196,19 @@ export class ASVGF extends RenderStage {
|
|
|
282
196
|
|
|
283
197
|
}
|
|
284
198
|
|
|
285
|
-
// Read motion at brightest pixel
|
|
286
199
|
const bestGx = gx.add( bestDx ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
287
200
|
const bestGy = gy.add( bestDy ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
288
201
|
const motion = textureLoad( motionTex, ivec2( bestGx, bestGy ) );
|
|
289
202
|
|
|
290
|
-
// Reproject via motion vector (UV-space → pixel coords)
|
|
203
|
+
// Reproject via motion vector (UV-space → pixel coords).
|
|
291
204
|
const prevXf = float( bestGx ).sub( motion.x.mul( resW ) );
|
|
292
205
|
const prevYf = float( bestGy ).sub( motion.y.mul( resH ) );
|
|
293
206
|
const prevX = int( prevXf ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
294
207
|
const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
295
208
|
|
|
296
|
-
// Previous frame luminance at reprojected position
|
|
297
209
|
const prevColor = textureLoad( prevTemporalTex, ivec2( prevX, prevY ) ).xyz;
|
|
298
210
|
const prevLum = luminance( prevColor );
|
|
299
211
|
|
|
300
|
-
// Temporal gradient = normalized luminance difference
|
|
301
212
|
const gradient = abs( bestLum.sub( prevLum ) )
|
|
302
213
|
.div( max( bestLum, float( 0.001 ) ) )
|
|
303
214
|
.clamp( 0.0, 1.0 );
|
|
@@ -319,43 +230,21 @@ export class ASVGF extends RenderStage {
|
|
|
319
230
|
|
|
320
231
|
}
|
|
321
232
|
|
|
322
|
-
//
|
|
323
|
-
// Compute pass 2: Temporal Accumulation
|
|
324
|
-
// ──────────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Build two temporal compute nodes — one for each ping-pong direction.
|
|
328
|
-
*
|
|
329
|
-
* Each node writes to its temporal StorageTexture (accumulated color + history)
|
|
330
|
-
* AND its prevND StorageTexture (current ND saved for next frame),
|
|
331
|
-
* eliminating the need for a separate copy pass.
|
|
332
|
-
*/
|
|
233
|
+
// One temporal node per ping-pong direction.
|
|
333
234
|
_buildTemporalCompute() {
|
|
334
235
|
|
|
335
|
-
this._temporalNodeA = this._buildTemporalForDirection(
|
|
336
|
-
|
|
337
|
-
);
|
|
338
|
-
this._temporalNodeB = this._buildTemporalForDirection(
|
|
339
|
-
this._temporalTexB, this._prevNDTexB
|
|
340
|
-
);
|
|
236
|
+
this._temporalNodeA = this._buildTemporalForDirection( this._temporalTexA );
|
|
237
|
+
this._temporalNodeB = this._buildTemporalForDirection( this._temporalTexB );
|
|
341
238
|
|
|
342
239
|
}
|
|
343
240
|
|
|
344
|
-
_buildTemporalForDirection( writeTemporalTex
|
|
241
|
+
_buildTemporalForDirection( writeTemporalTex ) {
|
|
345
242
|
|
|
346
243
|
const colorTex = this._colorTexNode;
|
|
347
|
-
const ndTex = this._normalDepthTexNode;
|
|
348
244
|
const motionTex = this._motionTexNode;
|
|
349
245
|
const prevTemporalTex = this._readTemporalTexNode;
|
|
350
|
-
const prevNDTex = this._readPrevNDTexNode;
|
|
351
|
-
const gradientTex = this._gradientReadTexNode;
|
|
352
246
|
|
|
353
247
|
const temporalAlpha = this.temporalAlpha;
|
|
354
|
-
const gradientScale = this.gradientScale;
|
|
355
|
-
const gradientMinU = this.gradientMin;
|
|
356
|
-
const gradientMaxU = this.gradientMax;
|
|
357
|
-
const phiNormal = this.phiNormal;
|
|
358
|
-
const phiDepth = this.phiDepth;
|
|
359
248
|
const maxAccumFrames = this.maxAccumFrames;
|
|
360
249
|
const varianceClipU = this.varianceClip;
|
|
361
250
|
const temporalEnabledU = this.temporalEnabledU;
|
|
@@ -373,18 +262,15 @@ export class ASVGF extends RenderStage {
|
|
|
373
262
|
|
|
374
263
|
const coord = ivec2( gx, gy );
|
|
375
264
|
const currentColor = textureLoad( colorTex, coord ).xyz;
|
|
376
|
-
const currentND = textureLoad( ndTex, coord );
|
|
377
265
|
|
|
378
|
-
// Default:
|
|
266
|
+
// Default: history = 1, no blend (used when temporal off or reprojection invalid).
|
|
379
267
|
const result = vec4( currentColor, 1.0 ).toVar();
|
|
380
268
|
|
|
381
269
|
If( temporalEnabledU.greaterThan( 0.5 ), () => {
|
|
382
270
|
|
|
383
|
-
// Read motion vector
|
|
384
271
|
const motion = textureLoad( motionTex, coord );
|
|
385
272
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
386
273
|
|
|
387
|
-
// Reprojected pixel coords
|
|
388
274
|
const prevXf = float( gx ).sub( motion.x.mul( resW ) );
|
|
389
275
|
const prevYf = float( gy ).sub( motion.y.mul( resH ) );
|
|
390
276
|
const prevOnScreen = prevXf.greaterThanEqual( 0.0 )
|
|
@@ -398,29 +284,16 @@ export class ASVGF extends RenderStage {
|
|
|
398
284
|
const prevY = int( prevYf ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
399
285
|
const prevCoord = ivec2( prevX, prevY );
|
|
400
286
|
|
|
401
|
-
// Normal/depth similarity check
|
|
402
|
-
const currentNormal = currentND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
403
|
-
const prevND = textureLoad( prevNDTex, prevCoord );
|
|
404
|
-
const prevNormal = prevND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
405
|
-
|
|
406
|
-
const similarity = normalDepthWeight(
|
|
407
|
-
currentNormal, prevNormal,
|
|
408
|
-
currentND.w, prevND.w,
|
|
409
|
-
phiNormal, phiDepth
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
// Previous frame colour + history length
|
|
413
287
|
const prevData = textureLoad( prevTemporalTex, prevCoord );
|
|
414
288
|
const prevColor = prevData.xyz;
|
|
415
289
|
const historyLength = prevData.w;
|
|
416
290
|
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
const nMin = vec3( 1e10 ).toVar();
|
|
422
|
-
const nMax = vec3( - 1e10 ).toVar();
|
|
291
|
+
// Euclidean colour-distance gate. Per-channel clipping
|
|
292
|
+
// fails at silhouettes where wall/box colours overlap
|
|
293
|
+
// in per-channel ranges. A normal/depth gate would be
|
|
294
|
+
// stronger but shading normals jitter too much for it.
|
|
423
295
|
const nMean = vec3( 0.0 ).toVar();
|
|
296
|
+
const nMeanSq = vec3( 0.0 ).toVar();
|
|
424
297
|
|
|
425
298
|
for ( let dy = - 1; dy <= 1; dy ++ ) {
|
|
426
299
|
|
|
@@ -429,67 +302,53 @@ export class ASVGF extends RenderStage {
|
|
|
429
302
|
const sx = gx.add( dx ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
430
303
|
const sy = gy.add( dy ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
431
304
|
const s = textureLoad( colorTex, ivec2( sx, sy ) ).xyz;
|
|
432
|
-
nMin.assign( min( nMin, s ) );
|
|
433
|
-
nMax.assign( max( nMax, s ) );
|
|
434
305
|
nMean.addAssign( s );
|
|
306
|
+
nMeanSq.addAssign( s.mul( s ) );
|
|
435
307
|
|
|
436
308
|
}
|
|
437
309
|
|
|
438
310
|
}
|
|
439
311
|
|
|
440
312
|
nMean.divAssign( 9.0 );
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// disocclusion; gradient not used here because with 1 SPP
|
|
454
|
-
// input the gradient is dominated by Monte Carlo noise, not
|
|
455
|
-
// scene changes, which drives alpha toward 1.0 and kills
|
|
456
|
-
// accumulation.
|
|
457
|
-
const effectiveAlpha = max(
|
|
313
|
+
nMeanSq.divAssign( 9.0 );
|
|
314
|
+
const nVariance = max( nMeanSq.sub( nMean.mul( nMean ) ), vec3( 0.0 ) );
|
|
315
|
+
const sigmaSq = dot( nVariance, vec3( 1.0 ) );
|
|
316
|
+
|
|
317
|
+
// reject ∈ [0,1]: 0 = matches mean (keep history),
|
|
318
|
+
// 1 = >k·σ away (force fresh sample). Squared form skips sqrt.
|
|
319
|
+
const diff = prevColor.sub( nMean );
|
|
320
|
+
const distSq = dot( diff, diff );
|
|
321
|
+
const sigmaSqK = sigmaSq.mul( varianceClipU.mul( varianceClipU ) );
|
|
322
|
+
const reject = distSq.div( max( sigmaSqK, float( 1e-6 ) ) ).clamp( 0.0, 1.0 );
|
|
323
|
+
|
|
324
|
+
const baseAlpha = max(
|
|
458
325
|
float( 1.0 ).div( historyLength.add( 1.0 ) ),
|
|
459
326
|
temporalAlpha
|
|
460
327
|
);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
328
|
+
const effectiveAlpha = mix( baseAlpha, float( 1.0 ), reject );
|
|
329
|
+
const blended = mix( prevColor, currentColor, effectiveAlpha );
|
|
330
|
+
const newHistory = mix(
|
|
331
|
+
min( historyLength.add( 1.0 ), maxAccumFrames ),
|
|
332
|
+
float( 1.0 ),
|
|
333
|
+
reject
|
|
334
|
+
);
|
|
467
335
|
|
|
468
336
|
result.assign( vec4( blended, newHistory ) );
|
|
469
337
|
|
|
470
338
|
} ).Else( () => {
|
|
471
339
|
|
|
472
|
-
// No valid reprojection — use current colour, reset history
|
|
473
340
|
result.assign( vec4( currentColor, 1.0 ) );
|
|
474
341
|
|
|
475
342
|
} );
|
|
476
343
|
|
|
477
344
|
} );
|
|
478
345
|
|
|
479
|
-
// Write temporal result (colour + history in .w)
|
|
480
346
|
textureStore(
|
|
481
347
|
writeTemporalTex,
|
|
482
348
|
uvec2( uint( gx ), uint( gy ) ),
|
|
483
349
|
result
|
|
484
350
|
).toWriteOnly();
|
|
485
351
|
|
|
486
|
-
// Write current ND to prevND for next frame
|
|
487
|
-
textureStore(
|
|
488
|
-
writePrevNDTex,
|
|
489
|
-
uvec2( uint( gx ), uint( gy ) ),
|
|
490
|
-
currentND
|
|
491
|
-
).toWriteOnly();
|
|
492
|
-
|
|
493
352
|
} );
|
|
494
353
|
|
|
495
354
|
} );
|
|
@@ -501,10 +360,6 @@ export class ASVGF extends RenderStage {
|
|
|
501
360
|
|
|
502
361
|
}
|
|
503
362
|
|
|
504
|
-
// ──────────────────────────────────────────────────
|
|
505
|
-
// Heatmap debug visualization (compute shader)
|
|
506
|
-
// ──────────────────────────────────────────────────
|
|
507
|
-
|
|
508
363
|
_buildHeatmapCompute() {
|
|
509
364
|
|
|
510
365
|
const rawColorTex = this._heatmapRawColorTexNode;
|
|
@@ -530,11 +385,10 @@ export class ASVGF extends RenderStage {
|
|
|
530
385
|
const coord = ivec2( gx, gy );
|
|
531
386
|
const result = vec4( 0.0, 0.0, 0.0, 1.0 ).toVar();
|
|
532
387
|
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// from inactive branches can contaminate the output.
|
|
388
|
+
// Must be chained If/ElseIf — separate If blocks let inactive-
|
|
389
|
+
// branch texture samples contaminate the output.
|
|
536
390
|
|
|
537
|
-
//
|
|
391
|
+
// 0: beauty
|
|
538
392
|
If( mode.equal( int( 0 ) ), () => {
|
|
539
393
|
|
|
540
394
|
const c = textureLoad( colorTex, coord ).xyz;
|
|
@@ -542,9 +396,7 @@ export class ASVGF extends RenderStage {
|
|
|
542
396
|
|
|
543
397
|
} ).ElseIf( mode.equal( int( 1 ) ), () => {
|
|
544
398
|
|
|
545
|
-
//
|
|
546
|
-
// Computes E[L²] - E[L]² over a 3×3 neighbourhood, which correctly
|
|
547
|
-
// highlights noisy regions regardless of accumulation state.
|
|
399
|
+
// 1: 3×3 luminance variance of raw path-tracer input
|
|
548
400
|
const meanLum = float( 0.0 ).toVar();
|
|
549
401
|
const meanLumSq = float( 0.0 ).toVar();
|
|
550
402
|
|
|
@@ -566,9 +418,7 @@ export class ASVGF extends RenderStage {
|
|
|
566
418
|
meanLum.divAssign( 9.0 );
|
|
567
419
|
meanLumSq.divAssign( 9.0 );
|
|
568
420
|
const variance = max( meanLumSq.sub( meanLum.mul( meanLum ) ), float( 0.0 ) );
|
|
569
|
-
|
|
570
|
-
// Relative variance (normalise by mean to handle HDR range),
|
|
571
|
-
// then scale into 0-1 for the heatmap.
|
|
421
|
+
// Normalise by mean for HDR, scale to [0,1] for ramp.
|
|
572
422
|
const relVar = variance.div( max( meanLum.mul( meanLum ), float( 0.0001 ) ) );
|
|
573
423
|
const t = relVar.mul( 10.0 ).clamp( 0.0, 1.0 );
|
|
574
424
|
|
|
@@ -582,14 +432,14 @@ export class ASVGF extends RenderStage {
|
|
|
582
432
|
|
|
583
433
|
} ).ElseIf( mode.equal( int( 2 ) ), () => {
|
|
584
434
|
|
|
585
|
-
//
|
|
435
|
+
// 2: history length
|
|
586
436
|
const historyLength = textureLoad( temporalTex, coord ).w;
|
|
587
437
|
const t = historyLength.div( 32.0 ).clamp( 0.0, 1.0 );
|
|
588
438
|
result.assign( vec4( float( 1.0 ).sub( t ), t, float( 0.2 ), 1.0 ) );
|
|
589
439
|
|
|
590
440
|
} ).ElseIf( mode.equal( int( 3 ) ), () => {
|
|
591
441
|
|
|
592
|
-
//
|
|
442
|
+
// 3: motion vectors
|
|
593
443
|
const motion = textureLoad( motionTex, coord );
|
|
594
444
|
const mx = abs( motion.x ).mul( 100.0 ).clamp( 0.0, 1.0 );
|
|
595
445
|
const my = abs( motion.y ).mul( 100.0 ).clamp( 0.0, 1.0 );
|
|
@@ -598,13 +448,13 @@ export class ASVGF extends RenderStage {
|
|
|
598
448
|
|
|
599
449
|
} ).ElseIf( mode.equal( int( 4 ) ), () => {
|
|
600
450
|
|
|
601
|
-
//
|
|
451
|
+
// 4: normals
|
|
602
452
|
const nd = textureLoad( ndTex, coord );
|
|
603
453
|
result.assign( vec4( nd.xyz, 1.0 ) );
|
|
604
454
|
|
|
605
455
|
} ).Else( () => {
|
|
606
456
|
|
|
607
|
-
//
|
|
457
|
+
// 5: temporal-luminance gradient
|
|
608
458
|
const grad = textureLoad( gradientTex, coord ).x;
|
|
609
459
|
const t = grad.mul( 5.0 ).clamp( 0.0, 1.0 );
|
|
610
460
|
result.assign( vec4( t, t.mul( 0.5 ), float( 1.0 ).sub( t ), 1.0 ) );
|
|
@@ -628,10 +478,6 @@ export class ASVGF extends RenderStage {
|
|
|
628
478
|
|
|
629
479
|
}
|
|
630
480
|
|
|
631
|
-
// ──────────────────────────────────────────────────
|
|
632
|
-
// Pipeline lifecycle
|
|
633
|
-
// ──────────────────────────────────────────────────
|
|
634
|
-
|
|
635
481
|
setupEventListeners() {
|
|
636
482
|
|
|
637
483
|
this.on( 'asvgf:reset', () => this.resetTemporalData() );
|
|
@@ -651,10 +497,7 @@ export class ASVGF extends RenderStage {
|
|
|
651
497
|
|
|
652
498
|
if ( ! data ) return;
|
|
653
499
|
if ( data.temporalAlpha !== undefined ) this.temporalAlpha.value = data.temporalAlpha;
|
|
654
|
-
if ( data.gradientScale !== undefined ) this.gradientScale.value = data.gradientScale;
|
|
655
500
|
if ( data.phiColor !== undefined ) this.phiColor.value = data.phiColor;
|
|
656
|
-
if ( data.phiNormal !== undefined ) this.phiNormal.value = data.phiNormal;
|
|
657
|
-
if ( data.phiDepth !== undefined ) this.phiDepth.value = data.phiDepth;
|
|
658
501
|
|
|
659
502
|
} );
|
|
660
503
|
|
|
@@ -664,14 +507,12 @@ export class ASVGF extends RenderStage {
|
|
|
664
507
|
|
|
665
508
|
if ( ! this.enabled ) return;
|
|
666
509
|
|
|
667
|
-
// Read inputs from context
|
|
668
510
|
const colorTex = context.getTexture( 'pathtracer:color' );
|
|
669
511
|
const normalDepthTex = context.getTexture( 'pathtracer:normalDepth' );
|
|
670
512
|
const motionTex = context.getTexture( 'motionVector:screenSpace' );
|
|
671
513
|
|
|
672
514
|
if ( ! colorTex ) return;
|
|
673
515
|
|
|
674
|
-
// Auto-size
|
|
675
516
|
const img = colorTex.image;
|
|
676
517
|
if ( img && img.width > 0 && img.height > 0 ) {
|
|
677
518
|
|
|
@@ -684,14 +525,11 @@ export class ASVGF extends RenderStage {
|
|
|
684
525
|
|
|
685
526
|
}
|
|
686
527
|
|
|
687
|
-
// Update context texture nodes
|
|
688
528
|
this._colorTexNode.value = colorTex;
|
|
689
|
-
if ( normalDepthTex ) this._normalDepthTexNode.value = normalDepthTex;
|
|
690
529
|
if ( motionTex ) this._motionTexNode.value = motionTex;
|
|
691
530
|
|
|
692
|
-
// Force-compile
|
|
693
|
-
//
|
|
694
|
-
// includes the required level parameter.
|
|
531
|
+
// Force first-frame compile while TextureNodes still hold EmptyTexture,
|
|
532
|
+
// so textureLoad codegen emits the required `level` parameter.
|
|
695
533
|
if ( ! this._compiled ) {
|
|
696
534
|
|
|
697
535
|
this.renderer.compute( this._gradientNode );
|
|
@@ -701,35 +539,26 @@ export class ASVGF extends RenderStage {
|
|
|
701
539
|
|
|
702
540
|
}
|
|
703
541
|
|
|
704
|
-
// Ping-pong
|
|
542
|
+
// Ping-pong: read opposite, write current
|
|
705
543
|
const readTemporal = this.currentMoments === 0
|
|
706
544
|
? this._temporalTexB : this._temporalTexA;
|
|
707
|
-
const readPrevND = this.currentMoments === 0
|
|
708
|
-
? this._prevNDTexB : this._prevNDTexA;
|
|
709
545
|
const writeNode = this.currentMoments === 0
|
|
710
546
|
? this._temporalNodeA : this._temporalNodeB;
|
|
711
547
|
const writeTemporal = this.currentMoments === 0
|
|
712
548
|
? this._temporalTexA : this._temporalTexB;
|
|
713
549
|
|
|
714
|
-
// Pass 1: Temporal gradient (shared memory 3×3 brightest search)
|
|
715
550
|
this._readTemporalTexNode.value = readTemporal;
|
|
716
|
-
this.renderer.compute( this._gradientNode );
|
|
717
|
-
|
|
718
|
-
// Pass 2: Temporal accumulation + prevND write
|
|
719
|
-
this._gradientReadTexNode.value = this._gradientStorageTex;
|
|
720
|
-
this._readPrevNDTexNode.value = readPrevND;
|
|
721
551
|
this.renderer.compute( writeNode );
|
|
722
552
|
|
|
723
|
-
// Publish outputs
|
|
724
553
|
context.setTexture( 'asvgf:output', writeTemporal );
|
|
725
554
|
context.setTexture( 'asvgf:temporalColor', writeTemporal );
|
|
726
555
|
|
|
727
|
-
// Swap for next frame
|
|
728
556
|
this.currentMoments = 1 - this.currentMoments;
|
|
729
557
|
|
|
730
|
-
// Render heatmap debug overlay if enabled
|
|
731
558
|
if ( this.showHeatmap ) {
|
|
732
559
|
|
|
560
|
+
this.renderer.compute( this._gradientNode );
|
|
561
|
+
|
|
733
562
|
this._heatmapRawColorTexNode.value = colorTex;
|
|
734
563
|
this._heatmapColorTexNode.value = writeTemporal;
|
|
735
564
|
this._heatmapTemporalTexNode.value = writeTemporal;
|
|
@@ -772,10 +601,7 @@ export class ASVGF extends RenderStage {
|
|
|
772
601
|
|
|
773
602
|
if ( ! params ) return;
|
|
774
603
|
if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
|
|
775
|
-
if ( params.gradientScale !== undefined ) this.gradientScale.value = params.gradientScale;
|
|
776
604
|
if ( params.phiColor !== undefined ) this.phiColor.value = params.phiColor;
|
|
777
|
-
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
778
|
-
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
779
605
|
if ( params.debugMode !== undefined ) this.debugMode.value = params.debugMode;
|
|
780
606
|
|
|
781
607
|
}
|
|
@@ -791,8 +617,6 @@ export class ASVGF extends RenderStage {
|
|
|
791
617
|
|
|
792
618
|
this._temporalTexA.setSize( width, height );
|
|
793
619
|
this._temporalTexB.setSize( width, height );
|
|
794
|
-
this._prevNDTexA.setSize( width, height );
|
|
795
|
-
this._prevNDTexB.setSize( width, height );
|
|
796
620
|
this._gradientStorageTex.setSize( width, height );
|
|
797
621
|
this._heatmapStorageTex.setSize( width, height );
|
|
798
622
|
this.heatmapTarget.setSize( width, height );
|
|
@@ -800,7 +624,6 @@ export class ASVGF extends RenderStage {
|
|
|
800
624
|
this.resW.value = width;
|
|
801
625
|
this.resH.value = height;
|
|
802
626
|
|
|
803
|
-
// Update dispatch dimensions
|
|
804
627
|
this._dispatchX = Math.ceil( width / 8 );
|
|
805
628
|
this._dispatchY = Math.ceil( height / 8 );
|
|
806
629
|
this._gradientNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
@@ -812,10 +635,8 @@ export class ASVGF extends RenderStage {
|
|
|
812
635
|
|
|
813
636
|
reset() {
|
|
814
637
|
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
// camera movement. Only explicit 'asvgf:reset' events (scene change,
|
|
818
|
-
// render mode switch) should clear temporal history.
|
|
638
|
+
// No-op: motion vectors handle camera moves; only explicit
|
|
639
|
+
// 'asvgf:reset' (scene change, render-mode switch) clears history.
|
|
819
640
|
|
|
820
641
|
}
|
|
821
642
|
|
|
@@ -826,22 +647,15 @@ export class ASVGF extends RenderStage {
|
|
|
826
647
|
this._temporalNodeB?.dispose();
|
|
827
648
|
this._temporalTexA?.dispose();
|
|
828
649
|
this._temporalTexB?.dispose();
|
|
829
|
-
this._prevNDTexA?.dispose();
|
|
830
|
-
this._prevNDTexB?.dispose();
|
|
831
650
|
this._gradientStorageTex?.dispose();
|
|
832
651
|
this._heatmapComputeNode?.dispose();
|
|
833
652
|
this._heatmapStorageTex?.dispose();
|
|
834
653
|
this.heatmapTarget?.dispose();
|
|
835
654
|
|
|
836
|
-
// Dispose input TextureNode objects
|
|
837
655
|
this._colorTexNode?.dispose();
|
|
838
|
-
this._normalDepthTexNode?.dispose();
|
|
839
656
|
this._motionTexNode?.dispose();
|
|
840
657
|
this._readTemporalTexNode?.dispose();
|
|
841
|
-
this._readPrevNDTexNode?.dispose();
|
|
842
|
-
this._gradientReadTexNode?.dispose();
|
|
843
658
|
|
|
844
|
-
// Dispose heatmap TextureNode objects
|
|
845
659
|
this._heatmapRawColorTexNode?.dispose();
|
|
846
660
|
this._heatmapColorTexNode?.dispose();
|
|
847
661
|
this._heatmapTemporalTexNode?.dispose();
|
|
@@ -849,7 +663,7 @@ export class ASVGF extends RenderStage {
|
|
|
849
663
|
this._heatmapMotionTexNode?.dispose();
|
|
850
664
|
this._heatmapGradientTexNode?.dispose();
|
|
851
665
|
|
|
852
|
-
//
|
|
666
|
+
// also removes the DOM node
|
|
853
667
|
this.heatmapHelper?.dispose();
|
|
854
668
|
|
|
855
669
|
}
|