rayzee 7.29.0 → 7.30.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/dist/rayzee.es.js +3724 -3009
- package/dist/rayzee.umd.js +56 -56
- package/package.json +1 -1
- package/src/EngineDefaults.js +272 -19
- package/src/EngineIssues.js +1 -0
- package/src/PathTracerApp.js +10 -1
- package/src/Processor/GeometryExtractor.js +21 -3
- package/src/Processor/PackedRayBuffer.js +26 -5
- package/src/Processor/SceneProcessor.js +71 -24
- package/src/Processor/StorageTexturePool.js +8 -4
- package/src/Processor/TextureCreator.js +8 -9
- package/src/Processor/Workers/TexturesWorker.js +13 -14
- package/src/Processor/utils.js +0 -6
- package/src/RenderSettings.js +4 -4
- package/src/Stages/Compositor.js +1 -0
- package/src/Stages/NRD.js +1395 -0
- package/src/Stages/NormalDepth.js +8 -4
- package/src/Stages/PathTracerStage.js +4 -0
- package/src/TSL/Common.js +8 -1
- package/src/TSL/FinalWriteKernel.js +9 -4
- package/src/TSL/GenerateKernel.js +1 -1
- package/src/TSL/ShadeKernel.js +14 -2
- package/src/TSL/TextureSampling.js +3 -2
- package/src/index.js +5 -0
- package/src/managers/DenoisingManager.js +80 -21
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
import { Fn, vec2, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
+
If, select, mix, max, min, dot, sqrt, exp2, pow, floor, fract, round, atan, normalize,
|
|
3
|
+
saturate, smoothstep, textureLoad, textureStore, localId, workgroupId, Return } from 'three/tsl';
|
|
4
|
+
import { RenderTarget, TextureNode } from 'three/webgpu';
|
|
5
|
+
import { DataTexture, HalfFloatType, FloatType, RGBAFormat, LinearFilter, Matrix4, Vector2, Vector3, Vector4, Box2 } from 'three';
|
|
6
|
+
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
|
+
import { createStorageTexture } from '../Processor/StorageTexturePool.js';
|
|
8
|
+
import { sanitizeRGB, sanitize1, FP16_MAX } from '../TSL/Common.js';
|
|
9
|
+
import { cameraRayDirection } from '../TSL/CameraRay.js';
|
|
10
|
+
import {
|
|
11
|
+
ALBEDO_EPS, GBUFFER_MISS_THRESHOLD as MISS_THRESHOLD, NRD_DEFAULTS,
|
|
12
|
+
NRD_HIT_DIST_A, NRD_HIT_DIST_B,
|
|
13
|
+
} from '../EngineDefaults.js';
|
|
14
|
+
|
|
15
|
+
const NRD_EPS = 1e-6;
|
|
16
|
+
const NORMAL_ENCODING_ERROR = 1.5 / 255.0;
|
|
17
|
+
const ROUGHNESS_SENSITIVITY = 0.01;
|
|
18
|
+
const EXP_WEIGHT_SCALE = 3.0;
|
|
19
|
+
const HISTORY_FIX_RADIUS = 2;
|
|
20
|
+
const FAST_CLAMP_RADIUS = 2;
|
|
21
|
+
const ANTI_FIREFLY_RADIUS = 3; // NRD's performance-mode value; its default is 4
|
|
22
|
+
|
|
23
|
+
const ANTI_FIREFLY_SIGMA_SCALE = 2.0;
|
|
24
|
+
const FIREFLY_MAX_RELATIVE_INTENSITY = 38.0;
|
|
25
|
+
const FIREFLY_RADIUS_SCALE = 0.1;
|
|
26
|
+
const FIREFLY_FAST_RELATIVE_INTENSITY = 4.0;
|
|
27
|
+
const ALMOST_ZERO_ANGLE_COS = Math.cos( 89.0 * Math.PI / 180.0 );
|
|
28
|
+
const CATROM_SHARPNESS = 0.5;
|
|
29
|
+
const WG_SIZE = 16;
|
|
30
|
+
|
|
31
|
+
const PASS = {
|
|
32
|
+
pre: { radiusScale: 1.0, fractionScale: 2.0 },
|
|
33
|
+
blur: { radiusScale: 1.0, fractionScale: 1.0 },
|
|
34
|
+
post: { radiusScale: 2.0, fractionScale: 0.5 },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// NRD g_Special8; .z scales the Gaussian weight.
|
|
38
|
+
const Q = 0.25 * Math.SQRT2;
|
|
39
|
+
const SPECIAL8 = [
|
|
40
|
+
[ - 1.0, 0.0, 1.0 ], [ 0.0, 1.0, 1.0 ], [ 1.0, 0.0, 1.0 ], [ 0.0, - 1.0, 1.0 ],
|
|
41
|
+
[ - Q, Q, 0.5 ], [ Q, Q, 0.5 ], [ Q, - Q, 0.5 ], [ - Q, - Q, 0.5 ],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// NRD ComputeNonExponentialWeight / ComputeExponentialWeight.
|
|
45
|
+
const weightSmooth = ( x, px, py ) => smoothstep( 0.0, 1.0, float( 1.0 ).sub( x.mul( px ).add( py ).abs() ) );
|
|
46
|
+
const weightExp = ( x, px, py ) => {
|
|
47
|
+
|
|
48
|
+
const t = x.mul( px ).add( py ).abs().mul( - EXP_WEIGHT_SCALE );
|
|
49
|
+
return float( 1.0 ).div( t.mul( t ).sub( t ).add( 1.0 ) );
|
|
50
|
+
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const acosApproxPositive = ( x ) =>
|
|
54
|
+
mix( float( 1.570796 ), float( 1.395402 ), saturate( x ) ).mul( sqrt( saturate( float( 1.0 ).sub( x ) ) ) );
|
|
55
|
+
const specMagicCurve = ( r, power ) =>
|
|
56
|
+
float( 1.0 ).sub( exp2( r.mul( r ).mul( - 200.0 ) ) ).mul( pow( saturate( r ), power ) );
|
|
57
|
+
const lobeTanHalfAngle = ( r, percentOfVolume ) =>
|
|
58
|
+
saturate( r ).mul( sqrt( percentOfVolume.div( float( 1.0 ).sub( percentOfVolume ).add( NRD_EPS ) ) ) );
|
|
59
|
+
const normalWeightParam = ( nonLinearAccumSpeed, lobeAngleFraction, roughness, maxPercentOfLobeVolume ) => {
|
|
60
|
+
|
|
61
|
+
const pov = maxPercentOfLobeVolume.mul( mix( saturate( lobeAngleFraction ), float( 1.0 ), nonLinearAccumSpeed ) );
|
|
62
|
+
const angle = max( atan( lobeTanHalfAngle( roughness, pov ) ), float( NORMAL_ENCODING_ERROR ) );
|
|
63
|
+
return float( 1.0 ).div( angle );
|
|
64
|
+
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const roughnessWeightParams = ( roughness, fraction ) => {
|
|
68
|
+
|
|
69
|
+
const a = float( 1.0 ).div( mix( float( ROUGHNESS_SENSITIVITY ), float( 1.0 ), saturate( roughness.mul( fraction ) ) ) );
|
|
70
|
+
return vec2( a, roughness.mul( a ).negate() );
|
|
71
|
+
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const relaxedRoughnessWeightParams = ( m, fraction, sensitivity ) => {
|
|
75
|
+
|
|
76
|
+
const a = float( 1.0 ).div( mix( float( sensitivity ), float( 1.0 ), mix( m.mul( m ), m, saturate( fraction ) ) ) );
|
|
77
|
+
return vec2( a, m.mul( a ).negate() );
|
|
78
|
+
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const hitDistWeightParams = ( normHitDist, nonLinearAccumSpeed ) => {
|
|
82
|
+
|
|
83
|
+
const a = float( 1.0 ).div( nonLinearAccumSpeed );
|
|
84
|
+
return vec2( a, normHitDist.mul( a ).negate() );
|
|
85
|
+
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const geometryWeightParams = ( sensitivity, frustumSize, X, N ) => {
|
|
89
|
+
|
|
90
|
+
const a = float( 1.0 ).div( sensitivity.mul( frustumSize ) );
|
|
91
|
+
return vec2( a, dot( N, X ).mul( a ).negate() );
|
|
92
|
+
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const mirrorUv = ( uv ) =>
|
|
96
|
+
vec2( 1.0 ).sub( vec2( 1.0 ).sub( fract( uv.mul( 0.5 ) ).mul( 2.0 ) ).abs() ).min( 0.99999 );
|
|
97
|
+
const linearToYCoCg = ( c ) => vec3(
|
|
98
|
+
dot( c, vec3( 0.25, 0.5, 0.25 ) ),
|
|
99
|
+
dot( c, vec3( 0.5, 0.0, - 0.5 ) ),
|
|
100
|
+
dot( c, vec3( - 0.25, 0.5, - 0.25 ) )
|
|
101
|
+
);
|
|
102
|
+
const yCoCgToLinear = ( c ) => {
|
|
103
|
+
|
|
104
|
+
const t = c.x.sub( c.z );
|
|
105
|
+
return max( vec3( t.add( c.y ), c.x.add( c.z ), t.sub( c.y ) ), vec3( 0.0 ) );
|
|
106
|
+
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const lumaScale = ( current, next ) => next.add( NRD_EPS ).div( current.add( NRD_EPS ) );
|
|
110
|
+
const changeLuma = ( v, newLuma ) => vec4( v.xyz.mul( lumaScale( v.x, newLuma ) ), v.w );
|
|
111
|
+
const clampNegativeToZero = ( v ) => vec4( linearToYCoCg( yCoCgToLinear( v.xyz ) ), saturate( v.w ) );
|
|
112
|
+
const stdDev = ( m1, m2 ) => sqrt( m2.sub( m1.mul( m1 ) ).abs() );
|
|
113
|
+
// rotator = (cos, sin, -sin, cos)
|
|
114
|
+
const rotate2 = ( rot, v ) => vec2( rot.x.mul( v.x ).add( rot.y.mul( v.y ) ), rot.z.mul( v.x ).add( rot.w.mul( v.y ) ) );
|
|
115
|
+
const denanify = ( w, s ) => select( w.equal( 0.0 ), vec4( 0.0 ), s );
|
|
116
|
+
|
|
117
|
+
function hash01( n ) {
|
|
118
|
+
|
|
119
|
+
let x = Math.imul( n | 0, 0x9E3779B1 ) >>> 0;
|
|
120
|
+
x ^= x >>> 15; x = Math.imul( x, 0x85EBCA6B ) >>> 0;
|
|
121
|
+
x ^= x >>> 13; x = Math.imul( x, 0xC2B2AE35 ) >>> 0;
|
|
122
|
+
x ^= x >>> 16;
|
|
123
|
+
return x / 4294967296;
|
|
124
|
+
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* NRD — port of NVIDIA Real-Time Denoisers' ReBLUR to TSL compute. Six passes per frame, as in
|
|
129
|
+
* nrd::REBLUR_DIFFUSE: PrePass → TemporalAccumulation → HistoryFix → Blur → PostBlur →
|
|
130
|
+
* TemporalStabilization. Internal signal is NRD's own: YCoCg demodulated lighting + normalized
|
|
131
|
+
* hit distance. See docs/NRD_DENOISER.md for the deviations from NRD and the measured ratios.
|
|
132
|
+
*
|
|
133
|
+
* Reads: pathtracer:color, pathtracer:albedo (.w = normHitDist), pathtracer:normalDepth,
|
|
134
|
+
* pathtracer:shadingNormal (.w = roughness), motionVector:screenSpace
|
|
135
|
+
* Publishes: nrd:output
|
|
136
|
+
* Events: denoiser:reset
|
|
137
|
+
*/
|
|
138
|
+
export class NRD extends RenderStage {
|
|
139
|
+
|
|
140
|
+
constructor( renderer, options = {} ) {
|
|
141
|
+
|
|
142
|
+
super( 'NRD', {
|
|
143
|
+
...options,
|
|
144
|
+
executionMode: StageExecutionMode.PER_CYCLE
|
|
145
|
+
} );
|
|
146
|
+
|
|
147
|
+
this.renderer = renderer;
|
|
148
|
+
this.pathTracer = options.pathTracer || null;
|
|
149
|
+
this.settings = { ...NRD_DEFAULTS, ...( options.settings || {} ) };
|
|
150
|
+
|
|
151
|
+
const w = options.width || 1;
|
|
152
|
+
const h = options.height || 1;
|
|
153
|
+
|
|
154
|
+
this.resW = uniform( w );
|
|
155
|
+
this.resH = uniform( h );
|
|
156
|
+
this.minRectDimMulUnproject = uniform( 1.0 );
|
|
157
|
+
this.inputFrames = uniform( 0.0 );
|
|
158
|
+
this.resetU = uniform( 0.0 );
|
|
159
|
+
this.debugMode = uniform( 0, 'int' );
|
|
160
|
+
|
|
161
|
+
this.rotPre = uniform( new Vector4( 1, 0, 0, 1 ), 'vec4' );
|
|
162
|
+
this.rotBlur = uniform( new Vector4( 1, 0, 0, 1 ), 'vec4' );
|
|
163
|
+
this.rotPost = uniform( new Vector4( 1, 0, 0, 1 ), 'vec4' );
|
|
164
|
+
|
|
165
|
+
this.camWorld = uniform( new Matrix4(), 'mat4' );
|
|
166
|
+
this.camProjInv = uniform( new Matrix4(), 'mat4' );
|
|
167
|
+
this.camView = uniform( new Matrix4(), 'mat4' );
|
|
168
|
+
this.prevCamView = uniform( new Matrix4(), 'mat4' );
|
|
169
|
+
this.camPos = uniform( new Vector3(), 'vec3' );
|
|
170
|
+
this.prevCamPos = uniform( new Vector3(), 'vec3' );
|
|
171
|
+
this.cameraProjection = uniform( 0, 'int' );
|
|
172
|
+
this.panoLonRange = uniform( new Vector2(), 'vec2' );
|
|
173
|
+
this.panoLatRange = uniform( new Vector2(), 'vec2' );
|
|
174
|
+
this.panoLevelHorizon = uniform( 1, 'int' );
|
|
175
|
+
|
|
176
|
+
// Declared here, valued by _syncUniforms below — which is the only place a setting becomes a
|
|
177
|
+
// uniform, so there is no second copy of the defaults to keep in step.
|
|
178
|
+
this.u = {};
|
|
179
|
+
for ( const name of [
|
|
180
|
+
'maxAccum', 'maxFast', 'stabilizationStrength', 'historyFixFrameNum', 'historyFixStride',
|
|
181
|
+
'prepassBlurRadius', 'minBlurRadius', 'maxBlurRadius', 'lobeAngleFraction', 'roughnessFraction',
|
|
182
|
+
'planeDistSensitivity', 'minHitDistanceWeight', 'fastClampSigmaScale', 'fireflyMinRelativeScale',
|
|
183
|
+
'antiFirefly', 'disocclusionThreshold', 'lobeVolumePercent', 'handoverFrames',
|
|
184
|
+
] ) this.u[ name ] = uniform( 0.0 );
|
|
185
|
+
this.u.antilag = uniform( new Vector2(), 'vec2' );
|
|
186
|
+
this.u.convergence = uniform( new Vector3(), 'vec3' );
|
|
187
|
+
this._syncUniforms();
|
|
188
|
+
|
|
189
|
+
this._colorTexNode = new TextureNode();
|
|
190
|
+
this._albedoTexNode = new TextureNode();
|
|
191
|
+
this._ndTexNode = new TextureNode();
|
|
192
|
+
this._snTexNode = new TextureNode();
|
|
193
|
+
this._motionTexNode = new TextureNode();
|
|
194
|
+
|
|
195
|
+
// Each deferred StorageTexture read needs its OWN sampled placeholder: TSL shares texture bindings
|
|
196
|
+
// by uuid, so nodes sharing EmptyTexture collapse into one binding that reads whatever bound last.
|
|
197
|
+
this._placeholders = new Map();
|
|
198
|
+
const readNode = () => {
|
|
199
|
+
|
|
200
|
+
const placeholder = new DataTexture( new Float32Array( 4 ), 1, 1, RGBAFormat, FloatType );
|
|
201
|
+
placeholder.needsUpdate = true;
|
|
202
|
+
const node = new TextureNode( placeholder );
|
|
203
|
+
this._placeholders.set( node, placeholder );
|
|
204
|
+
return node;
|
|
205
|
+
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
this._nTmpA = readNode();
|
|
209
|
+
this._nTmpB = readNode();
|
|
210
|
+
this._nHist = readNode();
|
|
211
|
+
this._nFast = readNode();
|
|
212
|
+
this._nInternal = readNode();
|
|
213
|
+
this._nGeom = readNode();
|
|
214
|
+
this._nData = readNode();
|
|
215
|
+
this._nStabRead = readNode();
|
|
216
|
+
|
|
217
|
+
this._allocStorage();
|
|
218
|
+
|
|
219
|
+
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
220
|
+
this.outputTarget = new RenderTarget( w, h, {
|
|
221
|
+
type: HalfFloatType,
|
|
222
|
+
format: RGBAFormat,
|
|
223
|
+
minFilter: LinearFilter,
|
|
224
|
+
magFilter: LinearFilter,
|
|
225
|
+
depthBuffer: false,
|
|
226
|
+
stencilBuffer: false
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
this._compiled = false;
|
|
230
|
+
this._needsReset = true;
|
|
231
|
+
this._stabPing = 0;
|
|
232
|
+
this._camInitialized = false;
|
|
233
|
+
|
|
234
|
+
this._dispatchX = Math.ceil( w / WG_SIZE );
|
|
235
|
+
this._dispatchY = Math.ceil( h / WG_SIZE );
|
|
236
|
+
|
|
237
|
+
this._buildKernels();
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Storage ───────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
_allocStorage() {
|
|
244
|
+
|
|
245
|
+
const tex = () => createStorageTexture( HalfFloatType );
|
|
246
|
+
this._tmpA = tex();
|
|
247
|
+
this._tmpB = tex();
|
|
248
|
+
this._hist = tex(); // recurrent history (PostBlur output)
|
|
249
|
+
this._fastHist = tex(); // .x fast luma
|
|
250
|
+
this._internal = tex(); // .x accumSpeed for next frame
|
|
251
|
+
// .xyz prev normal, .w prev viewZ. Half-float: the plane test's tolerance is orders above its error.
|
|
252
|
+
this._geomPrev = tex();
|
|
253
|
+
this._stabA = tex(); // .x stabilized luma
|
|
254
|
+
this._stabB = tex();
|
|
255
|
+
this._dataT = tex(); // .x fast luma, .y accumSpeed, .z occlusion bits, .w catrom ok
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_disposeStorage() {
|
|
260
|
+
|
|
261
|
+
for ( const t of [ this._tmpA, this._tmpB, this._hist, this._fastHist, this._internal, this._geomPrev, this._stabA, this._stabB, this._dataT ] ) t?.dispose();
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_disposeKernels() {
|
|
266
|
+
|
|
267
|
+
for ( const k of this._kernels ?? [] ) k?.dispose();
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Settings ──────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
updateParameters( params ) {
|
|
274
|
+
|
|
275
|
+
if ( ! params ) return;
|
|
276
|
+
for ( const key of Object.keys( params ) ) {
|
|
277
|
+
|
|
278
|
+
if ( key === 'debugMode' ) this.debugMode.value = params.debugMode | 0;
|
|
279
|
+
else if ( key in NRD_DEFAULTS ) this.settings[ key ] = params[ key ];
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this._syncUniforms();
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Derivations follow nrd::InstanceImpl::AddSharedConstants_Reblur.
|
|
288
|
+
_syncUniforms() {
|
|
289
|
+
|
|
290
|
+
const s = this.settings;
|
|
291
|
+
const u = this.u;
|
|
292
|
+
const maxAccum = Math.min( s.maxAccumulatedFrameNum, 63 );
|
|
293
|
+
const maxStab = Math.min( s.maxStabilizedFrameNum, maxAccum );
|
|
294
|
+
const maxBlur = Math.max( s.maxBlurRadius, s.minBlurRadius );
|
|
295
|
+
|
|
296
|
+
u.maxAccum.value = maxAccum;
|
|
297
|
+
u.maxFast.value = s.maxFastAccumulatedFrameNum;
|
|
298
|
+
u.stabilizationStrength.value = maxStab / ( 1.0 + maxStab );
|
|
299
|
+
u.historyFixFrameNum.value = s.historyFixFrameNum;
|
|
300
|
+
u.historyFixStride.value = s.historyFixBasePixelStride;
|
|
301
|
+
u.prepassBlurRadius.value = s.prepassBlurRadius;
|
|
302
|
+
u.minBlurRadius.value = s.minBlurRadius;
|
|
303
|
+
u.maxBlurRadius.value = maxBlur;
|
|
304
|
+
u.lobeAngleFraction.value = s.lobeAngleFraction * s.lobeAngleFraction;
|
|
305
|
+
u.roughnessFraction.value = s.roughnessFraction;
|
|
306
|
+
u.planeDistSensitivity.value = s.planeDistanceSensitivity;
|
|
307
|
+
u.minHitDistanceWeight.value = s.minHitDistanceWeight;
|
|
308
|
+
const t = Math.min( 1, Math.max( 0, maxBlur / 2 ) );
|
|
309
|
+
u.fastClampSigmaScale.value = 3.0 + ( s.fastHistoryClampingSigmaScale - 3.0 ) * t;
|
|
310
|
+
u.fireflyMinRelativeScale.value = s.fireflySuppressorMinRelativeScale;
|
|
311
|
+
u.antiFirefly.value = s.enableAntiFirefly ? 1.0 : 0.0;
|
|
312
|
+
u.antilag.value.set( s.antilagLuminanceSigmaScale, s.antilagLuminanceSensitivity );
|
|
313
|
+
u.disocclusionThreshold.value = s.disocclusionThreshold;
|
|
314
|
+
u.convergence.value.set( s.convergenceS, s.convergenceB, s.convergenceP );
|
|
315
|
+
u.lobeVolumePercent.value = s.lobeVolumePercent;
|
|
316
|
+
u.handoverFrames.value = s.handoverFrames > 0 ? s.handoverFrames : 2 * maxAccum;
|
|
317
|
+
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Shared TSL pieces ─────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
// Same ray the depth was measured along — NormalDepth generates it with this helper too.
|
|
323
|
+
_worldPosAt( uv, dist, cwm, cpi ) {
|
|
324
|
+
|
|
325
|
+
const dir = cameraRayDirection(
|
|
326
|
+
uv, cwm, cpi,
|
|
327
|
+
this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
|
|
328
|
+
);
|
|
329
|
+
return vec3( cwm[ 3 ] ).add( dir.mul( dist ) );
|
|
330
|
+
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_pixelUv( gx, gy ) {
|
|
334
|
+
|
|
335
|
+
return vec2( float( gx ).add( 0.5 ).div( this.resW ), float( gy ).add( 0.5 ).div( this.resH ) );
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_clampCoord( x, y ) {
|
|
340
|
+
|
|
341
|
+
return ivec2(
|
|
342
|
+
x.clamp( int( 0 ), int( this.resW ).sub( 1 ) ),
|
|
343
|
+
y.clamp( int( 0 ), int( this.resH ).sub( 1 ) )
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_uvToCoord( uv ) {
|
|
349
|
+
|
|
350
|
+
return this._clampCoord( int( uv.x.mul( this.resW ) ), int( uv.y.mul( this.resH ) ) );
|
|
351
|
+
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// NRD GetAdvancedNonLinearAccumSpeed.
|
|
355
|
+
_advancedNonLinear( accumSpeed ) {
|
|
356
|
+
|
|
357
|
+
const c = this.u.convergence;
|
|
358
|
+
const f = saturate( accumSpeed.div( float( 1.0 ).add( this.u.maxAccum.mul( c.z ) ) ) );
|
|
359
|
+
const e = c.x.mul( mix( c.y, float( 1.0 ), f ) );
|
|
360
|
+
return float( 1.0 ).div( float( 1.0 ).add( e.mul( accumSpeed ) ) );
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 0 for a fresh input, 1 once it carries `handoverFrames` samples. Every history and spatial term is
|
|
365
|
+
// scaled by (1 − this) so a converged render is handed back untouched.
|
|
366
|
+
_handover( k ) {
|
|
367
|
+
|
|
368
|
+
return saturate( k.div( max( this.u.handoverFrames, 1.0 ) ) );
|
|
369
|
+
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_bilinear( uv ) {
|
|
373
|
+
|
|
374
|
+
const t = uv.mul( vec2( this.resW, this.resH ) ).sub( 0.5 );
|
|
375
|
+
const origin = floor( t );
|
|
376
|
+
const f = saturate( t.sub( origin ) );
|
|
377
|
+
const ox = int( origin.x );
|
|
378
|
+
const oy = int( origin.y );
|
|
379
|
+
const inScreen = ( x, y ) => select(
|
|
380
|
+
x.greaterThanEqual( int( 0 ) ).and( y.greaterThanEqual( int( 0 ) ) )
|
|
381
|
+
.and( x.lessThan( int( this.resW ) ) ).and( y.lessThan( int( this.resH ) ) ),
|
|
382
|
+
float( 1.0 ), float( 0.0 )
|
|
383
|
+
);
|
|
384
|
+
return {
|
|
385
|
+
f,
|
|
386
|
+
taps: [
|
|
387
|
+
this._clampCoord( ox, oy ),
|
|
388
|
+
this._clampCoord( ox.add( 1 ), oy ),
|
|
389
|
+
this._clampCoord( ox, oy.add( 1 ) ),
|
|
390
|
+
this._clampCoord( ox.add( 1 ), oy.add( 1 ) ),
|
|
391
|
+
],
|
|
392
|
+
inScreen: [
|
|
393
|
+
inScreen( ox, oy ), inScreen( ox.add( 1 ), oy ),
|
|
394
|
+
inScreen( ox, oy.add( 1 ) ), inScreen( ox.add( 1 ), oy.add( 1 ) ),
|
|
395
|
+
],
|
|
396
|
+
customWeights: ( occ ) => [
|
|
397
|
+
occ[ 0 ].mul( float( 1.0 ).sub( f.x ) ).mul( float( 1.0 ).sub( f.y ) ),
|
|
398
|
+
occ[ 1 ].mul( f.x ).mul( float( 1.0 ).sub( f.y ) ),
|
|
399
|
+
occ[ 2 ].mul( float( 1.0 ).sub( f.x ) ).mul( f.y ),
|
|
400
|
+
occ[ 3 ].mul( f.x ).mul( f.y ),
|
|
401
|
+
],
|
|
402
|
+
filter: ( s ) => mix( mix( s[ 0 ], s[ 1 ], f.x ), mix( s[ 2 ], s[ 3 ], f.x ), f.y ),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// NRD Filtering::ApplyBilinearCustomWeights.
|
|
408
|
+
_applyCustomWeights( samples, weights, zero ) {
|
|
409
|
+
|
|
410
|
+
const acc = samples[ 0 ].mul( weights[ 0 ] )
|
|
411
|
+
.add( samples[ 1 ].mul( weights[ 1 ] ) )
|
|
412
|
+
.add( samples[ 2 ].mul( weights[ 2 ] ) )
|
|
413
|
+
.add( samples[ 3 ].mul( weights[ 3 ] ) );
|
|
414
|
+
const sum = weights[ 0 ].add( weights[ 1 ] ).add( weights[ 2 ] ).add( weights[ 3 ] );
|
|
415
|
+
return select( sum.lessThan( 1e-4 ), zero, acc.div( max( sum, 1e-4 ) ) );
|
|
416
|
+
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// NRD BicubicFilterNoCornersWithFallbackToBilinearFilterWithCustomWeights.
|
|
420
|
+
_sampleHistory( read, uv, bil, weights, allowCatRom ) {
|
|
421
|
+
|
|
422
|
+
const out = vec4( 0.0 ).toVar();
|
|
423
|
+
|
|
424
|
+
If( allowCatRom, () => {
|
|
425
|
+
|
|
426
|
+
const samplePos = saturate( uv ).mul( vec2( this.resW, this.resH ) );
|
|
427
|
+
const centerPos = floor( samplePos.sub( 0.5 ) ).add( 0.5 );
|
|
428
|
+
const f = saturate( samplePos.sub( centerPos ) );
|
|
429
|
+
const S = CATROM_SHARPNESS;
|
|
430
|
+
const w0 = f.mul( f.mul( f.mul( - S ).add( 2.0 * S ) ).sub( S ) );
|
|
431
|
+
const w1 = f.mul( f.mul( f.mul( 2.0 - S ).sub( 3.0 - S ) ) ).add( 1.0 );
|
|
432
|
+
const w2 = f.mul( f.mul( f.mul( - ( 2.0 - S ) ).add( 3.0 - 2.0 * S ) ).add( S ) );
|
|
433
|
+
const w3 = f.mul( f.mul( f.mul( S ).sub( S ) ) );
|
|
434
|
+
const wx = [ w0.x, w1.x, w2.x, w3.x ];
|
|
435
|
+
const wy = [ w0.y, w1.y, w2.y, w3.y ];
|
|
436
|
+
const bx = int( centerPos.x ).sub( 1 );
|
|
437
|
+
const by = int( centerPos.y ).sub( 1 );
|
|
438
|
+
|
|
439
|
+
const acc = vec4( 0.0 ).toVar();
|
|
440
|
+
const sum = float( 0.0 ).toVar();
|
|
441
|
+
|
|
442
|
+
for ( let j = 0; j < 4; j ++ ) {
|
|
443
|
+
|
|
444
|
+
for ( let i = 0; i < 4; i ++ ) {
|
|
445
|
+
|
|
446
|
+
if ( ( i === 0 || i === 3 ) && ( j === 0 || j === 3 ) ) continue;
|
|
447
|
+
const w = wx[ i ].mul( wy[ j ] );
|
|
448
|
+
acc.addAssign( read( this._clampCoord( bx.add( i ), by.add( j ) ) ).mul( w ) );
|
|
449
|
+
sum.addAssign( w );
|
|
450
|
+
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
out.assign( select( sum.lessThan( 1e-4 ), vec4( 0.0 ), acc.div( max( sum, 1e-4 ) ) ) );
|
|
456
|
+
|
|
457
|
+
} ).Else( () => {
|
|
458
|
+
|
|
459
|
+
out.assign( this._applyCustomWeights( bil.taps.map( ( c ) => read( c ) ), weights, vec4( 0.0 ) ) );
|
|
460
|
+
|
|
461
|
+
} );
|
|
462
|
+
|
|
463
|
+
return out;
|
|
464
|
+
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── Kernels ───────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
_buildKernels() {
|
|
470
|
+
|
|
471
|
+
this._kPre = this._buildSpatial( 'pre' );
|
|
472
|
+
this._kTa = this._buildTemporalAccumulation();
|
|
473
|
+
this._kHf = this._buildHistoryFix();
|
|
474
|
+
this._kBlur = this._buildSpatial( 'blur' );
|
|
475
|
+
this._kPost = this._buildSpatial( 'post' );
|
|
476
|
+
this._kTsA = this._buildTemporalStabilization( this._stabA );
|
|
477
|
+
this._kTsB = this._buildTemporalStabilization( this._stabB );
|
|
478
|
+
this._kernels = [ this._kPre, this._kTa, this._kHf, this._kBlur, this._kPost, this._kTsA, this._kTsB ];
|
|
479
|
+
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_dispatchOf( fn ) {
|
|
483
|
+
|
|
484
|
+
return fn( this.camWorld, this.camProjInv ).compute(
|
|
485
|
+
[ this._dispatchX, this._dispatchY, 1 ],
|
|
486
|
+
[ WG_SIZE, WG_SIZE, 1 ]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// REBLUR_Common_SpatialFilter. The pre-pass also runs the front end: demodulate, YCoCg, normHitDist.
|
|
492
|
+
_buildSpatial( kind ) {
|
|
493
|
+
|
|
494
|
+
const isPre = kind === 'pre';
|
|
495
|
+
const { radiusScale, fractionScale } = PASS[ kind ];
|
|
496
|
+
const u = this.u;
|
|
497
|
+
const colorTex = this._colorTexNode;
|
|
498
|
+
const albedoTex = this._albedoTexNode;
|
|
499
|
+
const ndTex = this._ndTexNode;
|
|
500
|
+
const snTex = this._snTexNode;
|
|
501
|
+
const dataTex = this._nData;
|
|
502
|
+
const inputTex = kind === 'blur' ? this._nTmpA : this._nTmpB;
|
|
503
|
+
const outputTex = kind === 'blur' ? this._tmpB : isPre ? this._tmpA : this._hist;
|
|
504
|
+
const geomOut = this._geomPrev;
|
|
505
|
+
const rotator = isPre ? this.rotPre : kind === 'blur' ? this.rotBlur : this.rotPost;
|
|
506
|
+
const maxRadiusU = isPre ? u.prepassBlurRadius : u.maxBlurRadius;
|
|
507
|
+
const camView = this.camView;
|
|
508
|
+
const camPos = this.camPos;
|
|
509
|
+
const resW = this.resW;
|
|
510
|
+
const resH = this.resH;
|
|
511
|
+
const inputFrames = this.inputFrames;
|
|
512
|
+
|
|
513
|
+
const readSignal = isPre
|
|
514
|
+
? ( c ) => {
|
|
515
|
+
|
|
516
|
+
const col = textureLoad( colorTex, c );
|
|
517
|
+
const alb = textureLoad( albedoTex, c );
|
|
518
|
+
const lighting = sanitizeRGB( col.xyz.div( max( alb.xyz, vec3( ALBEDO_EPS ) ) ) );
|
|
519
|
+
return vec4( linearToYCoCg( lighting ), sanitize1( alb.w, FP16_MAX ).min( 1.0 ) );
|
|
520
|
+
|
|
521
|
+
}
|
|
522
|
+
: ( c ) => textureLoad( inputTex, c );
|
|
523
|
+
|
|
524
|
+
const computeFn = Fn( ( [ cwm, cpi ] ) => {
|
|
525
|
+
|
|
526
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
527
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
528
|
+
|
|
529
|
+
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
530
|
+
|
|
531
|
+
const coord = ivec2( gx, gy );
|
|
532
|
+
const uvec = uvec2( uint( gx ), uint( gy ) );
|
|
533
|
+
const dist = textureLoad( ndTex, coord ).w.toVar();
|
|
534
|
+
const isSky = dist.greaterThanEqual( MISS_THRESHOLD );
|
|
535
|
+
const sn = textureLoad( snTex, coord ).toVar();
|
|
536
|
+
const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
|
|
537
|
+
const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
|
|
538
|
+
const viewZ = float( MISS_THRESHOLD ).toVar();
|
|
539
|
+
|
|
540
|
+
const result = readSignal( coord ).toVar();
|
|
541
|
+
|
|
542
|
+
If( isSky.not(), () => {
|
|
543
|
+
|
|
544
|
+
const pixelUv = this._pixelUv( gx, gy );
|
|
545
|
+
const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
|
|
546
|
+
viewZ.assign( camView.mul( vec4( X, 1.0 ) ).z.abs() );
|
|
547
|
+
|
|
548
|
+
If( maxRadiusU.greaterThan( 0.0 ), () => {
|
|
549
|
+
|
|
550
|
+
const V = normalize( camPos.sub( X ) );
|
|
551
|
+
const NoV = dot( N, V ).abs().toVar();
|
|
552
|
+
const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
|
|
553
|
+
|
|
554
|
+
const k = inputFrames.add( 1.0 );
|
|
555
|
+
const accumSpeedEff = isPre
|
|
556
|
+
? max( inputFrames, float( 10.0 ) )
|
|
557
|
+
: textureLoad( dataTex, coord ).y.add( inputFrames );
|
|
558
|
+
const nonLinear = ( isPre
|
|
559
|
+
? float( 1.0 ).div( accumSpeedEff.add( 1.0 ) )
|
|
560
|
+
: this._advancedNonLinear( accumSpeedEff ) ).toVar();
|
|
561
|
+
|
|
562
|
+
const smc = specMagicCurve( roughness, 0.5 ).toVar();
|
|
563
|
+
const hitDistScale = viewZ.mul( NRD_HIT_DIST_B ).add( NRD_HIT_DIST_A );
|
|
564
|
+
const centerH = result.w.toVar();
|
|
565
|
+
const hitDist = centerH.mul( hitDistScale );
|
|
566
|
+
const hitDistFactor = saturate( hitDist.div( frustumSize ) );
|
|
567
|
+
// NRD sizes the pre-pass for 1 spp; k samples carry 1/k of that variance.
|
|
568
|
+
const fade = float( 1.0 ).sub( this._handover( k ) );
|
|
569
|
+
const areaFactor = ( isPre ? hitDistFactor.div( k ) : hitDistFactor.mul( nonLinear ) ).mul( fade );
|
|
570
|
+
|
|
571
|
+
// NRD's 1px floor fights 1-spp boiling, which an accumulated input does not have.
|
|
572
|
+
const minRadius = u.minBlurRadius.mul( saturate( float( 2.0 ).sub( accumSpeedEff.div( max( u.maxAccum, 1.0 ) ) ) ) );
|
|
573
|
+
const blurRadius = max(
|
|
574
|
+
saturate( sqrt( saturate( areaFactor ) ).mul( radiusScale ) ).mul( maxRadiusU ).mul( smc ),
|
|
575
|
+
minRadius.mul( smc )
|
|
576
|
+
).toVar();
|
|
577
|
+
|
|
578
|
+
const geomParams = geometryWeightParams( u.planeDistSensitivity, frustumSize, X, N ).toVar();
|
|
579
|
+
const normalParam = normalWeightParam( nonLinear, u.lobeAngleFraction, roughness, u.lobeVolumePercent ).div( fractionScale ).toVar();
|
|
580
|
+
const roughParams = roughnessWeightParams( roughness, u.roughnessFraction.mul( fractionScale ) ).toVar();
|
|
581
|
+
const hdParams = hitDistWeightParams( centerH, nonLinear ).toVar();
|
|
582
|
+
let minHitDistWeight = u.minHitDistanceWeight.mul( fractionScale ).mul( smc );
|
|
583
|
+
if ( ! isPre ) minHitDistWeight = minHitDistWeight.mul( nonLinear );
|
|
584
|
+
minHitDistWeight = minHitDistWeight.toVar();
|
|
585
|
+
|
|
586
|
+
let skew = vec2( 1.0 );
|
|
587
|
+
if ( ! isPre ) {
|
|
588
|
+
|
|
589
|
+
const Nv = camView.mul( vec4( N, 0.0 ) ).xy;
|
|
590
|
+
skew = mix( vec2( 1.0 ).sub( Nv.abs() ), vec2( 1.0 ), NoV );
|
|
591
|
+
skew = skew.div( max( skew.x, skew.y ) );
|
|
592
|
+
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const skewPx = skew.mul( vec2( float( 1.0 ).div( resW ), float( 1.0 ).div( resH ) ) ).mul( blurRadius ).toVar();
|
|
596
|
+
|
|
597
|
+
const sum = float( 1.0 ).toVar();
|
|
598
|
+
|
|
599
|
+
for ( let n = 0; n < SPECIAL8.length; n ++ ) {
|
|
600
|
+
|
|
601
|
+
const off = SPECIAL8[ n ];
|
|
602
|
+
const uvS = pixelUv.add( rotate2( rotator, vec2( off[ 0 ], off[ 1 ] ) ).mul( skewPx ) );
|
|
603
|
+
const muv = mirrorUv( uvS );
|
|
604
|
+
const mirrored = muv.x.notEqual( uvS.x ).or( muv.y.notEqual( uvS.y ) );
|
|
605
|
+
const w = select( mirrored, float( 1.0 ), float( Math.exp( - 0.66 * off[ 2 ] * off[ 2 ] ) ) ).toVar();
|
|
606
|
+
|
|
607
|
+
const pos = this._uvToCoord( muv );
|
|
608
|
+
const zs = textureLoad( ndTex, pos ).w;
|
|
609
|
+
const Xs = this._worldPosAt( muv, zs, cwm, cpi );
|
|
610
|
+
const sns = textureLoad( snTex, pos );
|
|
611
|
+
const Ns = normalize( sns.xyz.mul( 2.0 ).sub( 1.0 ) );
|
|
612
|
+
|
|
613
|
+
const angle = acosApproxPositive( dot( N, Ns ) );
|
|
614
|
+
w.mulAssign( weightSmooth( angle, normalParam, float( 0.0 ) ) );
|
|
615
|
+
w.mulAssign( weightSmooth( sns.w, roughParams.x, roughParams.y ) );
|
|
616
|
+
w.mulAssign( weightSmooth( dot( N, Xs ), geomParams.x, geomParams.y ) );
|
|
617
|
+
w.assign( select( zs.greaterThanEqual( MISS_THRESHOLD ), float( 0.0 ), w ) );
|
|
618
|
+
|
|
619
|
+
const s = denanify( w, readSignal( pos ) );
|
|
620
|
+
w.mulAssign( minHitDistWeight.add( weightExp( s.w, hdParams.x, hdParams.y ) ) );
|
|
621
|
+
|
|
622
|
+
sum.addAssign( w );
|
|
623
|
+
result.addAssign( s.mul( w ) );
|
|
624
|
+
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
result.divAssign( max( sum, 1e-6 ) );
|
|
628
|
+
// NRD keeps hit distance unfiltered past the pre-pass (self-inference).
|
|
629
|
+
if ( ! isPre ) result.assign( vec4( result.xyz, centerH ) );
|
|
630
|
+
|
|
631
|
+
} );
|
|
632
|
+
|
|
633
|
+
} );
|
|
634
|
+
|
|
635
|
+
textureStore( outputTex, uvec, result ).toWriteOnly();
|
|
636
|
+
|
|
637
|
+
if ( kind === 'post' ) {
|
|
638
|
+
|
|
639
|
+
textureStore( geomOut, uvec, vec4( N, viewZ ) ).toWriteOnly();
|
|
640
|
+
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
} );
|
|
644
|
+
|
|
645
|
+
} );
|
|
646
|
+
|
|
647
|
+
return this._dispatchOf( computeFn );
|
|
648
|
+
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// REBLUR_TemporalAccumulation, diffuse branch (no specular virtual motion).
|
|
652
|
+
_buildTemporalAccumulation() {
|
|
653
|
+
|
|
654
|
+
const u = this.u;
|
|
655
|
+
const ndTex = this._ndTexNode;
|
|
656
|
+
const snTex = this._snTexNode;
|
|
657
|
+
const motionTex = this._motionTexNode;
|
|
658
|
+
const preTex = this._nTmpA;
|
|
659
|
+
const histTex = this._nHist;
|
|
660
|
+
const fastTex = this._nFast;
|
|
661
|
+
const internalTex = this._nInternal;
|
|
662
|
+
const geomTex = this._nGeom;
|
|
663
|
+
const outTex = this._tmpB;
|
|
664
|
+
const dataOut = this._dataT;
|
|
665
|
+
const camView = this.camView;
|
|
666
|
+
const prevCamView = this.prevCamView;
|
|
667
|
+
const camPos = this.camPos;
|
|
668
|
+
const prevCamPos = this.prevCamPos;
|
|
669
|
+
const resW = this.resW;
|
|
670
|
+
const resH = this.resH;
|
|
671
|
+
const inputFrames = this.inputFrames;
|
|
672
|
+
const resetU = this.resetU;
|
|
673
|
+
|
|
674
|
+
const computeFn = Fn( ( [ cwm, cpi ] ) => {
|
|
675
|
+
|
|
676
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
677
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
678
|
+
|
|
679
|
+
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
680
|
+
|
|
681
|
+
const coord = ivec2( gx, gy );
|
|
682
|
+
const uvec = uvec2( uint( gx ), uint( gy ) );
|
|
683
|
+
const dist = textureLoad( ndTex, coord ).w;
|
|
684
|
+
const current = textureLoad( preTex, coord ).toVar();
|
|
685
|
+
|
|
686
|
+
If( dist.greaterThanEqual( MISS_THRESHOLD ), () => {
|
|
687
|
+
|
|
688
|
+
textureStore( outTex, uvec, current ).toWriteOnly();
|
|
689
|
+
textureStore( dataOut, uvec, vec4( 0.0 ) ).toWriteOnly();
|
|
690
|
+
Return();
|
|
691
|
+
|
|
692
|
+
} );
|
|
693
|
+
|
|
694
|
+
const maxAccumEff = u.maxAccum.mul( float( 1.0 ).sub( resetU ) ).toVar();
|
|
695
|
+
const maxFastEff = u.maxFast.mul( float( 1.0 ).sub( resetU ) ).toVar();
|
|
696
|
+
|
|
697
|
+
const sn = textureLoad( snTex, coord );
|
|
698
|
+
const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
|
|
699
|
+
const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
|
|
700
|
+
|
|
701
|
+
const pixelUv = this._pixelUv( gx, gy );
|
|
702
|
+
const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
|
|
703
|
+
const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs().toVar();
|
|
704
|
+
const V = normalize( camPos.sub( X ) );
|
|
705
|
+
const NoV = dot( N, V ).abs().toVar();
|
|
706
|
+
const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
|
|
707
|
+
|
|
708
|
+
// Unnormalized, as NRD requires — its length carries the normal variance. A miss neighbour's
|
|
709
|
+
// shadingNormal is a literal (0,0,0), which decodes to a fixed fake direction, so substitute
|
|
710
|
+
// the centre normal instead of letting it skew the reprojection test along every silhouette.
|
|
711
|
+
const Navg = vec3( 0.0 ).toVar();
|
|
712
|
+
Navg.addAssign( N.mul( 0.25 ) ); // the centre; the sky early-out above proves it is a hit
|
|
713
|
+
for ( let j = - 1; j <= 0; j ++ ) {
|
|
714
|
+
|
|
715
|
+
for ( let i = - 1; i <= 0; i ++ ) {
|
|
716
|
+
|
|
717
|
+
if ( i === 0 && j === 0 ) continue;
|
|
718
|
+
const at = this._clampCoord( gx.add( i ), gy.add( j ) );
|
|
719
|
+
const n = normalize( textureLoad( snTex, at ).xyz.mul( 2.0 ).sub( 1.0 ) );
|
|
720
|
+
const isHit = textureLoad( ndTex, at ).w.lessThan( MISS_THRESHOLD );
|
|
721
|
+
Navg.addAssign( select( isHit, n, N ).mul( 0.25 ) );
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// MotionVector publishes (current − previous) uv; static geometry ⇒ Xprev = X.
|
|
728
|
+
const mv = textureLoad( motionTex, coord );
|
|
729
|
+
const smbUv = pixelUv.sub( mv.xy ).toVar();
|
|
730
|
+
const prevViewZc = prevCamView.mul( vec4( X, 1.0 ) ).z.abs().toVar();
|
|
731
|
+
|
|
732
|
+
const bil = this._bilinear( smbUv );
|
|
733
|
+
const threshold = frustumSize.mul( saturate( u.disocclusionThreshold.div( max( NoV, 0.05 ) ) ) ).sub( NRD_EPS ).toVar();
|
|
734
|
+
const noReset = float( 1.0 ).sub( resetU );
|
|
735
|
+
|
|
736
|
+
const occ = [];
|
|
737
|
+
const prevAccum = [];
|
|
738
|
+
for ( let k = 0; k < 4; k ++ ) {
|
|
739
|
+
|
|
740
|
+
const g = textureLoad( geomTex, bil.taps[ k ] );
|
|
741
|
+
const normalOk = select( dot( g.xyz, Navg ).greaterThan( ALMOST_ZERO_ANGLE_COS ), float( 1.0 ), float( 0.0 ) );
|
|
742
|
+
const planeOk = select( g.w.sub( prevViewZc ).abs().lessThanEqual( threshold ), float( 1.0 ), float( 0.0 ) );
|
|
743
|
+
const inRange = select( g.w.lessThan( MISS_THRESHOLD ), float( 1.0 ), float( 0.0 ) );
|
|
744
|
+
occ.push( normalOk.mul( planeOk ).mul( inRange ).mul( bil.inScreen[ k ] ).mul( noReset ).toVar() );
|
|
745
|
+
prevAccum.push( textureLoad( internalTex, bil.taps[ k ] ).x );
|
|
746
|
+
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const occW = bil.customWeights( occ ).map( ( w ) => w.toVar() );
|
|
750
|
+
const occSum = occ[ 0 ].add( occ[ 1 ] ).add( occ[ 2 ] ).add( occ[ 3 ] );
|
|
751
|
+
const allowCatRom = occSum.greaterThan( 3.5 ).toVar();
|
|
752
|
+
const fbits = occ[ 0 ].add( occ[ 1 ].mul( 2.0 ) ).add( occ[ 2 ].mul( 4.0 ) ).add( occ[ 3 ].mul( 8.0 ) );
|
|
753
|
+
|
|
754
|
+
const accumSpeed = this._applyCustomWeights( prevAccum, occW, float( 0.0 ) ).toVar();
|
|
755
|
+
|
|
756
|
+
const Vprev = normalize( prevCamPos.sub( X ) );
|
|
757
|
+
const NoVprev = dot( N, Vprev ).abs();
|
|
758
|
+
const sizeQuality = NoVprev.add( 1e-3 ).div( NoV.add( 1e-3 ) ).toVar();
|
|
759
|
+
sizeQuality.assign( mix( float( 0.1 ), float( 1.0 ), saturate( sizeQuality.mul( sizeQuality ) ) ) );
|
|
760
|
+
const footprint = sqrt( saturate( bil.filter( occ ) ) ).mul( sizeQuality ).toVar();
|
|
761
|
+
accumSpeed.assign( accumSpeed.mul( mix( footprint, float( 1.0 ), float( 1.0 ).div( accumSpeed.add( 1.0 ) ) ) ) );
|
|
762
|
+
accumSpeed.assign( min( accumSpeed, maxAccumEff ) );
|
|
763
|
+
|
|
764
|
+
const history = clampNegativeToZero(
|
|
765
|
+
this._sampleHistory( ( c ) => textureLoad( histTex, c ), smbUv, bil, occW, allowCatRom )
|
|
766
|
+
).toVar();
|
|
767
|
+
const fastHistory = max( this._applyCustomWeights( bil.taps.map( ( c ) => textureLoad( fastTex, c ).x ), occW, float( 0.0 ) ), 0.0 ).toVar();
|
|
768
|
+
|
|
769
|
+
// Inverse-variance blend: k samples in the input against accumSpeed frames of history.
|
|
770
|
+
const k = inputFrames.add( 1.0 );
|
|
771
|
+
const handover = this._handover( k ).toVar();
|
|
772
|
+
const historyFrames = accumSpeed.mul( float( 1.0 ).sub( handover ) );
|
|
773
|
+
const nonLinear = k.div( k.add( historyFrames ) ).toVar();
|
|
774
|
+
const minHitDistNonLinear = float( 1.0 ).div( float( 1.0 ).add( specMagicCurve( roughness, 0.25 ).mul( 0.5 ).mul( maxAccumEff ) ) );
|
|
775
|
+
const result = vec4(
|
|
776
|
+
mix( history.xyz, current.xyz, nonLinear ),
|
|
777
|
+
mix( history.w, current.w, max( nonLinear, minHitDistNonLinear ) )
|
|
778
|
+
).toVar();
|
|
779
|
+
|
|
780
|
+
const maxRel = u.fireflyMinRelativeScale.add( float( FIREFLY_MAX_RELATIVE_INTENSITY ).div( accumSpeed.add( 1.0 ) ) ).toVar();
|
|
781
|
+
const aff = accumSpeed.mul( u.maxBlurRadius ).mul( FIREFLY_RADIUS_SCALE ).toVar();
|
|
782
|
+
aff.assign( aff.div( aff.add( 1.0 ) ).mul( float( 1.0 ).sub( handover ) ) );
|
|
783
|
+
const lumaClamped = mix( result.x, min( result.x, history.x.mul( maxRel ) ), aff );
|
|
784
|
+
result.assign( changeLuma( result, lumaClamped ) );
|
|
785
|
+
const hdMaxRel = float( 1.2 ).add( float( 1.0 ).div( accumSpeed.add( 1.0 ) ) );
|
|
786
|
+
result.assign( vec4( result.xyz, mix( result.w, min( result.w, history.w.mul( hdMaxRel ) ), aff ) ) );
|
|
787
|
+
|
|
788
|
+
textureStore( outTex, uvec, result ).toWriteOnly();
|
|
789
|
+
|
|
790
|
+
const fastAccum = min( accumSpeed, maxFastEff ).mul( float( 1.0 ).sub( handover ) );
|
|
791
|
+
const fastNonLinear = k.div( k.add( fastAccum ) );
|
|
792
|
+
const fast = mix( fastHistory, current.x, fastNonLinear ).toVar();
|
|
793
|
+
const fastClamped = min( fast, history.x.mul( maxRel ).mul( FIREFLY_FAST_RELATIVE_INTENSITY ) );
|
|
794
|
+
fast.assign( mix( fast, fastClamped, aff ) );
|
|
795
|
+
|
|
796
|
+
textureStore( dataOut, uvec, vec4( fast, accumSpeed, fbits, select( allowCatRom, float( 1.0 ), float( 0.0 ) ) ) ).toWriteOnly();
|
|
797
|
+
|
|
798
|
+
} );
|
|
799
|
+
|
|
800
|
+
} );
|
|
801
|
+
|
|
802
|
+
return this._dispatchOf( computeFn );
|
|
803
|
+
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// REBLUR_HistoryFix.
|
|
807
|
+
_buildHistoryFix() {
|
|
808
|
+
|
|
809
|
+
const u = this.u;
|
|
810
|
+
const ndTex = this._ndTexNode;
|
|
811
|
+
const snTex = this._snTexNode;
|
|
812
|
+
const inTex = this._nTmpB;
|
|
813
|
+
const dataTex = this._nData;
|
|
814
|
+
const outTex = this._tmpA;
|
|
815
|
+
const fastOut = this._fastHist;
|
|
816
|
+
const camView = this.camView;
|
|
817
|
+
const resW = this.resW;
|
|
818
|
+
const resH = this.resH;
|
|
819
|
+
const inputFrames = this.inputFrames;
|
|
820
|
+
|
|
821
|
+
const computeFn = Fn( ( [ cwm, cpi ] ) => {
|
|
822
|
+
|
|
823
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
824
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
825
|
+
|
|
826
|
+
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
827
|
+
|
|
828
|
+
const coord = ivec2( gx, gy );
|
|
829
|
+
const uvec = uvec2( uint( gx ), uint( gy ) );
|
|
830
|
+
const dist = textureLoad( ndTex, coord ).w;
|
|
831
|
+
|
|
832
|
+
// Pass the sky's own signal through rather than zeroing it: it reaches `hist` unchanged
|
|
833
|
+
// (the spatial passes skip sky too), and the Catmull-Rom history tap has no per-tap gate,
|
|
834
|
+
// so a 0 here is a black sample for any foreground pixel whose 4×4 footprint touches it.
|
|
835
|
+
If( dist.greaterThanEqual( MISS_THRESHOLD ), () => {
|
|
836
|
+
|
|
837
|
+
const passThrough = textureLoad( inTex, coord );
|
|
838
|
+
textureStore( outTex, uvec, passThrough ).toWriteOnly();
|
|
839
|
+
textureStore( fastOut, uvec, vec4( passThrough.x, 0.0, 0.0, 0.0 ) ).toWriteOnly();
|
|
840
|
+
Return();
|
|
841
|
+
|
|
842
|
+
} );
|
|
843
|
+
|
|
844
|
+
const data = textureLoad( dataTex, coord ).toVar();
|
|
845
|
+
const frameNum = data.y.toVar();
|
|
846
|
+
const frameNumEff = frameNum.add( inputFrames );
|
|
847
|
+
// The luma clamps below guard history lag, which the handover retires.
|
|
848
|
+
const handover = this._handover( inputFrames.add( 1.0 ) ).toVar();
|
|
849
|
+
|
|
850
|
+
const sn = textureLoad( snTex, coord );
|
|
851
|
+
const N = normalize( sn.xyz.mul( 2.0 ).sub( 1.0 ) ).toVar();
|
|
852
|
+
const roughness = sn.w.clamp( 0.05, 1.0 ).toVar();
|
|
853
|
+
const pixelUv = this._pixelUv( gx, gy );
|
|
854
|
+
const X = this._worldPosAt( pixelUv, dist, cwm, cpi ).toVar();
|
|
855
|
+
const viewZ = camView.mul( vec4( X, 1.0 ) ).z.abs();
|
|
856
|
+
const frustumSize = this.minRectDimMulUnproject.mul( viewZ ).toVar();
|
|
857
|
+
|
|
858
|
+
const diff = textureLoad( inTex, coord ).toVar();
|
|
859
|
+
const nonLinear = float( 1.0 ).div( frameNumEff.add( 1.0 ) ).toVar();
|
|
860
|
+
|
|
861
|
+
const hitDistScale = viewZ.mul( NRD_HIT_DIST_B ).add( NRD_HIT_DIST_A );
|
|
862
|
+
const hitDistFactor = saturate( diff.w.mul( hitDistScale ).div( frustumSize ) );
|
|
863
|
+
const centerH = diff.w.toVar();
|
|
864
|
+
|
|
865
|
+
// Halved to match RELAX's frame count, then shrunk near contact (NRD).
|
|
866
|
+
const stride = select( frameNumEff.lessThan( u.historyFixFrameNum ), float( 1.0 ), float( 0.0 ) )
|
|
867
|
+
.mul( 0.5 ).mul( 2.0 / HISTORY_FIX_RADIUS ).mul( u.historyFixStride )
|
|
868
|
+
.mul( mix( float( 0.25 ).add( sqrt( hitDistFactor ).mul( 0.75 ) ), float( 1.0 ), nonLinear ) );
|
|
869
|
+
const strideR = round( stride ).toVar();
|
|
870
|
+
|
|
871
|
+
If( strideR.notEqual( 0.0 ), () => {
|
|
872
|
+
|
|
873
|
+
const normalParam = normalWeightParam( nonLinear, u.lobeAngleFraction, roughness, u.lobeVolumePercent ).toVar();
|
|
874
|
+
const geomParams = geometryWeightParams( u.planeDistSensitivity, frustumSize, X, N ).toVar();
|
|
875
|
+
const hdParams = hitDistWeightParams( centerH, nonLinear ).toVar();
|
|
876
|
+
const relaxedRough = relaxedRoughnessWeightParams( roughness.mul( roughness ), sqrt( u.roughnessFraction ), ROUGHNESS_SENSITIVITY ).toVar();
|
|
877
|
+
|
|
878
|
+
const sumd = float( 1.0 ).add( frameNum ).toVar();
|
|
879
|
+
diff.mulAssign( sumd );
|
|
880
|
+
|
|
881
|
+
for ( let j = - HISTORY_FIX_RADIUS; j <= HISTORY_FIX_RADIUS; j ++ ) {
|
|
882
|
+
|
|
883
|
+
for ( let i = - HISTORY_FIX_RADIUS; i <= HISTORY_FIX_RADIUS; i ++ ) {
|
|
884
|
+
|
|
885
|
+
if ( i === 0 && j === 0 ) continue;
|
|
886
|
+
if ( Math.abs( i ) + Math.abs( j ) === HISTORY_FIX_RADIUS * 2 ) continue;
|
|
887
|
+
|
|
888
|
+
const uvS = pixelUv.add( vec2( i, j ).mul( strideR ).mul( vec2( float( 1.0 ).div( resW ), float( 1.0 ).div( resH ) ) ) );
|
|
889
|
+
const muv = mirrorUv( uvS );
|
|
890
|
+
const pos = this._uvToCoord( muv );
|
|
891
|
+
|
|
892
|
+
const zs = textureLoad( ndTex, pos ).w;
|
|
893
|
+
const Xs = this._worldPosAt( muv, zs, cwm, cpi );
|
|
894
|
+
const sns = textureLoad( snTex, pos );
|
|
895
|
+
const Ns = normalize( sns.xyz.mul( 2.0 ).sub( 1.0 ) );
|
|
896
|
+
const angle = acosApproxPositive( dot( Ns, N ) );
|
|
897
|
+
|
|
898
|
+
const w = weightExp( angle, normalParam, float( 0.0 ) ).toVar();
|
|
899
|
+
w.mulAssign( weightExp( sns.w.mul( sns.w ), relaxedRough.x, relaxedRough.y ) );
|
|
900
|
+
w.mulAssign( float( 1.0 ).add( textureLoad( dataTex, pos ).y ) );
|
|
901
|
+
w.mulAssign( weightSmooth( dot( N, Xs ), geomParams.x, geomParams.y ) );
|
|
902
|
+
w.assign( select( zs.greaterThanEqual( MISS_THRESHOLD ), float( 0.0 ), w ) );
|
|
903
|
+
|
|
904
|
+
const s = denanify( w, textureLoad( inTex, pos ) );
|
|
905
|
+
w.mulAssign( weightExp( s.w, hdParams.x, hdParams.y ) );
|
|
906
|
+
|
|
907
|
+
sumd.addAssign( w );
|
|
908
|
+
diff.addAssign( s.mul( w ) );
|
|
909
|
+
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
diff.divAssign( max( sumd, 1e-6 ) );
|
|
915
|
+
|
|
916
|
+
} );
|
|
917
|
+
|
|
918
|
+
const diffLuma = diff.x.toVar();
|
|
919
|
+
|
|
920
|
+
const f = saturate( frameNum.div( max( u.historyFixFrameNum, NRD_EPS ) ) );
|
|
921
|
+
const fastCenter = mix( diffLuma, data.x, f ).toVar();
|
|
922
|
+
textureStore( fastOut, uvec, vec4( fastCenter, 0.0, 0.0, 0.0 ) ).toWriteOnly();
|
|
923
|
+
|
|
924
|
+
// 5×5 moments for the clamp, the outer ring only for anti-firefly.
|
|
925
|
+
const fastM1 = fastCenter.toVar();
|
|
926
|
+
const fastM2 = fastCenter.mul( fastCenter ).toVar();
|
|
927
|
+
const afM1 = float( 0.0 ).toVar();
|
|
928
|
+
const afM2 = float( 0.0 ).toVar();
|
|
929
|
+
|
|
930
|
+
const ringTap = ( i, j, onAF ) => {
|
|
931
|
+
|
|
932
|
+
const pos = this._clampCoord( gx.add( i ), gy.add( j ) );
|
|
933
|
+
const zs = textureLoad( ndTex, pos ).w;
|
|
934
|
+
const d = select( zs.greaterThanEqual( MISS_THRESHOLD ), fastCenter, textureLoad( dataTex, pos ).x );
|
|
935
|
+
|
|
936
|
+
if ( Math.abs( i ) <= FAST_CLAMP_RADIUS && Math.abs( j ) <= FAST_CLAMP_RADIUS ) {
|
|
937
|
+
|
|
938
|
+
fastM1.addAssign( d );
|
|
939
|
+
fastM2.addAssign( d.mul( d ) );
|
|
940
|
+
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if ( onAF ) {
|
|
944
|
+
|
|
945
|
+
afM1.addAssign( d );
|
|
946
|
+
afM2.addAssign( d.mul( d ) );
|
|
947
|
+
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const antiFireflyOn = u.antiFirefly.greaterThan( 0.5 );
|
|
953
|
+
|
|
954
|
+
for ( let j = - FAST_CLAMP_RADIUS; j <= FAST_CLAMP_RADIUS; j ++ ) {
|
|
955
|
+
|
|
956
|
+
for ( let i = - FAST_CLAMP_RADIUS; i <= FAST_CLAMP_RADIUS; i ++ ) {
|
|
957
|
+
|
|
958
|
+
if ( i === 0 && j === 0 ) continue;
|
|
959
|
+
ringTap( i, j, Math.abs( i ) > 1 || Math.abs( j ) > 1 );
|
|
960
|
+
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// The outer ring only feeds the anti-firefly moments — skip it when the gate is off.
|
|
966
|
+
If( antiFireflyOn, () => {
|
|
967
|
+
|
|
968
|
+
for ( let j = - ANTI_FIREFLY_RADIUS; j <= ANTI_FIREFLY_RADIUS; j ++ ) {
|
|
969
|
+
|
|
970
|
+
for ( let i = - ANTI_FIREFLY_RADIUS; i <= ANTI_FIREFLY_RADIUS; i ++ ) {
|
|
971
|
+
|
|
972
|
+
if ( Math.abs( i ) <= FAST_CLAMP_RADIUS && Math.abs( j ) <= FAST_CLAMP_RADIUS ) continue;
|
|
973
|
+
ringTap( i, j, true );
|
|
974
|
+
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
} );
|
|
980
|
+
|
|
981
|
+
If( antiFireflyOn, () => {
|
|
982
|
+
|
|
983
|
+
const invNorm = 1.0 / ( ( ANTI_FIREFLY_RADIUS * 2 + 1 ) ** 2 - 9 );
|
|
984
|
+
const m1 = afM1.mul( invNorm );
|
|
985
|
+
const m2 = afM2.mul( invNorm );
|
|
986
|
+
const sigma = stdDev( m1, m2 ).mul( ANTI_FIREFLY_SIGMA_SCALE );
|
|
987
|
+
diffLuma.assign( mix( diffLuma.clamp( m1.sub( sigma ), m1.add( sigma ) ), diffLuma, handover ) );
|
|
988
|
+
|
|
989
|
+
} );
|
|
990
|
+
|
|
991
|
+
{
|
|
992
|
+
|
|
993
|
+
const invNorm = 1.0 / ( ( FAST_CLAMP_RADIUS * 2 + 1 ) ** 2 );
|
|
994
|
+
const m1 = fastM1.mul( invNorm );
|
|
995
|
+
const m2 = fastM2.mul( invNorm );
|
|
996
|
+
const sigma = stdDev( m1, m2 ).mul( u.fastClampSigmaScale );
|
|
997
|
+
const clamped = diffLuma.clamp( m1.sub( sigma ), m1.add( sigma ) );
|
|
998
|
+
const fastEnabled = select( u.maxFast.lessThan( u.maxAccum ), float( 1.0 ), float( 0.0 ) );
|
|
999
|
+
const keep = mix( float( 1.0 ).div( float( 1.0 ).add( fastEnabled.mul( frameNum ).mul( 2.0 ) ) ), float( 1.0 ), handover );
|
|
1000
|
+
diffLuma.assign( mix( clamped, diffLuma, keep ) );
|
|
1001
|
+
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
textureStore( outTex, uvec, changeLuma( diff, diffLuma ) ).toWriteOnly();
|
|
1005
|
+
|
|
1006
|
+
} );
|
|
1007
|
+
|
|
1008
|
+
} );
|
|
1009
|
+
|
|
1010
|
+
return this._dispatchOf( computeFn );
|
|
1011
|
+
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// REBLUR_TemporalStabilization, plus remodulation and the debug views.
|
|
1015
|
+
_buildTemporalStabilization( stabWrite ) {
|
|
1016
|
+
|
|
1017
|
+
const u = this.u;
|
|
1018
|
+
const colorTex = this._colorTexNode;
|
|
1019
|
+
const albedoTex = this._albedoTexNode;
|
|
1020
|
+
const ndTex = this._ndTexNode;
|
|
1021
|
+
const snTex = this._snTexNode;
|
|
1022
|
+
const motionTex = this._motionTexNode;
|
|
1023
|
+
const histTex = this._nHist;
|
|
1024
|
+
const dataTex = this._nData;
|
|
1025
|
+
const stabRead = this._nStabRead;
|
|
1026
|
+
const internalOut = this._internal;
|
|
1027
|
+
const outTex = this._tmpA;
|
|
1028
|
+
const resW = this.resW;
|
|
1029
|
+
const resH = this.resH;
|
|
1030
|
+
const inputFrames = this.inputFrames;
|
|
1031
|
+
const resetU = this.resetU;
|
|
1032
|
+
const debugMode = this.debugMode;
|
|
1033
|
+
|
|
1034
|
+
const computeFn = Fn( () => {
|
|
1035
|
+
|
|
1036
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
1037
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
1038
|
+
|
|
1039
|
+
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
1040
|
+
|
|
1041
|
+
const coord = ivec2( gx, gy );
|
|
1042
|
+
const uvec = uvec2( uint( gx ), uint( gy ) );
|
|
1043
|
+
const dist = textureLoad( ndTex, coord ).w;
|
|
1044
|
+
const color = textureLoad( colorTex, coord ).toVar();
|
|
1045
|
+
|
|
1046
|
+
If( dist.greaterThanEqual( MISS_THRESHOLD ), () => {
|
|
1047
|
+
|
|
1048
|
+
// NRD leaves sky to the app. The stabilized luma still carries the sky's own value —
|
|
1049
|
+
// see HistoryFix: a 0 would be a black Catmull-Rom tap on the silhouette next frame.
|
|
1050
|
+
textureStore( outTex, uvec, color ).toWriteOnly();
|
|
1051
|
+
textureStore( stabWrite, uvec, vec4( textureLoad( histTex, coord ).x, 0.0, 0.0, 0.0 ) ).toWriteOnly();
|
|
1052
|
+
textureStore( internalOut, uvec, vec4( 0.0, 1.0, 0.0, 0.0 ) ).toWriteOnly();
|
|
1053
|
+
Return();
|
|
1054
|
+
|
|
1055
|
+
} );
|
|
1056
|
+
|
|
1057
|
+
const roughness = textureLoad( snTex, coord ).w.clamp( 0.05, 1.0 );
|
|
1058
|
+
const pixelUv = this._pixelUv( gx, gy );
|
|
1059
|
+
const mv = textureLoad( motionTex, coord );
|
|
1060
|
+
const smbUv = pixelUv.sub( mv.xy ).toVar();
|
|
1061
|
+
|
|
1062
|
+
const data = textureLoad( dataTex, coord ).toVar();
|
|
1063
|
+
const accumSpeed = data.y.toVar();
|
|
1064
|
+
const bits = uint( data.z.add( 0.5 ) ).toVar();
|
|
1065
|
+
const allowCatRom = data.w.greaterThan( 0.5 );
|
|
1066
|
+
|
|
1067
|
+
const bil = this._bilinear( smbUv );
|
|
1068
|
+
const occ = [ 1, 2, 4, 8 ].map( ( m ) =>
|
|
1069
|
+
select( bits.bitAnd( uint( m ) ).notEqual( uint( 0 ) ), float( 1.0 ), float( 0.0 ) ) );
|
|
1070
|
+
const occW = bil.customWeights( occ ).map( ( w ) => w.toVar() );
|
|
1071
|
+
const footprint = sqrt( saturate( bil.filter( occ ) ) ).toVar();
|
|
1072
|
+
|
|
1073
|
+
const diff = textureLoad( histTex, coord ).toVar();
|
|
1074
|
+
const diffLuma = diff.x.toVar();
|
|
1075
|
+
|
|
1076
|
+
const m1 = diffLuma.toVar();
|
|
1077
|
+
const m2 = diffLuma.mul( diffLuma ).toVar();
|
|
1078
|
+
for ( let j = - 1; j <= 1; j ++ ) {
|
|
1079
|
+
|
|
1080
|
+
for ( let i = - 1; i <= 1; i ++ ) {
|
|
1081
|
+
|
|
1082
|
+
if ( i === 0 && j === 0 ) continue;
|
|
1083
|
+
const pos = this._clampCoord( gx.add( i ), gy.add( j ) );
|
|
1084
|
+
const zs = textureLoad( ndTex, pos ).w;
|
|
1085
|
+
const l = select( zs.greaterThanEqual( MISS_THRESHOLD ), diffLuma, textureLoad( histTex, pos ).x );
|
|
1086
|
+
m1.addAssign( l );
|
|
1087
|
+
m2.addAssign( l.mul( l ) );
|
|
1088
|
+
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
m1.divAssign( 9.0 );
|
|
1094
|
+
m2.divAssign( 9.0 );
|
|
1095
|
+
const sigma = stdDev( m1, m2 ).toVar();
|
|
1096
|
+
|
|
1097
|
+
If( accumSpeed.lessThan( u.historyFixFrameNum ), () => {
|
|
1098
|
+
|
|
1099
|
+
diffLuma.assign( min( diffLuma, m1.mul( float( 1.2 ).add( float( 1.0 ).div( accumSpeed.add( 1.0 ) ) ) ) ) );
|
|
1100
|
+
|
|
1101
|
+
} );
|
|
1102
|
+
|
|
1103
|
+
const lumaHistory = max(
|
|
1104
|
+
this._sampleHistory( ( c ) => textureLoad( stabRead, c ), smbUv, bil, occW, allowCatRom ).x,
|
|
1105
|
+
0.0
|
|
1106
|
+
).toVar();
|
|
1107
|
+
|
|
1108
|
+
// NRD ComputeAntilag, REBLUR_ANTILAG_MODE 2, frame-rate scale 1.
|
|
1109
|
+
const s = sigma.mul( u.antilag.x );
|
|
1110
|
+
const magic = u.antilag.y.mul( 4.0 );
|
|
1111
|
+
const hc = lumaHistory.clamp( m1.sub( s ), m1.add( s ) );
|
|
1112
|
+
const d = lumaHistory.sub( hc ).abs().div( max( lumaHistory, hc ).add( NRD_EPS ) );
|
|
1113
|
+
const antilag = float( 1.0 ).div( float( 1.0 ).add( d.mul( accumSpeed.mul( footprint ) ).div( magic ) ) ).toVar();
|
|
1114
|
+
|
|
1115
|
+
const minAccum = min( accumSpeed, u.historyFixFrameNum );
|
|
1116
|
+
accumSpeed.assign( mix( minAccum, accumSpeed, antilag ) );
|
|
1117
|
+
|
|
1118
|
+
// NRD GetTemporalAccumulationParams.
|
|
1119
|
+
const k = inputFrames.add( 1.0 );
|
|
1120
|
+
const handover = this._handover( k );
|
|
1121
|
+
const wHist = footprint.mul( float( 1.0 ).sub( this._advancedNonLinear( accumSpeed.mul( float( 1.0 ).sub( handover ) ).div( k ) ) ) ).mul( antilag ).toVar();
|
|
1122
|
+
const sigmaScale = float( 1.0 ).add( wHist.mul( 6.0 ) );
|
|
1123
|
+
const stabStrength = u.stabilizationStrength.mul( float( 1.0 ).sub( resetU ) );
|
|
1124
|
+
|
|
1125
|
+
const clampedHistory = lumaHistory.clamp( m1.sub( sigma.mul( sigmaScale ) ), m1.add( sigma.mul( sigmaScale ) ) );
|
|
1126
|
+
const stabilized = mix( diffLuma, clampedHistory, min( wHist, stabStrength ) ).toVar();
|
|
1127
|
+
diff.assign( changeLuma( diff, stabilized ) );
|
|
1128
|
+
|
|
1129
|
+
textureStore( stabWrite, uvec, vec4( stabilized, 0.0, 0.0, 0.0 ) ).toWriteOnly();
|
|
1130
|
+
const maxAccumEff = u.maxAccum.mul( float( 1.0 ).sub( resetU ) );
|
|
1131
|
+
textureStore( internalOut, uvec, vec4( min( accumSpeed.add( 1.0 ), maxAccumEff ), roughness, 0.0, 0.0 ) ).toWriteOnly();
|
|
1132
|
+
|
|
1133
|
+
// Alpha stays the path tracer's coverage — Compositor reads it under transparentBackground.
|
|
1134
|
+
const albedo = textureLoad( albedoTex, coord );
|
|
1135
|
+
const lighting = yCoCgToLinear( diff.xyz ).mul( max( albedo.xyz, vec3( ALBEDO_EPS ) ) );
|
|
1136
|
+
const out = vec4( lighting, color.w ).toVar();
|
|
1137
|
+
|
|
1138
|
+
If( debugMode.equal( int( 1 ) ), () => {
|
|
1139
|
+
|
|
1140
|
+
const t = saturate( accumSpeed.div( max( u.maxAccum, 1.0 ) ) );
|
|
1141
|
+
out.assign( vec4( float( 1.0 ).sub( t ), t, 0.2, 1.0 ) );
|
|
1142
|
+
|
|
1143
|
+
} ).ElseIf( debugMode.equal( int( 2 ) ), () => {
|
|
1144
|
+
|
|
1145
|
+
out.assign( vec4( vec3( diff.w ), 1.0 ) );
|
|
1146
|
+
|
|
1147
|
+
} ).ElseIf( debugMode.equal( int( 3 ) ), () => {
|
|
1148
|
+
|
|
1149
|
+
out.assign( vec4( vec3( roughness ), 1.0 ) );
|
|
1150
|
+
|
|
1151
|
+
} ).ElseIf( debugMode.equal( int( 4 ) ), () => {
|
|
1152
|
+
|
|
1153
|
+
const l = data.x.div( data.x.add( 1.0 ) );
|
|
1154
|
+
out.assign( vec4( vec3( l ), 1.0 ) );
|
|
1155
|
+
|
|
1156
|
+
} ).ElseIf( debugMode.equal( int( 5 ) ), () => {
|
|
1157
|
+
|
|
1158
|
+
out.assign( vec4( vec3( data.z.div( 15.0 ) ), 1.0 ) );
|
|
1159
|
+
|
|
1160
|
+
} );
|
|
1161
|
+
|
|
1162
|
+
textureStore( outTex, uvec, out ).toWriteOnly();
|
|
1163
|
+
|
|
1164
|
+
} );
|
|
1165
|
+
|
|
1166
|
+
} );
|
|
1167
|
+
|
|
1168
|
+
return computeFn().compute(
|
|
1169
|
+
[ this._dispatchX, this._dispatchY, 1 ],
|
|
1170
|
+
[ WG_SIZE, WG_SIZE, 1 ]
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
1176
|
+
|
|
1177
|
+
setupEventListeners() {
|
|
1178
|
+
|
|
1179
|
+
this.on( 'denoiser:reset', () => this.resetHistory() );
|
|
1180
|
+
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
resetHistory() {
|
|
1184
|
+
|
|
1185
|
+
this._needsReset = true;
|
|
1186
|
+
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
_updateCamera() {
|
|
1190
|
+
|
|
1191
|
+
const pt = this.pathTracer;
|
|
1192
|
+
if ( ! pt?.uniforms ) return;
|
|
1193
|
+
|
|
1194
|
+
const world = pt.uniforms.get( 'cameraWorldMatrix' ).value;
|
|
1195
|
+
const view = pt.uniforms.get( 'cameraViewMatrix' ).value;
|
|
1196
|
+
const projInv = pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value;
|
|
1197
|
+
const proj = pt.uniforms.get( 'cameraProjectionMatrix' ).value;
|
|
1198
|
+
|
|
1199
|
+
if ( this._camInitialized ) {
|
|
1200
|
+
|
|
1201
|
+
this.prevCamView.value.copy( this.camView.value );
|
|
1202
|
+
this.prevCamPos.value.copy( this.camPos.value );
|
|
1203
|
+
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
this.camWorld.value.copy( world );
|
|
1207
|
+
this.camProjInv.value.copy( projInv );
|
|
1208
|
+
this.camView.value.copy( view );
|
|
1209
|
+
this.camPos.value.setFromMatrixPosition( world );
|
|
1210
|
+
this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
|
|
1211
|
+
this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
|
|
1212
|
+
this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
|
|
1213
|
+
this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
|
|
1214
|
+
|
|
1215
|
+
if ( ! this._camInitialized ) {
|
|
1216
|
+
|
|
1217
|
+
this.prevCamView.value.copy( view );
|
|
1218
|
+
this.prevCamPos.value.copy( this.camPos.value );
|
|
1219
|
+
this._camInitialized = true;
|
|
1220
|
+
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// NRD gUnproject; frustum size follows the shorter edge.
|
|
1224
|
+
const projY = proj.elements[ 5 ] || 1;
|
|
1225
|
+
const unproject = 1 / ( 0.5 * this.resH.value * projY );
|
|
1226
|
+
this.minRectDimMulUnproject.value = Math.min( this.resW.value, this.resH.value ) * unproject;
|
|
1227
|
+
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
_updateRotators( frameKey ) {
|
|
1231
|
+
|
|
1232
|
+
const set = ( u, salt ) => {
|
|
1233
|
+
|
|
1234
|
+
const a = hash01( frameKey * 3 + salt ) * Math.PI * 2;
|
|
1235
|
+
u.value.set( Math.cos( a ), Math.sin( a ), - Math.sin( a ), Math.cos( a ) );
|
|
1236
|
+
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
set( this.rotPre, 0 );
|
|
1240
|
+
set( this.rotBlur, 1 );
|
|
1241
|
+
set( this.rotPost, 2 );
|
|
1242
|
+
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
render( context ) {
|
|
1246
|
+
|
|
1247
|
+
if ( ! this.enabled ) return;
|
|
1248
|
+
|
|
1249
|
+
const colorTex = context.getTexture( 'pathtracer:color' );
|
|
1250
|
+
const albedoTex = context.getTexture( 'pathtracer:albedo' );
|
|
1251
|
+
const ndTex = context.getTexture( 'pathtracer:normalDepth' );
|
|
1252
|
+
const snTex = context.getTexture( 'pathtracer:shadingNormal' );
|
|
1253
|
+
const motionTex = context.getTexture( 'motionVector:screenSpace' );
|
|
1254
|
+
|
|
1255
|
+
if ( ! colorTex ) return;
|
|
1256
|
+
|
|
1257
|
+
// No guides ⇒ no guidance. Pass through rather than blur blind.
|
|
1258
|
+
if ( ! albedoTex || ! ndTex || ! snTex || ! motionTex ) {
|
|
1259
|
+
|
|
1260
|
+
context.setTexture( 'nrd:output', colorTex );
|
|
1261
|
+
return;
|
|
1262
|
+
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const img = colorTex.image;
|
|
1266
|
+
if ( img && img.width > 0 && img.height > 0 &&
|
|
1267
|
+
( img.width !== this.outputTarget.width || img.height !== this.outputTarget.height ) ) {
|
|
1268
|
+
|
|
1269
|
+
this.setSize( img.width, img.height );
|
|
1270
|
+
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Past the handover the input is the better estimate: republish it and skip the six dispatches.
|
|
1274
|
+
// The geometry history and prev camera freeze here, so mark them stale: a camera move is a SOFT
|
|
1275
|
+
// reset, and a 1-frame motion vector into a stale G-buffer would keep whatever taps passed.
|
|
1276
|
+
const inputFrames = context.getState( 'pathtracer:samples' ) ?? this.pathTracer?.frameCount ?? 0;
|
|
1277
|
+
if ( inputFrames + 1 >= this.u.handoverFrames.value ) {
|
|
1278
|
+
|
|
1279
|
+
context.setTexture( 'nrd:output', colorTex );
|
|
1280
|
+
this._needsReset = true;
|
|
1281
|
+
return;
|
|
1282
|
+
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
this._colorTexNode.value = colorTex;
|
|
1286
|
+
this._albedoTexNode.value = albedoTex;
|
|
1287
|
+
this._ndTexNode.value = ndTex;
|
|
1288
|
+
this._snTexNode.value = snTex;
|
|
1289
|
+
this._motionTexNode.value = motionTex;
|
|
1290
|
+
this._updateCamera();
|
|
1291
|
+
this.inputFrames.value = inputFrames;
|
|
1292
|
+
this._updateRotators( this.pathTracer?.uniforms?.get( 'seedFrame' )?.value ?? context.getState( 'frame' ) ?? 0 );
|
|
1293
|
+
this.resetU.value = this._needsReset ? 1.0 : 0.0;
|
|
1294
|
+
|
|
1295
|
+
// Compile while the read nodes still hold their placeholders, so textureLoad carries the `level`
|
|
1296
|
+
// argument StorageTexture reads require.
|
|
1297
|
+
if ( ! this._compiled ) {
|
|
1298
|
+
|
|
1299
|
+
for ( const k of this._kernels ) this.renderer.compute( k );
|
|
1300
|
+
this._compiled = true;
|
|
1301
|
+
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
this._nTmpA.value = this._tmpA;
|
|
1305
|
+
this._nTmpB.value = this._tmpB;
|
|
1306
|
+
this._nHist.value = this._hist;
|
|
1307
|
+
this._nFast.value = this._fastHist;
|
|
1308
|
+
this._nInternal.value = this._internal;
|
|
1309
|
+
this._nGeom.value = this._geomPrev;
|
|
1310
|
+
this._nData.value = this._dataT;
|
|
1311
|
+
this._nStabRead.value = this._stabPing === 0 ? this._stabB : this._stabA;
|
|
1312
|
+
|
|
1313
|
+
this.renderer.compute( this._kPre );
|
|
1314
|
+
this.renderer.compute( this._kTa );
|
|
1315
|
+
this.renderer.compute( this._kHf );
|
|
1316
|
+
this.renderer.compute( this._kBlur );
|
|
1317
|
+
this.renderer.compute( this._kPost );
|
|
1318
|
+
this.renderer.compute( this._stabPing === 0 ? this._kTsA : this._kTsB );
|
|
1319
|
+
|
|
1320
|
+
this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
|
|
1321
|
+
this.renderer.copyTextureToTexture( this._tmpA, this.outputTarget.texture, this._srcRegion );
|
|
1322
|
+
context.setTexture( 'nrd:output', this.outputTarget.texture );
|
|
1323
|
+
|
|
1324
|
+
this._stabPing = 1 - this._stabPing;
|
|
1325
|
+
this._needsReset = false;
|
|
1326
|
+
this._needsReset = false;
|
|
1327
|
+
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
setSize( width, height ) {
|
|
1331
|
+
|
|
1332
|
+
this.outputTarget.setSize( width, height );
|
|
1333
|
+
this.outputTarget.texture.needsUpdate = true;
|
|
1334
|
+
this.resW.value = width;
|
|
1335
|
+
this.resH.value = height;
|
|
1336
|
+
|
|
1337
|
+
this._dispatchX = Math.ceil( width / WG_SIZE );
|
|
1338
|
+
this._dispatchY = Math.ceil( height / WG_SIZE );
|
|
1339
|
+
const size = [ this._dispatchX, this._dispatchY, 1 ];
|
|
1340
|
+
for ( const k of this._kernels ) {
|
|
1341
|
+
|
|
1342
|
+
if ( k ) k.dispatchSize = size;
|
|
1343
|
+
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// StorageTextures stay at max; history from another size is meaningless.
|
|
1347
|
+
this._needsReset = true;
|
|
1348
|
+
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// three.js recreates the textures on the next dispatch after re-enable.
|
|
1352
|
+
releaseGPUMemory() {
|
|
1353
|
+
|
|
1354
|
+
this._disposeStorage();
|
|
1355
|
+
this.context?.removeTexture( 'nrd:output' );
|
|
1356
|
+
this.outputTarget?.texture?.dispose();
|
|
1357
|
+
this._needsReset = true;
|
|
1358
|
+
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
reallocateReservedStorage() {
|
|
1362
|
+
|
|
1363
|
+
this._disposeKernels();
|
|
1364
|
+
this._disposeStorage();
|
|
1365
|
+
// Restore the placeholders — the rebuilt kernels compile against them (see constructor).
|
|
1366
|
+
for ( const [ node, placeholder ] of this._placeholders ) node.value = placeholder;
|
|
1367
|
+
this._allocStorage();
|
|
1368
|
+
this._buildKernels();
|
|
1369
|
+
this._compiled = false;
|
|
1370
|
+
this._needsReset = true;
|
|
1371
|
+
this._stabPing = 0;
|
|
1372
|
+
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
dispose() {
|
|
1376
|
+
|
|
1377
|
+
this._disposeKernels();
|
|
1378
|
+
this._disposeStorage();
|
|
1379
|
+
this.outputTarget?.dispose();
|
|
1380
|
+
for ( const n of [
|
|
1381
|
+
this._colorTexNode, this._albedoTexNode, this._ndTexNode, this._snTexNode, this._motionTexNode,
|
|
1382
|
+
] ) n?.dispose();
|
|
1383
|
+
// The read nodes are exactly the placeholder map's keys.
|
|
1384
|
+
for ( const [ node, placeholder ] of this._placeholders ) {
|
|
1385
|
+
|
|
1386
|
+
node.dispose();
|
|
1387
|
+
placeholder.dispose();
|
|
1388
|
+
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
this._placeholders.clear();
|
|
1392
|
+
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
}
|