rayzee 7.22.4 → 7.22.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.22.4",
3
+ "version": "7.22.6",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -18,6 +18,9 @@ const MISS_THRESHOLD = 6e4;
18
18
  const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
19
19
  const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
20
20
 
21
+ // Frames before the temporal EMA variance is trusted on its own (Schied 2017 §4.2).
22
+ const VARIANCE_HISTORY_MIN = 4;
23
+
21
24
  // 5×5 à-trous kernel weights (B3-spline / Gaussian approx, Σ = 1.0).
22
25
  const ATROUS_KERNEL = [
23
26
  1.0 / 256.0, 4.0 / 256.0, 6.0 / 256.0, 4.0 / 256.0, 1.0 / 256.0,
@@ -66,7 +69,9 @@ export class EdgeFilter extends RenderStage {
66
69
  // phiNormal: normal cone exponent (bigger = tighter, preserves more edges).
67
70
  // phiDepth: RELATIVE depth tolerance (fraction of ray distance, scale-invariant).
68
71
  this.filterStrength = uniform( options.filterStrength ?? 1.0 );
69
- this.phiLuminance = uniform( options.phiLuminance ?? 4.0 );
72
+ // 1.0, not the old 4.0, which was tuned against the non-converging variance seed below
73
+ // and now over-blurs enough to lose to no denoising at all from ~16spp up.
74
+ this.phiLuminance = uniform( options.phiLuminance ?? 1.0 );
70
75
  this.phiNormal = uniform( options.phiNormal ?? 64.0 );
71
76
  this.phiDepth = uniform( options.phiDepth ?? 0.1 );
72
77
  this.iterations = options.atrousIterations ?? options.edgeAtrousIterations ?? 5;
@@ -75,6 +80,7 @@ export class EdgeFilter extends RenderStage {
75
80
  this.isLastIterationU = uniform( 0, 'int' );
76
81
  // 1 on pass 0 → seed variance from variance:output; later passes read the carried value.
77
82
  this.isFirstIterationU = uniform( 1, 'int' );
83
+ this.varianceHistoryU = uniform( 0, 'int' );
78
84
  this.resW = uniform( options.width || 1 );
79
85
  this.resH = uniform( options.height || 1 );
80
86
 
@@ -205,6 +211,7 @@ export class EdgeFilter extends RenderStage {
205
211
  const stepSize = this.stepSizeU;
206
212
  const isLastIterationU = this.isLastIterationU;
207
213
  const isFirstIterationU = this.isFirstIterationU;
214
+ const varianceHistoryU = this.varianceHistoryU;
208
215
  const resW = this.resW;
209
216
  const resH = this.resH;
210
217
 
@@ -233,19 +240,23 @@ export class EdgeFilter extends RenderStage {
233
240
  const demodAlbedo = select( centerIsHitBool, max( centerAlbedo, vec3( ALBEDO_EPS ) ), vec3( 1.0 ) );
234
241
  const centerAlbedoLum = max( luminance( demodAlbedo ), float( ALBEDO_EPS ) );
235
242
 
236
- // Variance-guided luminance σ (SVGF). .z = temporal, .w = spatial variance;
237
- // max() falls back to the spatial estimate as temporal collapses on a
238
- // converging accumulation buffer. Dividing by albedoLum rescales the
239
- // modulated-space variance into demodulated space (std ∝ 1/albedo).
243
+ // Variance-guided luminance σ (SVGF). .z = temporal, .w = spatial variance.
244
+ // Temporal only, once the EMA is warm: .w is the 3×3 contrast of the
245
+ // *accumulated* image, so it plateaus at scene detail instead of decaying with
246
+ // sample count. max()-ing it in pinned sigma_l wide forever, which left the
247
+ // geometric gates — binary reads off the jitter-free G-buffer — to snap every
248
+ // antialiased silhouette pixel to one side, destroying sub-pixel AA.
240
249
  //
241
- // Filtered alongside colour and carried between passes as √variance in the
242
- // ping-pong's spare alpha, so sigma_l tightens as the signal smooths. Stored as
243
- // the root because converged variance (~1e-6) is a half-float subnormal.
250
+ // Dividing by albedoLum rescales the modulated-space variance into demodulated
251
+ // space (std ∝ 1/albedo). Filtered alongside colour and carried between passes
252
+ // as √variance in the ping-pong's spare alpha, so sigma_l tightens as the signal
253
+ // smooths. Stored as the root because converged variance is a half-float subnormal.
244
254
  const isFirst = isFirstIterationU.equal( int( 1 ) );
255
+ const historyShort = varianceHistoryU.lessThan( int( VARIANCE_HISTORY_MIN ) );
245
256
  const seedVariance = ( at ) => {
246
257
 
247
258
  const v = textureLoad( varTex, at );
248
- return max( v.z, v.w );
259
+ return select( historyShort, max( v.z, v.w ), v.z );
249
260
 
250
261
  };
251
262
 
@@ -383,6 +394,8 @@ export class EdgeFilter extends RenderStage {
383
394
 
384
395
  }
385
396
 
397
+ this.varianceHistoryU.value = context.getState( 'accumulatedFrames' ) ?? 0;
398
+
386
399
  this._colorTexNode.value = colorTex;
387
400
  this._ndTexNode.value = ndTex;
388
401
  this._snTexNode.value = snTex;
@@ -356,7 +356,11 @@ export const handleRussianRoulette = Fn( ( [
356
356
  rrProb.mulAssign( depthFactor );
357
357
 
358
358
  const minProb = select( mc.isEmissive, float( 0.04 ), float( 0.02 ) );
359
- rrProb.assign( max( rrProb, minProb ) );
359
+ // Upper clamp is load-bearing, not cosmetic: pathContribution scales with throughput,
360
+ // which prior RR compensation inflates well past 1, so rrProb could exceed 1. The path
361
+ // then always survives while throughput is still divided by rrProb — a plain energy
362
+ // sink that compounds with depth (albedo-1 concave furnace read 0.955 instead of 1.0).
363
+ rrProb.assign( clamp( rrProb, minProb, 1.0 ) );
360
364
 
361
365
  const rrSample = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame );
362
366
  result.assign( select( rrSample.lessThan( rrProb ), rrProb, float( 0.0 ) ) );