rayzee 7.22.2 → 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.
- package/dist/rayzee.es.js +1594 -1468
- package/dist/rayzee.umd.js +43 -43
- package/package.json +1 -1
- package/src/EngineDefaults.js +24 -16
- package/src/PathTracerApp.js +89 -18
- package/src/Processor/QueueManager.js +6 -1
- package/src/RenderSettings.js +13 -1
- package/src/Stages/Compositor.js +132 -2
- package/src/Stages/PathTracer.js +199 -17
- package/src/TSL/FinalWriteKernel.js +61 -34
- package/src/TSL/GenerateKernel.js +4 -4
- package/src/TSL/ShadeKernel.js +4 -1
- package/src/index.js +1 -0
- package/src/managers/UniformManager.js +4 -0
package/src/Stages/PathTracer.js
CHANGED
|
@@ -54,6 +54,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
54
54
|
// Tier-2 dilation: per-pixel frozen mask (1=skip), written race-free in buildActivePixels; a frozen pixel
|
|
55
55
|
// stays active if any 8-neighbour is still active (Cycles box-filter) to avoid hard frozen/active seams.
|
|
56
56
|
this._frozenMaskAttr = null;
|
|
57
|
+
this._convDebugSource = null; // memoized handle set for the Compositor's convergence overlay
|
|
57
58
|
this._dilateFrozenUniform = uniform( 1, 'int' ); // 1 = dilate (default); 0 = plain per-pixel freeze
|
|
58
59
|
this._wavefrontReady = false;
|
|
59
60
|
|
|
@@ -99,6 +100,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
99
100
|
// single-flight guard for its async counter read. Zeroed on reset/camera-move/resize; never refreshed
|
|
100
101
|
// mid-motion (the readback early-return + frozen frameCount keep the stop from firing while moving).
|
|
101
102
|
this._convergedFraction = 0;
|
|
103
|
+
// Subject-only convergence, from the same eroded bits. Starts at 0 so the stop cannot fire unmeasured.
|
|
104
|
+
this._convergedGeometryFraction = 0;
|
|
105
|
+
this._geometryPixelCount = 0;
|
|
102
106
|
this._convergedReadbackPending = false;
|
|
103
107
|
|
|
104
108
|
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
@@ -272,6 +276,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
272
276
|
this._readbackGeneration ++;
|
|
273
277
|
// Drop the stale converged fraction so the early-stop can't fire on the pose we're leaving.
|
|
274
278
|
this._convergedFraction = 0;
|
|
279
|
+
this._convergedGeometryFraction = 0;
|
|
275
280
|
// Tier-2: drop the stale active-pixel count so bounce 0 full-sizes until the new pose re-measures.
|
|
276
281
|
this._lastActivePixelCount = 0;
|
|
277
282
|
|
|
@@ -372,6 +377,14 @@ export class PathTracer extends PathTracerStage {
|
|
|
372
377
|
|
|
373
378
|
}
|
|
374
379
|
|
|
380
|
+
// Full-frame kernel, so first chunk only, after the zeroing above. Frame 0 skips it — those bits
|
|
381
|
+
// still belong to the pre-reset view. Only on frames whose counters are actually read back.
|
|
382
|
+
if ( chunkIndex === 0 && frameValue > 0 && this.useAdaptiveSampling.value > 0 && this._willReadCountersThisFrame() ) {
|
|
383
|
+
|
|
384
|
+
km.dispatch( 'countConvergedDilated' );
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
|
|
375
388
|
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
376
389
|
|
|
377
390
|
this._wfCurrentBounce.value = bounce;
|
|
@@ -487,9 +500,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
487
500
|
// _convergedFraction is zeroed on camera-move and never refreshed mid-motion (readback early-returns).
|
|
488
501
|
_isConvergedComplete() {
|
|
489
502
|
|
|
503
|
+
// BOTH fractions must clear the bar. The whole-frame one counts every pixel, so it is diluted by
|
|
504
|
+
// however much easy background is in shot — the bar effectively becomes (bar−bgShare)/(1−bgShare) on
|
|
505
|
+
// the subject, which makes it framing-dependent. The geometry-only fraction gates that. Neither
|
|
506
|
+
// replaces the other: background can be genuinely noisy (DOF, sharp env under AA jitter), and then
|
|
507
|
+
// the whole-frame fraction is the binding one.
|
|
490
508
|
return this.useAdaptiveSampling.value > 0
|
|
491
509
|
&& this.frameCount >= this.adaptiveMinSamples.value
|
|
492
|
-
&& this._convergedFraction >= this.adaptiveStopFraction.value
|
|
510
|
+
&& this._convergedFraction >= this.adaptiveStopFraction.value
|
|
511
|
+
&& this._convergedGeometryFraction >= this.adaptiveStopFraction.value;
|
|
493
512
|
|
|
494
513
|
}
|
|
495
514
|
|
|
@@ -497,10 +516,62 @@ export class PathTracer extends PathTracerStage {
|
|
|
497
516
|
|
|
498
517
|
super.reset();
|
|
499
518
|
this._convergedFraction = 0;
|
|
519
|
+
this._convergedGeometryFraction = 0;
|
|
500
520
|
this._lastActivePixelCount = 0;
|
|
501
521
|
|
|
502
522
|
}
|
|
503
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Per-pixel convergence state for the Compositor's debug overlay: the buffers FinalWrite maintains
|
|
526
|
+
* plus the uniforms needed to re-derive its predicate. Read-only — the overlay never writes back.
|
|
527
|
+
* Null until the wavefront kernels exist.
|
|
528
|
+
*/
|
|
529
|
+
getConvergenceDebugSource() {
|
|
530
|
+
|
|
531
|
+
if ( ! this._m2Attr || ! this._frozenMaskAttr ) return null;
|
|
532
|
+
// Polled every frame while the overlay is on — rebuild only when the buffers actually change.
|
|
533
|
+
if ( this._convDebugSource?.m2 === this._m2Attr && this._convDebugSource.frozenMask === this._frozenMaskAttr ) {
|
|
534
|
+
|
|
535
|
+
return this._convDebugSource;
|
|
536
|
+
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this._convDebugSource = {
|
|
540
|
+
m2: this._m2Attr,
|
|
541
|
+
frozenMask: this._frozenMaskAttr,
|
|
542
|
+
frame: this.frame,
|
|
543
|
+
renderWidth: this._wfRenderWidth,
|
|
544
|
+
renderHeight: this._wfRenderHeight,
|
|
545
|
+
noiseThreshold: this.noiseThreshold,
|
|
546
|
+
pixelFreezeThreshold: this.pixelFreezeThreshold,
|
|
547
|
+
usePixelFreeze: this.usePixelFreeze,
|
|
548
|
+
adaptiveMinSamples: this.adaptiveMinSamples,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return this._convDebugSource;
|
|
552
|
+
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Whole-frame adaptive-sampling telemetry, from the counters readback (settled views only).
|
|
557
|
+
* `converged` is the fraction that met the noise threshold; `activePixels` is the count the
|
|
558
|
+
* frozen-compaction path actually traced last settled frame.
|
|
559
|
+
*/
|
|
560
|
+
getConvergenceStats() {
|
|
561
|
+
|
|
562
|
+
const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
converged: this._convergedFraction,
|
|
566
|
+
convergedGeometry: this._convergedGeometryFraction,
|
|
567
|
+
geometryPixels: this._geometryPixelCount,
|
|
568
|
+
activePixels: this._lastActivePixelCount,
|
|
569
|
+
totalPixels: total,
|
|
570
|
+
frame: this.frameCount,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
}
|
|
574
|
+
|
|
504
575
|
// Parent resizes storageTextures/shaderBuilder; wavefront also needs its buffers/uniforms/kernels rebuilt.
|
|
505
576
|
_handleResize() {
|
|
506
577
|
|
|
@@ -551,6 +622,22 @@ export class PathTracer extends PathTracerStage {
|
|
|
551
622
|
|
|
552
623
|
}
|
|
553
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Whether THIS frame's counters will actually be read back, evaluated before the dispatches that fill
|
|
627
|
+
* them. Mirrors _maybeReadbackCounters' guards exactly; both run inside the same synchronous render()
|
|
628
|
+
* call, so no async continuation can flip a flag between them.
|
|
629
|
+
*
|
|
630
|
+
* countConvergedDilated is a full-frame pass whose only consumer is that readback, and the readback is
|
|
631
|
+
* on a 4-frame cadence — counting every frame threw ~75% of the work away.
|
|
632
|
+
*/
|
|
633
|
+
_willReadCountersThisFrame() {
|
|
634
|
+
|
|
635
|
+
if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return false;
|
|
636
|
+
if ( this._readbackPending || this._convergedReadbackPending ) return false;
|
|
637
|
+
return this._readbackFrameCounter + 1 >= this._readbackEveryNFrames;
|
|
638
|
+
|
|
639
|
+
}
|
|
640
|
+
|
|
554
641
|
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
555
642
|
_maybeReadbackCounters() {
|
|
556
643
|
|
|
@@ -620,6 +707,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
620
707
|
|
|
621
708
|
const c = new Uint32Array( buf );
|
|
622
709
|
this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
|
|
710
|
+
// No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
|
|
711
|
+
// whole-frame fraction decide alone.
|
|
712
|
+
const geo = c[ COUNTER.GEOMETRY_COUNT ];
|
|
713
|
+
this._geometryPixelCount = geo;
|
|
714
|
+
this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
|
|
623
715
|
// Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
|
|
624
716
|
this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
|
|
625
717
|
|
|
@@ -673,6 +765,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
673
765
|
this._readbackFrameCounter = 0;
|
|
674
766
|
this._readbackGeneration ++;
|
|
675
767
|
this._convergedFraction = 0;
|
|
768
|
+
this._convergedGeometryFraction = 0;
|
|
676
769
|
this._lastActivePixelCount = 0;
|
|
677
770
|
|
|
678
771
|
// Chunked path pool: the wavefront buffers are sized to the fixed device budget B (not the
|
|
@@ -761,10 +854,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
761
854
|
// MAX_STORAGE_TEXTURE_SIZE and can't be resized, so recreate the pool at the (new) live reserved size and
|
|
762
855
|
// rebuild the wavefront kernels in place. The stage OBJECT is unchanged, so manager/event refs stay valid;
|
|
763
856
|
// only GPU textures + compute pipelines are rebuilt. Scene buffers (BVH/tri/material) are resolution-
|
|
764
|
-
// independent and untouched. Caller must have rendering paused.
|
|
857
|
+
// independent and untouched. Caller must have rendering paused.
|
|
765
858
|
reallocateReservedStorage() {
|
|
766
859
|
|
|
767
|
-
if ( ! this.
|
|
860
|
+
if ( ! this.storageTextures.writeColor ) return;
|
|
768
861
|
|
|
769
862
|
// Clamp into the (possibly just-lowered) reserve: create() sizes the write StorageTextures at
|
|
770
863
|
// MAX_STORAGE_TEXTURE_SIZE but the readTarget at what's passed here, and the tracked render size still
|
|
@@ -777,6 +870,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
777
870
|
this.storageTextures.create( w, h );
|
|
778
871
|
this.resolution.value.set( w, h );
|
|
779
872
|
|
|
873
|
+
// No kernels before the first scene with materials — nothing to rebuild, and a build here would compile
|
|
874
|
+
// against 0 materials. The pool above is recreated regardless: it is allocated at construction, so a
|
|
875
|
+
// reserve raised before the first model load must still reach it or every later copy overflows it.
|
|
876
|
+
if ( ! this._packedBuffers || ! ( this.materialData?.materialCount > 0 ) ) return;
|
|
877
|
+
|
|
780
878
|
// Rebuild kernels: re-references the fresh write textures + recreates the per-pixel aux buffers
|
|
781
879
|
// (m2/streak/frozenMask) at the new maxPixels. Prev-frame nodes are repointed per-frame in render().
|
|
782
880
|
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
@@ -838,20 +936,28 @@ export class PathTracer extends PathTracerStage {
|
|
|
838
936
|
// max resolution (small — 12 B/pixel) and NEVER realloc on a resize.
|
|
839
937
|
const maxPixels = MAX_STORAGE_TEXTURE_SIZE * MAX_STORAGE_TEXTURE_SIZE;
|
|
840
938
|
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
939
|
+
// Only reallocated on growth, never on a plain rebuild: the Compositor's overlay binds m2/frozenMask
|
|
940
|
+
// read-only and a fresh attribute would strand its bind group. Surviving contents are safe — every
|
|
941
|
+
// consumer re-seeds at frame 0.
|
|
942
|
+
if ( ! this._m2Attr || this._m2Attr.count < maxPixels ) {
|
|
943
|
+
|
|
944
|
+
// Tier-1 convergence: per-pixel running mean of luminance² (Welford second moment), read+written by FinalWrite.
|
|
945
|
+
freeStorageAttribute( this.renderer, this._m2Attr );
|
|
946
|
+
this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array( maxPixels ), 1 );
|
|
947
|
+
|
|
948
|
+
// Tier-2: per-pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
|
|
949
|
+
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
950
|
+
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
951
|
+
|
|
952
|
+
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
953
|
+
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
954
|
+
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
955
|
+
|
|
956
|
+
}
|
|
845
957
|
|
|
846
|
-
|
|
847
|
-
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
848
|
-
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
958
|
+
const m2RW = storage( this._m2Attr, 'float' );
|
|
849
959
|
const streakRW = storage( this._streakAttr, 'uint' );
|
|
850
960
|
const streakRO = storage( this._streakAttr, 'uint' ).toReadOnly();
|
|
851
|
-
|
|
852
|
-
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
853
|
-
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
854
|
-
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
855
961
|
const frozenMaskRW = storage( this._frozenMaskAttr, 'uint' );
|
|
856
962
|
const frozenMaskRO = storage( this._frozenMaskAttr, 'uint' ).toReadOnly();
|
|
857
963
|
|
|
@@ -924,6 +1030,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
924
1030
|
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
925
1031
|
|
|
926
1032
|
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
1033
|
+
atomicStore( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
1034
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
927
1035
|
|
|
928
1036
|
} );
|
|
929
1037
|
|
|
@@ -983,6 +1091,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
983
1091
|
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
984
1092
|
|
|
985
1093
|
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
1094
|
+
atomicStore( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
1095
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
986
1096
|
|
|
987
1097
|
} );
|
|
988
1098
|
|
|
@@ -1005,7 +1115,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
1005
1115
|
// (streak<K), softening the boundary. Mask is read by FinalWrite too → race-free.
|
|
1006
1116
|
const frozen = uint( 0 ).toVar();
|
|
1007
1117
|
|
|
1008
|
-
|
|
1118
|
+
// Bits 31/30 of the streak word are the converged + geometry flags — mask them off wherever the streak is read.
|
|
1119
|
+
If( this.frame.greaterThan( uint( 0 ) ).and( streakRO.element( p ).bitAnd( uint( 0x3FFFFFFF ) ).greaterThanEqual( uint( freezeK ) ) ), () => {
|
|
1009
1120
|
|
|
1010
1121
|
frozen.assign( uint( 1 ) );
|
|
1011
1122
|
|
|
@@ -1021,7 +1132,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1021
1132
|
If( nx.greaterThanEqual( int( 0 ) ).and( nx.lessThan( this._wfRenderWidth ) )
|
|
1022
1133
|
.and( ny.greaterThanEqual( int( 0 ) ) ).and( ny.lessThan( this._wfRenderHeight ) ), () => {
|
|
1023
1134
|
|
|
1024
|
-
If( streakRO.element( uint( ny.mul( this._wfRenderWidth ).add( nx ) ) ).lessThan( uint( freezeK ) ), () => {
|
|
1135
|
+
If( streakRO.element( uint( ny.mul( this._wfRenderWidth ).add( nx ) ) ).bitAnd( uint( 0x3FFFFFFF ) ).lessThan( uint( freezeK ) ), () => {
|
|
1025
1136
|
|
|
1026
1137
|
frozen.assign( uint( 0 ) ); // a still-active neighbour keeps this pixel active
|
|
1027
1138
|
|
|
@@ -1052,6 +1163,74 @@ export class PathTracer extends PathTracerStage {
|
|
|
1052
1163
|
buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
|
|
1053
1164
|
);
|
|
1054
1165
|
|
|
1166
|
+
// Whole-frame converged count, eroded 3×3 (Cycles film_adaptive_sampling_filter_x/y parity): a pixel
|
|
1167
|
+
// counts only if it AND its 8 in-bounds neighbours carry last frame's converged bit. A pointwise count
|
|
1168
|
+
// is blind to undiscovered energy — all-zero pixels have zero variance and pass any threshold, so on
|
|
1169
|
+
// indirect-dominant scenes it peaks on a black image and retires the frame at ~10 spp. Erosion lets
|
|
1170
|
+
// each discovered speckle poison its neighbourhood while energy is still being found.
|
|
1171
|
+
// Reads the PREVIOUS frame's bits, which costs nothing: the stop already lags by the readback cadence.
|
|
1172
|
+
const countConvergedFn = Fn( () => {
|
|
1173
|
+
|
|
1174
|
+
const tid = instanceIndex;
|
|
1175
|
+
const W = this._wfRenderWidth;
|
|
1176
|
+
const H = this._wfRenderHeight;
|
|
1177
|
+
If( tid.lessThan( uint( W.mul( H ) ) ), () => {
|
|
1178
|
+
|
|
1179
|
+
const px = int( tid ).mod( W );
|
|
1180
|
+
const py = int( tid ).div( W );
|
|
1181
|
+
const CONV_BIT = uint( 0x80000000 );
|
|
1182
|
+
const GEOM_BIT = uint( 0x40000000 );
|
|
1183
|
+
|
|
1184
|
+
const word = streakRO.element( tid ).toVar();
|
|
1185
|
+
const isGeometry = word.bitAnd( GEOM_BIT ).notEqual( uint( 0 ) );
|
|
1186
|
+
const allConverged = word.bitAnd( CONV_BIT ).notEqual( uint( 0 ) ).toVar();
|
|
1187
|
+
|
|
1188
|
+
If( allConverged, () => {
|
|
1189
|
+
|
|
1190
|
+
for ( const [ dx, dy ] of [[ - 1, - 1 ], [ 0, - 1 ], [ 1, - 1 ], [ - 1, 0 ], [ 1, 0 ], [ - 1, 1 ], [ 0, 1 ], [ 1, 1 ]] ) {
|
|
1191
|
+
|
|
1192
|
+
const nx = px.add( int( dx ) );
|
|
1193
|
+
const ny = py.add( int( dy ) );
|
|
1194
|
+
If( nx.greaterThanEqual( int( 0 ) ).and( nx.lessThan( W ) )
|
|
1195
|
+
.and( ny.greaterThanEqual( int( 0 ) ) ).and( ny.lessThan( H ) ), () => {
|
|
1196
|
+
|
|
1197
|
+
If( streakRO.element( uint( ny.mul( W ).add( nx ) ) ).bitAnd( CONV_BIT ).equal( uint( 0 ) ), () => {
|
|
1198
|
+
|
|
1199
|
+
allConverged.assign( false );
|
|
1200
|
+
|
|
1201
|
+
} );
|
|
1202
|
+
|
|
1203
|
+
} );
|
|
1204
|
+
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
} );
|
|
1208
|
+
|
|
1209
|
+
If( allConverged, () => {
|
|
1210
|
+
|
|
1211
|
+
atomicAdd( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 1 ) );
|
|
1212
|
+
|
|
1213
|
+
} );
|
|
1214
|
+
|
|
1215
|
+
// Same eroded verdict, restricted to subject pixels — the framing-independent view.
|
|
1216
|
+
If( isGeometry, () => {
|
|
1217
|
+
|
|
1218
|
+
atomicAdd( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 1 ) );
|
|
1219
|
+
If( allConverged, () => {
|
|
1220
|
+
|
|
1221
|
+
atomicAdd( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 1 ) );
|
|
1222
|
+
|
|
1223
|
+
} );
|
|
1224
|
+
|
|
1225
|
+
} );
|
|
1226
|
+
|
|
1227
|
+
} );
|
|
1228
|
+
|
|
1229
|
+
} );
|
|
1230
|
+
this._kernelManager.register( 'countConvergedDilated',
|
|
1231
|
+
countConvergedFn().compute( [ Math.ceil( ( w * h ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1055
1234
|
const seedEnterFn = Fn( () => {
|
|
1056
1235
|
|
|
1057
1236
|
const active = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
@@ -1314,7 +1493,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
1314
1493
|
visMode: this.visMode,
|
|
1315
1494
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
1316
1495
|
cleanAuxNormalEnabled: this._cleanAuxNormalUniform,
|
|
1317
|
-
counters,
|
|
1318
1496
|
m2BufferRW: m2RW,
|
|
1319
1497
|
useAdaptiveSampling: this.useAdaptiveSampling,
|
|
1320
1498
|
noiseThreshold: this.noiseThreshold,
|
|
@@ -1324,6 +1502,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1324
1502
|
pixelFreezeThreshold: this.pixelFreezeThreshold,
|
|
1325
1503
|
streakBufferRW: streakRW,
|
|
1326
1504
|
frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
|
|
1505
|
+
convergenceOverlay: this.convergenceOverlay,
|
|
1327
1506
|
chunkRowBase: this._wfChunkRowBase,
|
|
1328
1507
|
chunkRows: this._wfChunkRows,
|
|
1329
1508
|
} );
|
|
@@ -1398,6 +1577,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
1398
1577
|
const h = this._wfRenderHeight.value;
|
|
1399
1578
|
|
|
1400
1579
|
this._kernelManager.setDispatchForGrid( 'debug', w, h );
|
|
1580
|
+
// Full-frame (not per-chunk); sized from live values so an in-place resize can't strand its grid.
|
|
1581
|
+
this._kernelManager.setDispatchForCount( 'countConvergedDilated', w * h );
|
|
1401
1582
|
|
|
1402
1583
|
}
|
|
1403
1584
|
|
|
@@ -1440,6 +1621,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1440
1621
|
this._m2Attr = null;
|
|
1441
1622
|
this._streakAttr = null;
|
|
1442
1623
|
this._frozenMaskAttr = null;
|
|
1624
|
+
this._convDebugSource = null;
|
|
1443
1625
|
this._wavefrontReady = false;
|
|
1444
1626
|
|
|
1445
1627
|
}
|
|
@@ -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,
|
|
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
|
|
53
|
-
|
|
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
|
|
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
|
-
//
|
|
143
|
-
// so
|
|
144
|
-
//
|
|
145
|
-
|
|
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
|
-
//
|
|
165
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
197
|
+
If( frame.equal( uint( 0 ) ), () => {
|
|
181
198
|
|
|
182
|
-
|
|
183
|
-
streakBufferRW.element( pixelId ).assign( uint( 0 ) );
|
|
199
|
+
newStreak.assign( uint( 0 ) );
|
|
184
200
|
|
|
185
|
-
|
|
201
|
+
} );
|
|
186
202
|
|
|
187
|
-
|
|
203
|
+
If( wasFrozen.not().and( frame.greaterThan( uint( 0 ) ) ), () => {
|
|
188
204
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 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
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/index.js
CHANGED
|
@@ -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' );
|