rayzee 6.2.0 → 6.3.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.
@@ -201,8 +201,8 @@ export class AdaptiveSampling extends RenderStage {
201
201
  } );
202
202
 
203
203
  // Dispatch dimensions
204
- this._dispatchX = Math.ceil( w / 16 );
205
- this._dispatchY = Math.ceil( h / 16 );
204
+ this._dispatchX = Math.ceil( w / 8 );
205
+ this._dispatchY = Math.ceil( h / 8 );
206
206
 
207
207
  // Input: variance texture from Variance
208
208
  // Use regular TextureNode (not StorageTexture) as compile-time placeholder so
@@ -243,7 +243,7 @@ export class AdaptiveSampling extends RenderStage {
243
243
  const resH = this.resolutionHeight;
244
244
  const outputTex = this._outputStorageTex;
245
245
 
246
- const WG_SIZE = 16;
246
+ const WG_SIZE = 8;
247
247
 
248
248
  const computeFn = Fn( () => {
249
249
 
@@ -300,7 +300,7 @@ export class AdaptiveSampling extends RenderStage {
300
300
  const resW = this.resolutionWidth;
301
301
  const resH = this.resolutionHeight;
302
302
 
303
- const WG_SIZE = 16;
303
+ const WG_SIZE = 8;
304
304
 
305
305
  const computeFn = Fn( () => {
306
306
 
@@ -406,8 +406,8 @@ export class AdaptiveSampling extends RenderStage {
406
406
  this.resolutionHeight.value = height;
407
407
 
408
408
  // Update dispatch dimensions
409
- this._dispatchX = Math.ceil( width / 16 );
410
- this._dispatchY = Math.ceil( height / 16 );
409
+ this._dispatchX = Math.ceil( width / 8 );
410
+ this._dispatchY = Math.ceil( height / 8 );
411
411
  this._computeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
412
412
  this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
413
413
 
@@ -1,18 +1,16 @@
1
- import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max,
1
+ import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, sqrt,
2
2
  textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
3
3
  import { TextureNode, StorageTexture } from 'three/webgpu';
4
4
  import { HalfFloatType, RGBAFormat, LinearFilter } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { luminance } from '../TSL/Common.js';
7
+ import { ALBEDO_EPS } from '../EngineDefaults.js';
7
8
 
8
- // ── wgslFn helpers ──────────────────────────────────────────
9
-
10
- /**
11
- * Bilateral edge-stopping weight.
12
- *
13
- * Combines luminance, normal, depth, and color similarity into
14
- * a single weight multiplied by the kernel weight.
15
- */
9
+ // SVGF bilateral edge-stopping weight. All three φ params are relative
10
+ // tolerances (unitless fractions) so the filter is scale-invariant across
11
+ // scenes, HDR ranges, and camera distances. sigmaL is precomputed by the
12
+ // caller as phiLum * √variance / albedoLum + ε, compensating for the
13
+ // 1/albedo noise amplification introduced by demodulation.
16
14
  const bilateralWeight = /*@__PURE__*/ wgslFn( `
17
15
  fn bilateralWeight(
18
16
  centerLum: f32, sLum: f32,
@@ -20,48 +18,34 @@ const bilateralWeight = /*@__PURE__*/ wgslFn( `
20
18
  centerDepth: f32, sDepth: f32,
21
19
  centerColor: vec3f, sColor: vec3f,
22
20
  kernelW: f32,
23
- phiLum: f32, phiNorm: f32, phiDep: f32, phiCol: f32
21
+ sigmaL: f32, phiNorm: f32, phiDep: f32, phiCol: f32
24
22
  ) -> f32 {
25
23
 
26
- let lumW = exp( -abs( centerLum - sLum ) * phiLum );
27
- // clamp dot to [0,1] not just max(., 0): miss-ray normals decode to
28
- // non-unit (-1,-1,-1) with dot=3, which would saturate pow to +inf
29
- // and poison output via inf*0 = NaN. See project_tsl_pitfalls memory.
24
+ let lumW = exp( -abs( centerLum - sLum ) / sigmaL );
25
+ // clamp dot to [0,1]: miss-ray normals decode to (-1,-1,-1) with
26
+ // dot=3 pow saturates to +inf → inf*0 = NaN. See project_tsl_pitfalls.
30
27
  let normW = pow( clamp( dot( centerNormal, sNormal ), 0.0, 1.0 ), phiNorm );
31
- let depW = exp( -abs( centerDepth - sDepth ) / max( phiDep, 0.001 ) );
28
+ let depW = exp( -abs( centerDepth - sDepth ) / max( centerDepth * phiDep, 0.001 ) );
32
29
  let maxDiff = max( max( abs( centerColor.x - sColor.x ),
33
30
  abs( centerColor.y - sColor.y ) ),
34
31
  abs( centerColor.z - sColor.z ) );
35
- let colW = exp( -maxDiff * phiCol );
32
+ let avgLum = max( ( centerLum + sLum ) * 0.5, 0.0001 );
33
+ let colW = exp( -( maxDiff / avgLum ) / max( phiCol, 0.0001 ) );
36
34
  return kernelW * lumW * normW * depW * colW;
37
35
 
38
36
  }
39
37
  ` );
40
38
 
41
39
  /**
42
- * WebGPU Bilateral Filtering Stage (Compute Shader)
43
- *
44
- * Edge-aware A-trous wavelet filter for spatial denoising.
45
- * Runs multiple iterations with increasing step size (2^i),
46
- * ping-ponging between two StorageTextures.
47
- *
48
- * Algorithm:
49
- * 1. textureLoad center pixel (color + normalDepth)
50
- * 2. Unrolled 5×5 a-trous kernel with edge-stopping weights
51
- * 3. Normalize accumulated color
52
- * 4. textureStore filtered result
53
- * 5. Repeat for 4 iterations (step sizes 1, 2, 4, 8)
54
- *
55
- * Edge-stopping functions:
56
- * - Luminance: exp(-|ΔL| * σ_l)
57
- * - Normal: dot(n1,n2)^σ_n
58
- * - Depth: exp(-|Δz| / σ_z)
59
- * - Color: exp(-maxDiff * σ_c)
40
+ * BilateralFilter 5×5 à-trous wavelet, edge-preserving, multi-iteration.
60
41
  *
61
- * Execution: ALWAYS
42
+ * Reads asvgf:demodulated (lighting), filters in demodulated space across
43
+ * `iterations` ping-pong passes with step size 2^i, multiplies by albedo on
44
+ * the final pass to remodulate. φ params are relative tolerances.
62
45
  *
63
- * Textures published: bilateralFiltering:output
64
- * Textures read: configurable color input + pathtracer:normalDepth
46
+ * Publishes: bilateralFiltering:output (modulated)
47
+ * Reads: asvgf:demodulated (or fallback), pathtracer:normalDepth,
48
+ * pathtracer:albedo, variance:output
65
49
  */
66
50
  export class BilateralFilter extends RenderStage {
67
51
 
@@ -73,29 +57,33 @@ export class BilateralFilter extends RenderStage {
73
57
  } );
74
58
 
75
59
  this.renderer = renderer;
76
- this.inputTextureName = options.inputTextureName || 'asvgf:output';
60
+ this.inputTextureName = options.inputTextureName || 'asvgf:demodulated';
77
61
  this.normalDepthTextureName = options.normalDepthTextureName || 'pathtracer:normalDepth';
62
+ this.albedoTextureName = options.albedoTextureName || 'pathtracer:albedo';
63
+ this.varianceTextureName = options.varianceTextureName || 'variance:output';
78
64
  this.iterations = options.iterations ?? 4;
79
65
 
80
- // Edge-stopping parameters
81
- this.phiColor = uniform( options.phiColor ?? 10.0 );
66
+ // All φ are relative tolerances (fractions of mean/depth). Bigger =
67
+ // more permissive blending across edges.
68
+ this.phiColor = uniform( options.phiColor ?? 0.5 );
82
69
  this.phiNormal = uniform( options.phiNormal ?? 128.0 );
83
- this.phiDepth = uniform( options.phiDepth ?? 1.0 );
70
+ this.phiDepth = uniform( options.phiDepth ?? 0.05 );
84
71
  this.phiLuminance = uniform( options.phiLuminance ?? 4.0 );
85
72
  this.stepSizeU = uniform( 1, 'int' );
73
+ // 1 on the final iteration → multiply by albedo to remodulate.
74
+ this.isLastIterationU = uniform( 0, 'int' );
86
75
  this.resW = uniform( options.width || 1 );
87
76
  this.resH = uniform( options.height || 1 );
88
77
 
89
- // Input texture nodes
90
78
  this._readTexNode = new TextureNode();
91
79
  this._normalDepthTexNode = new TextureNode();
80
+ this._albedoTexNode = new TextureNode();
81
+ this._varianceTexNode = new TextureNode();
92
82
 
93
- // Ping-pong StorageTextures
94
83
  const w = options.width || 1;
95
84
  const h = options.height || 1;
96
85
 
97
- // LinearFilter so textureLoad codegen includes required level parameter
98
- // when _readTexNode.value is later set to a StorageTexture
86
+ // LinearFilter required for textureLoad codegen on StorageTextures.
99
87
  this._storageTexA = new StorageTexture( w, h );
100
88
  this._storageTexA.type = HalfFloatType;
101
89
  this._storageTexA.format = RGBAFormat;
@@ -110,7 +98,6 @@ export class BilateralFilter extends RenderStage {
110
98
 
111
99
  this._compiled = false;
112
100
 
113
- // Dispatch dimensions
114
101
  this._dispatchX = Math.ceil( w / 8 );
115
102
  this._dispatchY = Math.ceil( h / 8 );
116
103
 
@@ -118,15 +105,7 @@ export class BilateralFilter extends RenderStage {
118
105
 
119
106
  }
120
107
 
121
- /**
122
- * Build two compute nodes — one for each ping-pong write direction.
123
- *
124
- * _computeNodeA: writes to StorageTexA, reads from _readTexNode
125
- * _computeNodeB: writes to StorageTexB, reads from _readTexNode
126
- *
127
- * Read-side texture wrapped in TextureNode so compile-time type is
128
- * regular Texture (avoids Three.js WGSL textureLoad codegen bug).
129
- */
108
+ // One compute node per ping-pong write direction.
130
109
  _buildCompute() {
131
110
 
132
111
  this._computeNodeA = this._buildComputeForDirection( this._storageTexA );
@@ -138,11 +117,14 @@ export class BilateralFilter extends RenderStage {
138
117
 
139
118
  const readTexNode = this._readTexNode;
140
119
  const ndTexNode = this._normalDepthTexNode;
120
+ const albedoTexNode = this._albedoTexNode;
121
+ const varTexNode = this._varianceTexNode;
141
122
  const phiColor = this.phiColor;
142
123
  const phiNormal = this.phiNormal;
143
124
  const phiDepth = this.phiDepth;
144
125
  const phiLuminance = this.phiLuminance;
145
126
  const stepSize = this.stepSizeU;
127
+ const isLastIterationU = this.isLastIterationU;
146
128
  const resW = this.resW;
147
129
  const resH = this.resH;
148
130
 
@@ -165,18 +147,29 @@ export class BilateralFilter extends RenderStage {
165
147
  If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
166
148
 
167
149
  const coord = ivec2( gx, gy );
168
-
169
- // Centre sample
170
150
  const centerColor = textureLoad( readTexNode, coord ).xyz;
171
151
  const centerND = textureLoad( ndTexNode, coord );
172
152
  const centerNormal = centerND.xyz.mul( 2.0 ).sub( 1.0 );
173
153
  const centerDepth = centerND.w;
174
154
  const centerLum = luminance( centerColor );
155
+ const centerSafeAlbedo = max( textureLoad( albedoTexNode, coord ).xyz, vec3( ALBEDO_EPS ) );
156
+ const centerAlbedoLum = max( luminance( centerSafeAlbedo ), float( ALBEDO_EPS ) ).toVar();
157
+
158
+ // sigma_l = phiLum * √variance / albedoLum + ε. Dividing by
159
+ // albedoLum compensates for the 1/albedo noise amplification
160
+ // from demodulation — otherwise dark materials get an
161
+ // under-estimated sigma → over-strict luminance gate → no
162
+ // blending → silhouette dark-outline artifact.
163
+ const variance = textureLoad( varTexNode, coord ).z;
164
+ const sigmaL = phiLuminance
165
+ .mul( sqrt( max( variance, float( 0.0 ) ) ) )
166
+ .div( centerAlbedoLum )
167
+ .add( float( 0.0001 ) );
175
168
 
176
169
  const colorSum = vec3( 0.0 ).toVar();
177
170
  const weightSum = float( 0.0 ).toVar();
178
171
 
179
- // Unrolled 5×5 a-trous kernel
172
+ // 5×5 à-trous kernel (Gaussian-approx, Σ=1)
180
173
  for ( let iy = 0; iy < 5; iy ++ ) {
181
174
 
182
175
  for ( let ix = 0; ix < 5; ix ++ ) {
@@ -202,7 +195,7 @@ export class BilateralFilter extends RenderStage {
202
195
  centerDepth, sDepth,
203
196
  centerColor, sColor,
204
197
  float( kw ),
205
- phiLuminance, phiNormal, phiDepth, phiColor
198
+ sigmaL, phiNormal, phiDepth, phiColor
206
199
  );
207
200
 
208
201
  colorSum.addAssign( sColor.mul( w ) );
@@ -214,10 +207,15 @@ export class BilateralFilter extends RenderStage {
214
207
 
215
208
  const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
216
209
 
210
+ // Remodulate by albedo only on the final iteration so the
211
+ // inner ping-pong stays in demodulated space.
212
+ const isLast = isLastIterationU.equal( int( 1 ) );
213
+ const output = isLast.select( filtered.mul( centerSafeAlbedo ), filtered );
214
+
217
215
  textureStore(
218
216
  writeStorageTex,
219
217
  uvec2( uint( gx ), uint( gy ) ),
220
- vec4( filtered, 1.0 )
218
+ vec4( output, 1.0 )
221
219
  ).toWriteOnly();
222
220
 
223
221
  } );
@@ -236,12 +234,14 @@ export class BilateralFilter extends RenderStage {
236
234
  if ( ! this.enabled ) return;
237
235
 
238
236
  const inputTex = context.getTexture( this.inputTextureName )
237
+ || context.getTexture( 'asvgf:output' )
239
238
  || context.getTexture( 'pathtracer:color' );
240
239
  const ndTex = context.getTexture( this.normalDepthTextureName );
240
+ const albedoTex = context.getTexture( this.albedoTextureName );
241
+ const varTex = context.getTexture( this.varianceTextureName );
241
242
 
242
243
  if ( ! inputTex ) return;
243
244
 
244
- // Auto-size
245
245
  const img = inputTex.image;
246
246
  if ( img && img.width > 0 && img.height > 0 ) {
247
247
 
@@ -254,12 +254,13 @@ export class BilateralFilter extends RenderStage {
254
254
 
255
255
  }
256
256
 
257
- // Set normalDepth (may be null shader handles gracefully)
257
+ // RenderTarget textures safe to bind before first-compile.
258
258
  if ( ndTex ) this._normalDepthTexNode.value = ndTex;
259
+ if ( albedoTex ) this._albedoTexNode.value = albedoTex;
259
260
 
260
- // Force-compile both compute nodes on first frame while _readTexNode
261
- // still holds EmptyTexture. This ensures WGSLNodeBuilder.generateTextureLoad()
262
- // sees isStorageTexture=false and emits the required level parameter.
261
+ // First-frame compile while StorageTexture-typed nodes still hold
262
+ // EmptyTexture codegen then emits textureLoad with the level
263
+ // parameter, which the runtime requires for non-zero reads.
263
264
  if ( ! this._compiled ) {
264
265
 
265
266
  this.renderer.compute( this._computeNodeA );
@@ -268,7 +269,10 @@ export class BilateralFilter extends RenderStage {
268
269
 
269
270
  }
270
271
 
271
- // Iteration dispatch: ping-pong between StorageTexA and StorageTexB
272
+ if ( varTex ) this._varianceTexNode.value = varTex;
273
+
274
+ // À-trous iterations: step size 2^i, ping-pong write direction.
275
+ // Last iteration multiplies by albedo to remodulate.
272
276
  let readTex = inputTex;
273
277
  let writeNode = this._computeNodeA;
274
278
  let nextWriteNode = this._computeNodeB;
@@ -277,26 +281,36 @@ export class BilateralFilter extends RenderStage {
277
281
 
278
282
  this.stepSizeU.value = 1 << i;
279
283
  this._readTexNode.value = readTex;
284
+ this.isLastIterationU.value = ( i === this.iterations - 1 ) ? 1 : 0;
280
285
 
281
286
  this.renderer.compute( writeNode );
282
287
 
283
- // Next iteration reads from what we just wrote
284
288
  readTex = ( writeNode === this._computeNodeA )
285
289
  ? this._storageTexA
286
290
  : this._storageTexB;
287
291
 
288
- // Swap write direction
289
292
  const tmp = writeNode;
290
293
  writeNode = nextWriteNode;
291
294
  nextWriteNode = tmp;
292
295
 
293
296
  }
294
297
 
295
- // Publish final output (last written StorageTexture)
296
298
  context.setTexture( 'bilateralFiltering:output', readTex );
297
299
 
298
300
  }
299
301
 
302
+ // Accepts the same keys as ASVGF presets; unknown keys ignored.
303
+ updateParameters( params ) {
304
+
305
+ if ( ! params ) return;
306
+ if ( params.phiColor !== undefined ) this.phiColor.value = params.phiColor;
307
+ if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
308
+ if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
309
+ if ( params.phiLuminance !== undefined ) this.phiLuminance.value = params.phiLuminance;
310
+ if ( params.atrousIterations !== undefined ) this.iterations = params.atrousIterations;
311
+
312
+ }
313
+
300
314
  setSize( width, height ) {
301
315
 
302
316
  this._storageTexA.setSize( width, height );
@@ -326,6 +340,8 @@ export class BilateralFilter extends RenderStage {
326
340
  this._storageTexB?.dispose();
327
341
  this._readTexNode?.dispose();
328
342
  this._normalDepthTexNode?.dispose();
343
+ this._albedoTexNode?.dispose();
344
+ this._varianceTexNode?.dispose();
329
345
 
330
346
  }
331
347
 
@@ -57,6 +57,7 @@ export class Compositor extends RenderStage {
57
57
 
58
58
  return context.getTexture( 'bloom:output' )
59
59
  || context.getTexture( 'edgeFiltering:output' )
60
+ || context.getTexture( 'bilateralFiltering:output' )
60
61
  || context.getTexture( 'asvgf:output' )
61
62
  || context.getTexture( 'ssrc:output' )
62
63
  || context.getTexture( 'pathtracer:color' );