rayzee 7.22.3 → 7.22.4

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.
@@ -9,7 +9,7 @@
9
9
 
10
10
  import {
11
11
  Fn, wgslFn, float, vec2, vec4, int, uint, uvec2,
12
- If, mix, select, texture, textureStore, length, atomicAdd,
12
+ If, mix, select, texture, textureStore, length,
13
13
  localId, workgroupId,
14
14
  } from 'three/tsl';
15
15
 
@@ -17,7 +17,6 @@ import {
17
17
  readRayRadiance, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
18
18
  } from '../Processor/PackedRayBuffer.js';
19
19
  import { luminance } from './Common.js';
20
- import { COUNTER } from '../Processor/QueueManager.js';
21
20
 
22
21
  const WG_SIZE = 16;
23
22
 
@@ -49,12 +48,17 @@ export function buildFinalWriteKernel( params ) {
49
48
  // Clean-aux normal (1 = temporally accumulate + renormalize the aux normal). On only for clean-aux
50
49
  // OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
51
50
  cleanAuxNormalEnabled,
52
- // Tier-1 convergence early-stop: per-pixel Welford luminance second moment + converged-pixel counter.
53
- counters, m2BufferRW, useAdaptiveSampling, noiseThreshold, adaptiveMinSamples,
51
+ // Tier-1 convergence early-stop: per-pixel Welford luminance second moment + converged bit (stamped
52
+ // into streak bit 31; counted with 3×3 erosion by countConvergedDilated at the next frame start).
53
+ m2BufferRW, useAdaptiveSampling, noiseThreshold, adaptiveMinSamples,
54
54
 
55
- // Tier-2 per-pixel freeze: streakBufferRW = per-pixel freeze-candidate streak (stamped here);
56
- // frozenMaskRO = dilated frozen mask from buildActivePixels (pass-through gates on it, not streak).
55
+ // Tier-2 per-pixel freeze: streakBufferRW = per-pixel freeze-candidate streak in bits 0-30 (stamped
56
+ // here); frozenMaskRO = dilated frozen mask from buildActivePixels (pass-through gates on it, not streak).
57
57
  usePixelFreeze, pixelFreezeThreshold, streakBufferRW, frozenMaskRO,
58
+
59
+ // Display-only convergence overlay (Compositor). Only keeps m2 updated so the overlay has a live
60
+ // error field with adaptive sampling off — the counter and the freeze streak stay under convOn.
61
+ convergenceOverlay,
58
62
  } = params;
59
63
 
60
64
  const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
@@ -62,6 +66,9 @@ export function buildFinalWriteKernel( params ) {
62
66
  // useAdaptiveSampling is registered via UniformManager.ub() → an int 0/1 uniform, so compare against int(0).
63
67
  const convOn = useAdaptiveSampling.greaterThan( int( 0 ) );
64
68
  const adaptiveOn = usePixelFreeze.greaterThan( int( 0 ) );
69
+ // The m2/relErr block feeds three consumers: the frame early-stop, the per-pixel freeze, and the debug
70
+ // overlay. Any one of them being on has to keep it live.
71
+ const statsOn = convOn.or( adaptiveOn ).or( convergenceOverlay.greaterThan( int( 0 ) ) );
65
72
 
66
73
  const computeFn = Fn( () => {
67
74
 
@@ -139,13 +146,10 @@ export function buildFinalWriteKernel( params ) {
139
146
 
140
147
  } );
141
148
 
142
- // Tier-1 convergence: per-pixel running second moment of LUMINANCE (Welford). luminance() is linear,
143
- // so luminance(running-mean color) == running-mean luminance == E[L]; m2 tracks E[L²] under the SAME
144
- // global 1/(frame+1) alpha (NO per-pixel alpha). sampleVar = E[L²]-E[L]²; varOfMean = sampleVar/(N);
145
- // absSE = SE(mean); a pixel converges once frame>=minSamples AND the √-normalized error < threshold.
146
- // The m2 write runs every frame (incl. frame 0, where alpha==1 self-inits it → no explicit clear).
147
- // Sits AFTER the accumulation mix (finalColor is the mean) and BEFORE the visMode-11 mutation.
148
- If( convOn, () => {
149
+ // Per-pixel running second moment of luminance under the SAME global 1/(frame+1) alpha as the
150
+ // colour, so sampleVar = E[L²]-E[L]². Must sit after the accumulation mix (finalColor is the mean)
151
+ // and before the visMode-11 mutation. Frame 0 self-inits m2 via alpha==1 — no explicit clear.
152
+ If( statsOn, () => {
149
153
 
150
154
  const sampleLum = luminance( sampleColor.xyz );
151
155
  const prevM2 = m2BufferRW.element( pixelId ).toVar();
@@ -155,44 +159,67 @@ export function buildFinalWriteKernel( params ) {
155
159
  const meanLum = luminance( finalColor ).toVar();
156
160
  const sampleVar = m2.sub( meanLum.mul( meanLum ) ).max( float( 0 ) );
157
161
  const varOfMean = sampleVar.div( float( frame ).add( 1.0 ) );
158
- // absSE = absolute standard error of the mean luminance; relErr = its ratio to the mean.
159
- // Combined criterion: bright pixels converge on relErr<threshold; dark/dim pixels (where relErr
160
- // stays high forever) converge on absSE<absFloor, since their absolute noise is imperceptible.
161
162
  const absSE = varOfMean.sqrt().toVar();
162
163
  const relErr = absSE.div( meanLum.add( float( 1e-4 ) ) ); // Tier-2 freeze predicate reads this
163
164
 
164
- // Cycles-style convergence: normalize the standard error by √luminance for dim pixels (lum<1),
165
- // by luminance for bright ones. The √ loosens the relative bar in shadows so dark pixels converge
166
- // on their small absolute noise instead of stalling forever on a pure relative bar.
165
+ // √luminance normalization below 1 loosens the bar in shadows, so dark pixels converge on their
166
+ // small absolute noise instead of stalling forever on a pure relative one.
167
167
  const convNorm = select( meanLum.lessThan( float( 1.0 ) ), meanLum.sqrt(), meanLum );
168
168
  const converged = absSE.div( convNorm.add( float( 1e-4 ) ) ).lessThan( noiseThreshold );
169
169
 
170
- If( frame.greaterThanEqual( uint( adaptiveMinSamples ) ).and( converged ), () => {
170
+ // Must cover adaptiveOn: nested under convOn alone, freeze neither updated its streak nor ran
171
+ // its frame-0 clear, so buildActivePixels kept applying the last run's mask across resets.
172
+ If( convOn.or( adaptiveOn ), () => {
171
173
 
172
- atomicAdd( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 1 ) );
174
+ // Streak word: bit 31 = converged, bit 30 = hit geometry, bits 0-29 = freeze streak. Both flags
175
+ // are read by countConvergedDilated, which erodes them 3×3 before counting — a pointwise count is
176
+ // blind to undiscovered energy, which has zero variance and so passes any threshold.
177
+ const eligible = convOn.and( frame.greaterThanEqual( uint( adaptiveMinSamples ) ) ).and( converged );
178
+ const prevWord = streakBufferRW.element( pixelId ).toVar();
179
+ const oldStreak = prevWord.bitAnd( uint( 0x3FFFFFFF ) ).toVar();
180
+ const newStreak = oldStreak.toVar();
173
181
 
174
- } );
182
+ // Sticky within a run, else silhouette pixels flicker in and out of the subject denominator under
183
+ // AA jitter. Alpha is the only hit/miss signal valid here: the aux G-buffer is denoiser-gated and
184
+ // the ray flags do not survive to FinalWrite.
185
+ const hitGeometry = sampleColor.w.greaterThan( float( 0.5 ) );
186
+ const wasGeometry = prevWord.bitAnd( uint( 0x40000000 ) ).notEqual( uint( 0 ) );
187
+ const geometryBit = select(
188
+ frame.equal( uint( 0 ) ),
189
+ select( hitGeometry, uint( 0x40000000 ), uint( 0 ) ),
190
+ select( hitGeometry.or( wasGeometry ), uint( 0x40000000 ), uint( 0 ) )
191
+ ).toVar();
175
192
 
176
- // Maintain the freeze streak on the relErr-only predicate (NO absFloor — it bakes dim regions dark
177
- // when it permanently freezes a pixel). Monotonic: streak>=K freezes for the run, so global alpha stays exact.
178
- If( adaptiveOn, () => {
193
+ // relErr-only, no absolute floor — a floor bakes dim regions dark once it freezes them.
194
+ // Monotonic: streak>=K freezes for the run, so the global alpha stays exact.
195
+ If( adaptiveOn, () => {
179
196
 
180
- If( frame.equal( uint( 0 ) ), () => {
197
+ If( frame.equal( uint( 0 ) ), () => {
181
198
 
182
- // Frame 0 (post-reset/camera-move) clears the frozen state so each run re-freezes fresh.
183
- streakBufferRW.element( pixelId ).assign( uint( 0 ) );
199
+ newStreak.assign( uint( 0 ) );
184
200
 
185
- } );
201
+ } );
186
202
 
187
- If( wasFrozen.not().and( frame.greaterThan( uint( 0 ) ) ), () => {
203
+ If( wasFrozen.not().and( frame.greaterThan( uint( 0 ) ) ), () => {
188
204
 
189
- const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
190
- .and( relErr.lessThan( pixelFreezeThreshold ) );
191
- const newStreak = select( freezeCandidate, streakBufferRW.element( pixelId ).add( uint( 1 ) ), uint( 0 ) ).toVar();
192
- streakBufferRW.element( pixelId ).assign( newStreak );
205
+ // meanLum>0: an all-zero pixel has zero variance and passes any bar, but freezing skips its
206
+ // rays so it could never then discover its energy (−39% mean luminance on Veach bidir). The
207
+ // whole-frame count deliberately has no such guard — true black must count as converged there.
208
+ const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
209
+ .and( relErr.lessThan( pixelFreezeThreshold ) )
210
+ .and( meanLum.greaterThan( float( 0 ) ) );
211
+ newStreak.assign( select( freezeCandidate, oldStreak.add( uint( 1 ) ), uint( 0 ) ) );
212
+
213
+ } );
193
214
 
194
215
  } );
195
216
 
217
+ streakBufferRW.element( pixelId ).assign(
218
+ newStreak
219
+ .bitOr( select( eligible, uint( 0x80000000 ), uint( 0 ) ) )
220
+ .bitOr( geometryBit )
221
+ );
222
+
196
223
  } );
197
224
 
198
225
  } );
@@ -88,10 +88,10 @@ export function buildGenerateKernel( params ) {
88
88
  // pdf inits to 0 = prevBouncePdf (megakernel parity PathTracerCore.js:556). The bounce>0 env/emissive
89
89
  // MIS gate skips until an opaque scatter writes a real combinedPdf; free bounces preserve it.
90
90
  writeRayThroughputPdf( rayBufferRW, rayID, vec4( 1.0, 1.0, 1.0, 0.0 ).xyz, float( 0.0 ) );
91
- // Alpha inits to 1 in transparent-bg mode (megakernel parity: PathTracerCore.js:554). Shade zeroes
92
- // it only on env-escape-without-opaque; a ray that dies inside geometry (e.g. SSS walk termination)
93
- // keeps alpha 1 → solid. Non-transparent mode is inert (FinalWrite forces alpha 1).
94
- writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), select( transparentBackground, float( 1.0 ), float( 0.0 ) ) ) );
91
+ // Alpha inits to 1 unconditionally (megakernel parity: PathTracerCore.js:554). Shade zeroes it only on
92
+ // env-escape-without-opaque, so the lane always carries coverage — which the convergence gate reads to
93
+ // tell subject from background. Output is unaffected: FinalWrite forces alpha 1 when transparent-bg is off.
94
+ writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), float( 1.0 ) ) );
95
95
 
96
96
  If( auxOn, () => {
97
97
 
@@ -469,7 +469,10 @@ export function buildShadeKernel( params ) {
469
469
  // geometry (megakernel parity: PathTracerCore.js:784). A secondary bounce off an opaque
470
470
  // surface that escapes to env keeps alpha 1 (HAS_HIT_OPAQUE set), so glass-in-front-of-an-
471
471
  // object stays opaque while glass-in-front-of-sky exports see-through.
472
- If( transparentBackground.and( flags.bitAnd( uint( RAY_FLAG.HAS_HIT_OPAQUE ) ).equal( uint( 0 ) ) ), () => {
472
+ // Ungated (was transparentBackground-only) so the alpha lane always carries coverage: it is the
473
+ // only hit/miss signal valid at FinalWrite. The image is unchanged — FinalWrite forces alpha 1
474
+ // when transparent-bg is off.
475
+ If( flags.bitAnd( uint( RAY_FLAG.HAS_HIT_OPAQUE ) ).equal( uint( 0 ) ), () => {
473
476
 
474
477
  currentRadiance.w.assign( 0.0 );
475
478
 
@@ -186,6 +186,10 @@ export class UniformManager {
186
186
  u( 'pixelFreezeThreshold', DEFAULT_STATE.pixelFreezeThreshold, 'float' );
187
187
  u( 'pixelFreezeStability', DEFAULT_STATE.pixelFreezeStability, 'int' );
188
188
 
189
+ // Convergence debug overlay: keeps FinalWrite's m2 estimate alive when adaptive sampling is off,
190
+ // so the Compositor overlay reads a live error field instead of a stale buffer.
191
+ ub( 'convergenceOverlay', DEFAULT_STATE.convergenceOverlay );
192
+
189
193
  // Accumulation
190
194
  ub( 'enableAccumulation', true );
191
195
  u( 'accumulationAlpha', 0.0, 'float' );