rayzee 11.6.0 → 11.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/{ArchiveImporter-iofnC8zB.js → ArchiveImporter-DYcRXeKE.js} +2 -2
- package/dist/{BidirectionalIntegrator-CqAilt2I.js → BidirectionalIntegrator-BvjRpbTP.js} +2 -2
- package/dist/{NeuralRetouch-DFI4pOjO.js → NeuralRetouch-ClaMiKYg.js} +1 -1
- package/dist/{NeuralSuperRes-ChCiiilS.js → NeuralSuperRes-WNEXun9s.js} +1 -1
- package/dist/{PathTracer-BX4sJLx7.js → PathTracer-B9wZPPVL.js} +6 -8
- package/dist/{PhysicalSky-Biv39eTA.js → PhysicalSky-DcjItqKi.js} +1 -1
- package/dist/{RayzeeRenderer-D8XRKU3A.js → RayzeeRenderer-p-NeE6E2.js} +2 -3
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +3 -3
- package/dist/rayzee.es.js +865 -1591
- package/dist/rayzee.umd.js +87 -134
- package/dist/{sceneCachePolicy-W5PLbzuR.js → sceneCachePolicy-B9w3BnaP.js} +1 -1
- package/package.json +1 -1
- package/src/Passes/OIDNDenoiser.js +9 -11
- package/src/PathTracerApp.js +6 -17
- package/src/Pipeline/BindingAudit.js +2 -3
- package/src/Pipeline/PipelineContext.js +0 -1
- package/src/Pipeline/RenderPipeline.js +1 -2
- package/src/Pipeline/RenderStage.js +1 -1
- package/src/Processor/PackedRayBuffer.js +2 -2
- package/src/Processor/StorageTexturePool.js +1 -1
- package/src/RayzeeRenderer.js +1 -1
- package/src/Stages/DenoiserSettings.js +5 -81
- package/src/Stages/MotionVector.js +21 -215
- package/src/Stages/NormalDepth.js +5 -5
- package/src/Stages/PathTracer.js +1 -1
- package/src/Stages/PathTracerStage.js +4 -17
- package/src/TSL/FinalWriteKernel.js +2 -2
- package/src/TSL/ShadeKernel.js +3 -3
- package/src/index.js +1 -1
- package/src/managers/DenoisingManager.js +78 -187
- package/src/Stages/ASVGF.js +0 -932
- package/src/Stages/BilateralFilter.js +0 -486
- package/src/Stages/EdgeFilter.js +0 -545
- package/src/Stages/Variance.js +0 -443
package/src/Stages/EdgeFilter.js
DELETED
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
-
If, dot, max, mix, pow, sqrt, select,
|
|
3
|
-
textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
|
|
4
|
-
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
|
-
import { FloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
|
|
6
|
-
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
|
-
import { luminance } from '../TSL/Common.js';
|
|
8
|
-
import { MAX_STORAGE_TEXTURE_SIZE } from '../Processor/StorageTexturePool.js';
|
|
9
|
-
import { ALBEDO_EPS } from './DenoiserSettings.js';
|
|
10
|
-
|
|
11
|
-
// NormalDepth writes depth ≈ 65504 for miss (env/background) rays. This sits below
|
|
12
|
-
// the sentinel and above any real ray distance, so it's a clean hit/miss flag.
|
|
13
|
-
const MISS_THRESHOLD = 6e4;
|
|
14
|
-
|
|
15
|
-
// NaN/±Inf guard on demodulated lighting: dividing color by a near-zero albedo can
|
|
16
|
-
// blow up, and one poisoned tap would smear through the à-trous passes. Per-channel:
|
|
17
|
-
// NaN (x≠x) → 0, ±Inf clamped to a bounded HDR range. Ceiling matches ASVGF/Variance
|
|
18
|
-
// (1e7); the FloatType ping-pong textures hold it without HalfFloat's 65k clip.
|
|
19
|
-
const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
20
|
-
const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
|
|
21
|
-
|
|
22
|
-
// Frames before the temporal EMA variance is trusted on its own (Schied 2017 §4.2).
|
|
23
|
-
const VARIANCE_HISTORY_MIN = 4;
|
|
24
|
-
|
|
25
|
-
// 5×5 à-trous kernel weights (B3-spline / Gaussian approx, Σ = 1.0).
|
|
26
|
-
const ATROUS_KERNEL = [
|
|
27
|
-
1.0 / 256.0, 4.0 / 256.0, 6.0 / 256.0, 4.0 / 256.0, 1.0 / 256.0,
|
|
28
|
-
4.0 / 256.0, 16.0 / 256.0, 24.0 / 256.0, 16.0 / 256.0, 4.0 / 256.0,
|
|
29
|
-
6.0 / 256.0, 24.0 / 256.0, 36.0 / 256.0, 24.0 / 256.0, 6.0 / 256.0,
|
|
30
|
-
4.0 / 256.0, 16.0 / 256.0, 24.0 / 256.0, 16.0 / 256.0, 4.0 / 256.0,
|
|
31
|
-
1.0 / 256.0, 4.0 / 256.0, 6.0 / 256.0, 4.0 / 256.0, 1.0 / 256.0,
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* WebGPU Edge-Aware Filtering Stage (Compute Shader).
|
|
36
|
-
*
|
|
37
|
-
* A spatial-only SVGF à-trous denoiser (Dammertz 2010 + Schied 2017 edge-stops)
|
|
38
|
-
* that runs directly on the progressively-accumulated frame — no temporal
|
|
39
|
-
* reprojection, motion vectors, or history buffers, so it is free of the
|
|
40
|
-
* ghosting/lag artefacts of the ASVGF chain. Intended for static-camera
|
|
41
|
-
* progressive rendering where a light, artefact-free spatial filter is wanted.
|
|
42
|
-
*
|
|
43
|
-
* Pipeline per frame:
|
|
44
|
-
* 1. Demodulate: lighting = color / albedo (miss rays use a neutral albedo so
|
|
45
|
-
* bright HDR sky neither overflows the divide nor gets zeroed on remodulate).
|
|
46
|
-
* 2. À-trous: `iterations` ping-pong passes with step size 2^i over a dense
|
|
47
|
-
* 5×5 Gaussian kernel, growing the effective footprint to tens of pixels.
|
|
48
|
-
* Edge-stops: variance-guided luminance σ (from variance:output), a
|
|
49
|
-
* shading-normal cone, and a scale-invariant relative depth gate.
|
|
50
|
-
* 3. Remodulate + strength blend on the final pass.
|
|
51
|
-
*
|
|
52
|
-
* Reads: pathtracer:color, pathtracer:normalDepth, pathtracer:shadingNormal,
|
|
53
|
-
* pathtracer:albedo, variance:output
|
|
54
|
-
* Publishes: edgeFiltering:output
|
|
55
|
-
* Mode: PER_CYCLE
|
|
56
|
-
*/
|
|
57
|
-
export class EdgeFilter extends RenderStage {
|
|
58
|
-
|
|
59
|
-
constructor( renderer, options = {} ) {
|
|
60
|
-
|
|
61
|
-
super( 'EdgeAwareFiltering', {
|
|
62
|
-
...options,
|
|
63
|
-
executionMode: StageExecutionMode.PER_CYCLE
|
|
64
|
-
} );
|
|
65
|
-
|
|
66
|
-
this.renderer = renderer;
|
|
67
|
-
|
|
68
|
-
// filterStrength: final blend, 0 = raw color, 1 = fully filtered.
|
|
69
|
-
// phiLuminance: variance-scaled luminance edge-stop (bigger = more blending).
|
|
70
|
-
// phiNormal: normal cone exponent (bigger = tighter, preserves more edges).
|
|
71
|
-
// phiDepth: RELATIVE depth tolerance (fraction of ray distance, scale-invariant).
|
|
72
|
-
this.filterStrength = uniform( options.filterStrength ?? 1.0 );
|
|
73
|
-
// 1.0, not the old 4.0, which was tuned against the non-converging variance seed below
|
|
74
|
-
// and now over-blurs enough to lose to no denoising at all from ~16spp up.
|
|
75
|
-
this.phiLuminance = uniform( options.phiLuminance ?? 1.0 );
|
|
76
|
-
this.phiNormal = uniform( options.phiNormal ?? 64.0 );
|
|
77
|
-
this.phiDepth = uniform( options.phiDepth ?? 0.1 );
|
|
78
|
-
this.iterations = options.atrousIterations ?? options.edgeAtrousIterations ?? 5;
|
|
79
|
-
|
|
80
|
-
this.stepSizeU = uniform( 1, 'int' );
|
|
81
|
-
this.isLastIterationU = uniform( 0, 'int' );
|
|
82
|
-
// 1 on pass 0 → seed variance from variance:output; later passes read the carried value.
|
|
83
|
-
this.isFirstIterationU = uniform( 1, 'int' );
|
|
84
|
-
this.varianceHistoryU = uniform( 0, 'int' );
|
|
85
|
-
this.resW = uniform( options.width || 1 );
|
|
86
|
-
this.resH = uniform( options.height || 1 );
|
|
87
|
-
|
|
88
|
-
// Input texture nodes (RenderTarget-backed — safe to bind pre-compile).
|
|
89
|
-
this._colorTexNode = new TextureNode();
|
|
90
|
-
this._ndTexNode = new TextureNode();
|
|
91
|
-
this._snTexNode = new TextureNode();
|
|
92
|
-
this._albedoTexNode = new TextureNode();
|
|
93
|
-
this._varTexNode = new TextureNode();
|
|
94
|
-
|
|
95
|
-
// À-trous read node: defaults to EmptyTexture at compile time so the WGSL
|
|
96
|
-
// codegen emits the `level` parameter required for later StorageTexture reads.
|
|
97
|
-
this._readTexNode = new TextureNode();
|
|
98
|
-
|
|
99
|
-
const w = options.width || 1;
|
|
100
|
-
const h = options.height || 1;
|
|
101
|
-
|
|
102
|
-
// Pre-allocate StorageTextures at max — defensive against three.js #33061
|
|
103
|
-
// (TSL compute pipeline keeps a stale GPUTextureView after setSize()).
|
|
104
|
-
// LinearFilter required for textureLoad codegen on StorageTextures.
|
|
105
|
-
this._demodTex = this._makeStorageTex(); // demodulated lighting (pre-pass output)
|
|
106
|
-
this._storageTexA = this._makeStorageTex(); // à-trous ping
|
|
107
|
-
this._storageTexB = this._makeStorageTex(); // à-trous pong
|
|
108
|
-
|
|
109
|
-
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
110
|
-
|
|
111
|
-
// Active-size RT published downstream (UV-sampled); the over-allocated
|
|
112
|
-
// StorageTexture must not be published — UV sampling reads the wrong region.
|
|
113
|
-
// FloatType to match the storage textures so copyTextureToTexture is format-compatible.
|
|
114
|
-
this.outputTarget = new RenderTarget( w, h, {
|
|
115
|
-
type: FloatType,
|
|
116
|
-
format: RGBAFormat,
|
|
117
|
-
minFilter: LinearFilter,
|
|
118
|
-
magFilter: LinearFilter,
|
|
119
|
-
depthBuffer: false,
|
|
120
|
-
stencilBuffer: false
|
|
121
|
-
} );
|
|
122
|
-
|
|
123
|
-
this._compiled = false;
|
|
124
|
-
|
|
125
|
-
this._dispatchX = Math.ceil( w / 16 );
|
|
126
|
-
this._dispatchY = Math.ceil( h / 16 );
|
|
127
|
-
|
|
128
|
-
this._buildCompute();
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
_makeStorageTex() {
|
|
133
|
-
|
|
134
|
-
// FloatType: demodulated lighting on dark materials (color / 0.01) exceeds
|
|
135
|
-
// HalfFloat's 65k cap on HDR — same reason ASVGF's ping-pong is FloatType.
|
|
136
|
-
const tex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
|
|
137
|
-
tex.type = FloatType;
|
|
138
|
-
tex.format = RGBAFormat;
|
|
139
|
-
tex.minFilter = LinearFilter;
|
|
140
|
-
tex.magFilter = LinearFilter;
|
|
141
|
-
return tex;
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
_buildCompute() {
|
|
146
|
-
|
|
147
|
-
this._computeDemod = this._buildDemod();
|
|
148
|
-
// One à-trous node per ping-pong write direction.
|
|
149
|
-
this._computeAtrousA = this._buildAtrous( this._storageTexA );
|
|
150
|
-
this._computeAtrousB = this._buildAtrous( this._storageTexB );
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Demodulate pass: lighting = color / albedo, written to _demodTex.
|
|
155
|
-
_buildDemod() {
|
|
156
|
-
|
|
157
|
-
const colorTex = this._colorTexNode;
|
|
158
|
-
const ndTex = this._ndTexNode;
|
|
159
|
-
const albedoTex = this._albedoTexNode;
|
|
160
|
-
const demodTex = this._demodTex;
|
|
161
|
-
const resW = this.resW;
|
|
162
|
-
const resH = this.resH;
|
|
163
|
-
|
|
164
|
-
const WG_SIZE = 16;
|
|
165
|
-
|
|
166
|
-
const computeFn = Fn( () => {
|
|
167
|
-
|
|
168
|
-
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
169
|
-
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
170
|
-
|
|
171
|
-
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
172
|
-
|
|
173
|
-
const coord = ivec2( gx, gy );
|
|
174
|
-
const color = textureLoad( colorTex, coord ).xyz;
|
|
175
|
-
const depth = textureLoad( ndTex, coord ).w;
|
|
176
|
-
const isHit = depth.lessThan( float( MISS_THRESHOLD ) );
|
|
177
|
-
|
|
178
|
-
// Miss rays (env/background) have black aux albedo — dividing by it would
|
|
179
|
-
// overflow HalfFloat on bright HDR sky and remodulate back to zero. Use a
|
|
180
|
-
// neutral albedo of 1 for misses so the demod/remod round-trip is identity.
|
|
181
|
-
const albedo = textureLoad( albedoTex, coord ).xyz;
|
|
182
|
-
const demodAlbedo = select( isHit, max( albedo, vec3( ALBEDO_EPS ) ), vec3( 1.0 ) );
|
|
183
|
-
|
|
184
|
-
const lighting = sanitizeRGB( color.div( demodAlbedo ) );
|
|
185
|
-
|
|
186
|
-
textureStore( demodTex, uvec2( uint( gx ), uint( gy ) ), vec4( lighting, 1.0 ) ).toWriteOnly();
|
|
187
|
-
|
|
188
|
-
} );
|
|
189
|
-
|
|
190
|
-
} );
|
|
191
|
-
|
|
192
|
-
return computeFn().compute(
|
|
193
|
-
[ this._dispatchX, this._dispatchY, 1 ],
|
|
194
|
-
[ WG_SIZE, WG_SIZE, 1 ]
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// À-trous pass over demodulated lighting, writing to `writeStorageTex`.
|
|
200
|
-
_buildAtrous( writeStorageTex ) {
|
|
201
|
-
|
|
202
|
-
const readTex = this._readTexNode;
|
|
203
|
-
const colorTex = this._colorTexNode;
|
|
204
|
-
const ndTex = this._ndTexNode;
|
|
205
|
-
const snTex = this._snTexNode;
|
|
206
|
-
const albedoTex = this._albedoTexNode;
|
|
207
|
-
const varTex = this._varTexNode;
|
|
208
|
-
const filterStrength = this.filterStrength;
|
|
209
|
-
const phiLuminance = this.phiLuminance;
|
|
210
|
-
const phiNormal = this.phiNormal;
|
|
211
|
-
const phiDepth = this.phiDepth;
|
|
212
|
-
const stepSize = this.stepSizeU;
|
|
213
|
-
const isLastIterationU = this.isLastIterationU;
|
|
214
|
-
const isFirstIterationU = this.isFirstIterationU;
|
|
215
|
-
const varianceHistoryU = this.varianceHistoryU;
|
|
216
|
-
const resW = this.resW;
|
|
217
|
-
const resH = this.resH;
|
|
218
|
-
|
|
219
|
-
const WG_SIZE = 16;
|
|
220
|
-
|
|
221
|
-
const computeFn = Fn( () => {
|
|
222
|
-
|
|
223
|
-
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
224
|
-
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
225
|
-
|
|
226
|
-
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
227
|
-
|
|
228
|
-
const coord = ivec2( gx, gy );
|
|
229
|
-
|
|
230
|
-
const centerSample = textureLoad( readTex, coord );
|
|
231
|
-
const centerLighting = centerSample.xyz;
|
|
232
|
-
const centerLum = luminance( centerLighting );
|
|
233
|
-
const centerND = textureLoad( ndTex, coord );
|
|
234
|
-
const centerDepth = centerND.w;
|
|
235
|
-
// Shading (bump-perturbed) normal so normal/bump detail isn't collapsed.
|
|
236
|
-
const centerNormal = textureLoad( snTex, coord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
237
|
-
const centerIsHitBool = centerDepth.lessThan( float( MISS_THRESHOLD ) );
|
|
238
|
-
const centerIsHit = select( centerIsHitBool, float( 1.0 ), float( 0.0 ) );
|
|
239
|
-
|
|
240
|
-
const centerAlbedo = textureLoad( albedoTex, coord ).xyz;
|
|
241
|
-
const demodAlbedo = select( centerIsHitBool, max( centerAlbedo, vec3( ALBEDO_EPS ) ), vec3( 1.0 ) );
|
|
242
|
-
const centerAlbedoLum = max( luminance( demodAlbedo ), float( ALBEDO_EPS ) );
|
|
243
|
-
|
|
244
|
-
// Variance-guided luminance σ (SVGF). .z = temporal, .w = spatial variance.
|
|
245
|
-
// Temporal only, once the EMA is warm: .w is the 3×3 contrast of the
|
|
246
|
-
// *accumulated* image, so it plateaus at scene detail instead of decaying with
|
|
247
|
-
// sample count. max()-ing it in pinned sigma_l wide forever, which left the
|
|
248
|
-
// geometric gates — binary reads off the jitter-free G-buffer — to snap every
|
|
249
|
-
// antialiased silhouette pixel to one side, destroying sub-pixel AA.
|
|
250
|
-
//
|
|
251
|
-
// Dividing by albedoLum rescales the modulated-space variance into demodulated
|
|
252
|
-
// space (std ∝ 1/albedo). Filtered alongside colour and carried between passes
|
|
253
|
-
// as √variance in the ping-pong's spare alpha, so sigma_l tightens as the signal
|
|
254
|
-
// smooths. Stored as the root because converged variance is a half-float subnormal.
|
|
255
|
-
const isFirst = isFirstIterationU.equal( int( 1 ) );
|
|
256
|
-
const historyShort = varianceHistoryU.lessThan( int( VARIANCE_HISTORY_MIN ) );
|
|
257
|
-
const seedVariance = ( at ) => {
|
|
258
|
-
|
|
259
|
-
const v = textureLoad( varTex, at );
|
|
260
|
-
return select( historyShort, max( v.z, v.w ), v.z );
|
|
261
|
-
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const centerVariance = isFirst.select(
|
|
265
|
-
seedVariance( coord ),
|
|
266
|
-
centerSample.w.mul( centerSample.w )
|
|
267
|
-
).toVar();
|
|
268
|
-
|
|
269
|
-
const sigmaL = phiLuminance.mul( sqrt( max( centerVariance, float( 0.0 ) ) ) )
|
|
270
|
-
.div( centerAlbedoLum ).add( float( 0.0001 ) );
|
|
271
|
-
|
|
272
|
-
const colorSum = vec3( 0.0 ).toVar();
|
|
273
|
-
const weightSum = float( 0.0 ).toVar();
|
|
274
|
-
// Squared weights, normalised by weightSum² — the variance of a weighted mean.
|
|
275
|
-
const varianceSum = float( 0.0 ).toVar();
|
|
276
|
-
|
|
277
|
-
for ( let iy = 0; iy < 5; iy ++ ) {
|
|
278
|
-
|
|
279
|
-
for ( let ix = 0; ix < 5; ix ++ ) {
|
|
280
|
-
|
|
281
|
-
const dx = ix - 2;
|
|
282
|
-
const dy = iy - 2;
|
|
283
|
-
const kw = ATROUS_KERNEL[ iy * 5 + ix ];
|
|
284
|
-
|
|
285
|
-
const sx = gx.add( stepSize.mul( dx ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
286
|
-
const sy = gy.add( stepSize.mul( dy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
287
|
-
const sCoord = ivec2( sx, sy );
|
|
288
|
-
|
|
289
|
-
const sSample = textureLoad( readTex, sCoord );
|
|
290
|
-
const sLighting = sSample.xyz;
|
|
291
|
-
const sLum = luminance( sLighting );
|
|
292
|
-
const sND = textureLoad( ndTex, sCoord );
|
|
293
|
-
const sDepth = sND.w;
|
|
294
|
-
const sNormal = textureLoad( snTex, sCoord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
295
|
-
const sampleIsHit = select( sDepth.lessThan( float( MISS_THRESHOLD ) ), float( 1.0 ), float( 0.0 ) );
|
|
296
|
-
|
|
297
|
-
const lumW = centerLum.sub( sLum ).abs().div( sigmaL ).negate().exp();
|
|
298
|
-
// Clamp dot to [0,1] before pow — miss-ray normals decode to non-unit
|
|
299
|
-
// (-1,-1,-1) with dot 3, which would saturate pow to +inf → inf*0 = NaN.
|
|
300
|
-
const normW = pow( dot( centerNormal, sNormal ).clamp( 0.0, 1.0 ), phiNormal );
|
|
301
|
-
// Relative depth tolerance → scale-invariant across scene sizes.
|
|
302
|
-
const depW = centerDepth.sub( sDepth ).abs()
|
|
303
|
-
.div( max( centerDepth.mul( phiDepth ), float( 0.001 ) ) ).negate().exp();
|
|
304
|
-
|
|
305
|
-
const bothHit = centerIsHit.mul( sampleIsHit );
|
|
306
|
-
const bothMiss = centerIsHit.oneMinus().mul( sampleIsHit.oneMinus() );
|
|
307
|
-
const sameKind = bothHit.add( bothMiss );
|
|
308
|
-
|
|
309
|
-
// Geometric weights are only meaningful for hit-vs-hit pairs.
|
|
310
|
-
const geomW = mix( float( 1.0 ), normW.mul( depW ), bothHit );
|
|
311
|
-
const w = float( kw ).mul( lumW ).mul( geomW ).mul( sameKind );
|
|
312
|
-
|
|
313
|
-
const sVariance = isFirst.select(
|
|
314
|
-
seedVariance( sCoord ),
|
|
315
|
-
sSample.w.mul( sSample.w )
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
colorSum.addAssign( sLighting.mul( w ) );
|
|
319
|
-
weightSum.addAssign( w );
|
|
320
|
-
varianceSum.addAssign( w.mul( w ).mul( max( sVariance, float( 0.0 ) ) ) );
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
|
|
327
|
-
const filteredVariance = varianceSum
|
|
328
|
-
.div( max( weightSum.mul( weightSum ), float( 1e-8 ) ) );
|
|
329
|
-
|
|
330
|
-
// Final pass: remodulate by albedo and blend against the raw color by
|
|
331
|
-
// filterStrength. Inner passes stay in demodulated space.
|
|
332
|
-
const isLast = isLastIterationU.equal( int( 1 ) );
|
|
333
|
-
const rawColor = textureLoad( colorTex, coord ).xyz;
|
|
334
|
-
const remodded = filtered.mul( demodAlbedo );
|
|
335
|
-
const finalModulated = mix( rawColor, remodded, filterStrength );
|
|
336
|
-
const output = isLast.select( finalModulated, filtered );
|
|
337
|
-
|
|
338
|
-
// The last pass restores the path tracer's coverage — Compositor reads .w as
|
|
339
|
-
// image alpha when transparentBackground is on.
|
|
340
|
-
const carriedAlpha = isLast.select(
|
|
341
|
-
textureLoad( colorTex, coord ).w,
|
|
342
|
-
sqrt( max( filteredVariance, float( 0.0 ) ) )
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
textureStore( writeStorageTex, uvec2( uint( gx ), uint( gy ) ), vec4( output, carriedAlpha ) ).toWriteOnly();
|
|
346
|
-
|
|
347
|
-
} );
|
|
348
|
-
|
|
349
|
-
} );
|
|
350
|
-
|
|
351
|
-
return computeFn().compute(
|
|
352
|
-
[ this._dispatchX, this._dispatchY, 1 ],
|
|
353
|
-
[ WG_SIZE, WG_SIZE, 1 ]
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
render( context ) {
|
|
359
|
-
|
|
360
|
-
if ( ! this.enabled ) return;
|
|
361
|
-
|
|
362
|
-
const colorTex = context.getTexture( 'pathtracer:color' );
|
|
363
|
-
const ndTex = context.getTexture( 'pathtracer:normalDepth' );
|
|
364
|
-
// Fall back to geometric normalDepth if the mapped normal isn't published.
|
|
365
|
-
const snTex = context.getTexture( 'pathtracer:shadingNormal' ) || ndTex;
|
|
366
|
-
const albedoTex = context.getTexture( 'pathtracer:albedo' );
|
|
367
|
-
const varTex = context.getTexture( 'variance:output' );
|
|
368
|
-
|
|
369
|
-
// The SVGF filter needs color + geometry + albedo + variance. Without the full
|
|
370
|
-
// set there's no edge/variance guidance — pass the input through rather than
|
|
371
|
-
// producing an unguided blur.
|
|
372
|
-
if ( ! colorTex || ! ndTex || ! albedoTex || ! varTex ) {
|
|
373
|
-
|
|
374
|
-
if ( colorTex ) context.setTexture( 'edgeFiltering:output', colorTex );
|
|
375
|
-
return;
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if ( context.getState( 'interactionMode' ) ) {
|
|
380
|
-
|
|
381
|
-
context.setTexture( 'edgeFiltering:output', colorTex );
|
|
382
|
-
return;
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const img = colorTex.image;
|
|
387
|
-
if ( img && img.width > 0 && img.height > 0 ) {
|
|
388
|
-
|
|
389
|
-
if ( img.width !== this.outputTarget.width ||
|
|
390
|
-
img.height !== this.outputTarget.height ) {
|
|
391
|
-
|
|
392
|
-
this.setSize( img.width, img.height );
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
this.varianceHistoryU.value = context.getState( 'accumulatedFrames' ) ?? 0;
|
|
399
|
-
|
|
400
|
-
this._colorTexNode.value = colorTex;
|
|
401
|
-
this._ndTexNode.value = ndTex;
|
|
402
|
-
this._snTexNode.value = snTex;
|
|
403
|
-
this._albedoTexNode.value = albedoTex;
|
|
404
|
-
this._varTexNode.value = varTex;
|
|
405
|
-
|
|
406
|
-
// First-frame compile while _readTexNode still holds EmptyTexture — codegen
|
|
407
|
-
// then emits textureLoad with the level parameter the runtime requires for
|
|
408
|
-
// non-zero StorageTexture reads. The throwaway dispatches also initialise the
|
|
409
|
-
// ping-pong textures. _readTexNode must NOT be assigned before this.
|
|
410
|
-
if ( ! this._compiled ) {
|
|
411
|
-
|
|
412
|
-
this.renderer.compute( this._computeDemod );
|
|
413
|
-
this.renderer.compute( this._computeAtrousA );
|
|
414
|
-
this.renderer.compute( this._computeAtrousB );
|
|
415
|
-
this._compiled = true;
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// 1. Demodulate → _demodTex.
|
|
420
|
-
this.renderer.compute( this._computeDemod );
|
|
421
|
-
|
|
422
|
-
// 2. À-trous iterations: step 2^i, ping-pong write direction. First pass reads
|
|
423
|
-
// _demodTex; the last pass remodulates + strength-blends.
|
|
424
|
-
let readTex = this._demodTex;
|
|
425
|
-
let writeNode = this._computeAtrousA;
|
|
426
|
-
let nextWriteNode = this._computeAtrousB;
|
|
427
|
-
|
|
428
|
-
for ( let i = 0; i < this.iterations; i ++ ) {
|
|
429
|
-
|
|
430
|
-
this.stepSizeU.value = 1 << i;
|
|
431
|
-
this.isLastIterationU.value = ( i === this.iterations - 1 ) ? 1 : 0;
|
|
432
|
-
this.isFirstIterationU.value = i === 0 ? 1 : 0;
|
|
433
|
-
this._readTexNode.value = readTex;
|
|
434
|
-
|
|
435
|
-
this.renderer.compute( writeNode );
|
|
436
|
-
|
|
437
|
-
readTex = ( writeNode === this._computeAtrousA )
|
|
438
|
-
? this._storageTexA
|
|
439
|
-
: this._storageTexB;
|
|
440
|
-
|
|
441
|
-
const tmp = writeNode;
|
|
442
|
-
writeNode = nextWriteNode;
|
|
443
|
-
nextWriteNode = tmp;
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Copy the final result out of the over-allocated StorageTexture into the
|
|
448
|
-
// active-size RenderTarget; downstream stages UV-sample the latter.
|
|
449
|
-
this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
|
|
450
|
-
this.renderer.copyTextureToTexture( readTex, this.outputTarget.texture, this._srcRegion );
|
|
451
|
-
|
|
452
|
-
context.setTexture( 'edgeFiltering:output', this.outputTarget.texture );
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
setFilteringEnabled( enabled ) {
|
|
457
|
-
|
|
458
|
-
this.enabled = enabled;
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
updateUniforms( params ) {
|
|
463
|
-
|
|
464
|
-
if ( ! params ) return;
|
|
465
|
-
if ( params.filterStrength !== undefined ) this.filterStrength.value = params.filterStrength;
|
|
466
|
-
if ( params.phiLuminance !== undefined ) this.phiLuminance.value = params.phiLuminance;
|
|
467
|
-
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
468
|
-
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
469
|
-
if ( params.atrousIterations !== undefined ) this.iterations = params.atrousIterations;
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
|
|
474
|
-
// after re-enable (no temporal state to re-anchor). See ASVGF.releaseGPUMemory.
|
|
475
|
-
releaseGPUMemory() {
|
|
476
|
-
|
|
477
|
-
this._demodTex?.dispose();
|
|
478
|
-
this._storageTexA?.dispose();
|
|
479
|
-
this._storageTexB?.dispose();
|
|
480
|
-
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
481
|
-
this.context?.removeTexture( 'edgeFiltering:output' );
|
|
482
|
-
this.outputTarget?.texture?.dispose();
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
reset() {
|
|
487
|
-
|
|
488
|
-
// No per-frame or temporal state — variance guidance is owned by the Variance stage.
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
setSize( width, height ) {
|
|
493
|
-
|
|
494
|
-
// StorageTextures stay at their max allocation (see constructor).
|
|
495
|
-
this.outputTarget.setSize( width, height );
|
|
496
|
-
this.outputTarget.texture.needsUpdate = true;
|
|
497
|
-
this.resW.value = width;
|
|
498
|
-
this.resH.value = height;
|
|
499
|
-
|
|
500
|
-
this._dispatchX = Math.ceil( width / 16 );
|
|
501
|
-
this._dispatchY = Math.ceil( height / 16 );
|
|
502
|
-
const size = [ this._dispatchX, this._dispatchY, 1 ];
|
|
503
|
-
this._computeDemod.dispatchSize = size;
|
|
504
|
-
this._computeAtrousA.dispatchSize = size;
|
|
505
|
-
this._computeAtrousB.dispatchSize = size;
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Reserved-storage change (e.g. 4K toggle): recreate the MAX-preallocated StorageTextures at the new live
|
|
510
|
-
// reserved size + rebuild the compute nodes in place. Same stage object, so refs/wiring stay valid.
|
|
511
|
-
reallocateReservedStorage() {
|
|
512
|
-
|
|
513
|
-
this._computeDemod?.dispose();
|
|
514
|
-
this._computeAtrousA?.dispose();
|
|
515
|
-
this._computeAtrousB?.dispose();
|
|
516
|
-
this._demodTex?.dispose();
|
|
517
|
-
this._storageTexA?.dispose();
|
|
518
|
-
this._storageTexB?.dispose();
|
|
519
|
-
this._demodTex = this._makeStorageTex();
|
|
520
|
-
this._storageTexA = this._makeStorageTex();
|
|
521
|
-
this._storageTexB = this._makeStorageTex();
|
|
522
|
-
this._buildCompute();
|
|
523
|
-
this._compiled = false;
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
dispose() {
|
|
528
|
-
|
|
529
|
-
this._computeDemod?.dispose();
|
|
530
|
-
this._computeAtrousA?.dispose();
|
|
531
|
-
this._computeAtrousB?.dispose();
|
|
532
|
-
this._demodTex?.dispose();
|
|
533
|
-
this._storageTexA?.dispose();
|
|
534
|
-
this._storageTexB?.dispose();
|
|
535
|
-
this.outputTarget?.dispose();
|
|
536
|
-
this._colorTexNode?.dispose();
|
|
537
|
-
this._ndTexNode?.dispose();
|
|
538
|
-
this._snTexNode?.dispose();
|
|
539
|
-
this._albedoTexNode?.dispose();
|
|
540
|
-
this._varTexNode?.dispose();
|
|
541
|
-
this._readTexNode?.dispose();
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
}
|