rayzee 7.22.6 → 7.22.8
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 +224 -151
- package/dist/rayzee.umd.js +34 -22
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -1
- package/src/Passes/OIDNDenoiser.js +163 -26
- package/src/PathTracerApp.js +33 -10
- package/src/Stages/PathTracer.js +64 -8
- package/src/TSL/FinalWriteKernel.js +5 -3
- package/src/managers/DenoisingManager.js +12 -0
- package/src/managers/UniformManager.js +4 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -604,7 +604,8 @@ export const PRODUCTION_RENDER_CONFIG = {
|
|
|
604
604
|
// so easy scenes finish well under it while hard GI scenes use the full budget.
|
|
605
605
|
maxSamples: 150, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
|
|
606
606
|
renderMode: 1, enableAlphaShadows: true,
|
|
607
|
-
|
|
607
|
+
// 'high' is the only tier that reaches OIDN's _large weights (calb_cnrm); ~2x denoise cost.
|
|
608
|
+
enableOIDN: true, oidnQuality: 'high',
|
|
608
609
|
interactionModeEnabled: false,
|
|
609
610
|
// 0.94 against the eroded count ≈ the old raw-count 0.98; erosion holds the fraction a few points lower.
|
|
610
611
|
useAdaptiveSampling: true,
|
|
@@ -52,20 +52,65 @@ import { getAssetConfig } from '../AssetConfig.js';
|
|
|
52
52
|
const _tmOut = new Float32Array( 3 );
|
|
53
53
|
|
|
54
54
|
const MODEL_CONFIG = {
|
|
55
|
-
//
|
|
55
|
+
// No cleanAux flag in oidn-web, so the blob is it — and must match what setCleanAuxNormal
|
|
56
|
+
// feeds: `fast` point-samples the normal (noisy aux), balance/high accumulate it. calb_cnrm
|
|
57
|
+
// is also the only model with a `_large` blob.
|
|
56
58
|
QUALITY_MODELS: {
|
|
57
59
|
fast: 'rt_hdr_alb_nrm_small',
|
|
58
|
-
balance: '
|
|
60
|
+
balance: 'rt_hdr_calb_cnrm',
|
|
59
61
|
high: 'rt_hdr_calb_cnrm_large'
|
|
60
62
|
},
|
|
61
63
|
DEFAULT_OPTIONS: {
|
|
62
64
|
enableOIDN: true,
|
|
63
65
|
oidnQuality: 'fast',
|
|
64
66
|
debugGbufferMaps: true,
|
|
65
|
-
tileSize
|
|
67
|
+
// Tiles run at tileSize + 2*overlap (96px, 112 for _large), so small tiles pay that padding
|
|
68
|
+
// every tile: 256 puts 3.06x the pixels through the UNet at 512². Measured 3.64x faster.
|
|
69
|
+
tileSize: 512
|
|
66
70
|
}
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
// Stands in for OIDN's inputScale, which oidn-web only applies on its CPU path — the GPUBuffer
|
|
74
|
+
// path leaves the uniform at 1.0. Equivalent because oidn-web does PUForward(col*inputScale) in
|
|
75
|
+
// and PUInverse(..)/inputScale out, so pre-multiplying here and dividing it back out of the
|
|
76
|
+
// output exposure round-trips. Alpha is preserved for _cacheInputAlpha.
|
|
77
|
+
const COLOR_SCALE_WGSL = /* wgsl */`
|
|
78
|
+
@group(0) @binding(0) var<storage, read_write> col: array<vec4<f32>>;
|
|
79
|
+
@group(0) @binding(1) var<uniform> scale: f32;
|
|
80
|
+
|
|
81
|
+
@compute @workgroup_size(64)
|
|
82
|
+
fn main( @builtin(global_invocation_id) gid: vec3<u32>,
|
|
83
|
+
@builtin(num_workgroups) nwg: vec3<u32> ) {
|
|
84
|
+
let i = gid.y * nwg.x * 64u + gid.x;
|
|
85
|
+
if ( i >= arrayLength( &col ) ) { return; }
|
|
86
|
+
let c = col[ i ];
|
|
87
|
+
col[ i ] = vec4<f32>( c.xyz * scale, c.w );
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const SCALE_WG_SIZE = 64;
|
|
92
|
+
// 1D would exceed maxComputeWorkgroupsPerDimension (65535) past ~2048², so the dispatch is 2D.
|
|
93
|
+
const SCALE_MAX_WG_X = 32768;
|
|
94
|
+
|
|
95
|
+
// OIDN's autoexposure, mirroring oidn-web's avgLogLum. Whole-frame, so it cannot seam per tile.
|
|
96
|
+
const OIDN_AUTOEXPOSURE_KEY = 0.18;
|
|
97
|
+
|
|
98
|
+
function oidnInputScale( f32, pixelCount ) {
|
|
99
|
+
|
|
100
|
+
let logSum = 0;
|
|
101
|
+
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
102
|
+
|
|
103
|
+
const o = i * 4;
|
|
104
|
+
const lum = 0.212671 * f32[ o ] + 0.71516 * f32[ o + 1 ] + 0.072169 * f32[ o + 2 ];
|
|
105
|
+
logSum += Math.log2( Math.max( lum, 0 ) + 0.0001 );
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const scale = OIDN_AUTOEXPOSURE_KEY / Math.pow( 2, logSum / pixelCount );
|
|
110
|
+
return Number.isFinite( scale ) && scale > 0 ? scale : 1.0;
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
|
|
69
114
|
export class OIDNDenoiser extends EventDispatcher {
|
|
70
115
|
|
|
71
116
|
constructor( output, renderer, scene, camera, options = {} ) {
|
|
@@ -110,6 +155,11 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
110
155
|
// order guarantees the overwrites are serialized).
|
|
111
156
|
this._gpuInputPadBuffer = null;
|
|
112
157
|
this._gpuInputPaddedRowBytes = 0;
|
|
158
|
+
// 1.0 until the first readback.
|
|
159
|
+
this._colorScalePipeline = null;
|
|
160
|
+
this._colorScaleBindGroup = null;
|
|
161
|
+
this._colorScaleUniform = null;
|
|
162
|
+
this._oidnInputScale = 1.0;
|
|
113
163
|
// Pooled MAP_READ staging buffer for _cacheInputAlpha. Only allocated
|
|
114
164
|
// when transparent-background readback is used, destroyed on resolution
|
|
115
165
|
// change or dispose. Same spirit as r184's ReadbackBuffer — we can't use
|
|
@@ -130,6 +180,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
130
180
|
this.quality = this.config.oidnQuality;
|
|
131
181
|
this.debugGbufferMaps = this.config.debugGbufferMaps;
|
|
132
182
|
this.tileSize = this.config.tileSize;
|
|
183
|
+
// tileSize actually baked into the live UNet, so a runtime change forces a rebuild.
|
|
184
|
+
this._activeTileSize = 0;
|
|
133
185
|
|
|
134
186
|
// State management
|
|
135
187
|
this.state = {
|
|
@@ -212,8 +264,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
212
264
|
this.state.isLoading = true;
|
|
213
265
|
const tzaUrl = this._generateTzaUrl();
|
|
214
266
|
|
|
215
|
-
//
|
|
216
|
-
if ( this.currentTZAUrl === tzaUrl && this.unet ) {
|
|
267
|
+
// maxTileSize is a constructor arg, so comparing the URL alone left tileSize changes inert.
|
|
268
|
+
if ( this.currentTZAUrl === tzaUrl && this._activeTileSize === this.tileSize && this.unet ) {
|
|
217
269
|
|
|
218
270
|
this.state.isLoading = false;
|
|
219
271
|
return;
|
|
@@ -251,6 +303,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
251
303
|
} );
|
|
252
304
|
|
|
253
305
|
this.currentTZAUrl = tzaUrl;
|
|
306
|
+
this._activeTileSize = this.tileSize;
|
|
254
307
|
this.dispatchEvent( { type: 'loaded' } );
|
|
255
308
|
log.debug( 'UNet weights loaded:', tzaUrl );
|
|
256
309
|
|
|
@@ -477,21 +530,18 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
477
530
|
|
|
478
531
|
copyTex( textures.color, this._gpuInputBuffers.color );
|
|
479
532
|
copyTex( textures.albedo, this._gpuInputBuffers.albedo );
|
|
533
|
+
// The normal stays [0,1]-encoded. This contradicts OIDN's API ("must be in the [-1,1]
|
|
534
|
+
// range") but is right for this port: OIDN's own getNormal (cpu_input_process.isph) does
|
|
535
|
+
// `value*0.5+0.5` before the network, and oidn-web omits that remap. Decoding to [-1,1]
|
|
536
|
+
// measured 0.878 -> 2.439 denoise ratio at 64 spp. Do not "fix" it.
|
|
480
537
|
copyTex( textures.normal, this._gpuInputBuffers.normal );
|
|
481
538
|
|
|
482
539
|
device.queue.submit( [ encoder.finish() ] );
|
|
483
540
|
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
await this._cacheInputAlpha( device, width, height );
|
|
489
|
-
|
|
490
|
-
} else {
|
|
491
|
-
|
|
492
|
-
this._cachedAlpha = null;
|
|
493
|
-
|
|
494
|
-
}
|
|
541
|
+
// One readback serves the autoexposure scale and the alpha OIDN discards. Must precede the scale.
|
|
542
|
+
await this._readbackColor( device, width, height );
|
|
543
|
+
if ( globalThis.__OIDN_NO_INPUT_SCALE ) this._oidnInputScale = 1.0;
|
|
544
|
+
this._applyColorScale( device, width * height );
|
|
495
545
|
|
|
496
546
|
// Draw the current noisy frame as the base — denoised tiles paint on top progressively
|
|
497
547
|
this.ctx.drawImage( this.input, 0, 0, width, height );
|
|
@@ -571,6 +621,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
571
621
|
|
|
572
622
|
this._alphaReadbackBuffer?.destroy();
|
|
573
623
|
this._alphaReadbackMapped = false;
|
|
624
|
+
// Bind group holds the destroyed buffer; the pipeline is device-scoped and survives.
|
|
625
|
+
this._colorScaleBindGroup = null;
|
|
626
|
+
this._colorScaleUniform?.destroy();
|
|
627
|
+
this._colorScaleUniform = null;
|
|
574
628
|
this._gpuInputBuffers = { color: null, albedo: null, normal: null };
|
|
575
629
|
this._gpuInputPadBuffer = null;
|
|
576
630
|
this._gpuInputPaddedRowBytes = 0;
|
|
@@ -580,10 +634,10 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
580
634
|
}
|
|
581
635
|
|
|
582
636
|
/**
|
|
583
|
-
* Reads the
|
|
584
|
-
*
|
|
637
|
+
* Reads back the colour buffer for the autoexposure scale, plus alpha when transparent
|
|
638
|
+
* background is on (OIDN discards alpha).
|
|
585
639
|
*/
|
|
586
|
-
async
|
|
640
|
+
async _readbackColor( device, width, height ) {
|
|
587
641
|
|
|
588
642
|
const byteSize = width * height * 16; // rgba32float, tightly packed
|
|
589
643
|
|
|
@@ -620,13 +674,20 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
620
674
|
}
|
|
621
675
|
|
|
622
676
|
const f32 = new Float32Array( staging.getMappedRange() );
|
|
623
|
-
|
|
624
|
-
// Extract alpha channel as uint8 (pre-multiplied is not needed — alpha is 0 or 1)
|
|
625
677
|
const pixelCount = width * height;
|
|
626
|
-
const alpha = new Uint8Array( pixelCount );
|
|
627
|
-
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
628
678
|
|
|
629
|
-
|
|
679
|
+
this._oidnInputScale = oidnInputScale( f32, pixelCount );
|
|
680
|
+
|
|
681
|
+
let alpha = null;
|
|
682
|
+
if ( this.getTransparentBackground() ) {
|
|
683
|
+
|
|
684
|
+
// Alpha as uint8; pre-multiplication is not needed — alpha is 0 or 1
|
|
685
|
+
alpha = new Uint8Array( pixelCount );
|
|
686
|
+
for ( let i = 0; i < pixelCount; i ++ ) {
|
|
687
|
+
|
|
688
|
+
alpha[ i ] = Math.min( Math.max( f32[ i * 4 + 3 ] * 255, 0 ), 255 ) | 0;
|
|
689
|
+
|
|
690
|
+
}
|
|
630
691
|
|
|
631
692
|
}
|
|
632
693
|
|
|
@@ -638,6 +699,81 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
638
699
|
|
|
639
700
|
}
|
|
640
701
|
|
|
702
|
+
/** Display exposure with the input scale divided back out — output arrives pre-multiplied. */
|
|
703
|
+
_outputExposure() {
|
|
704
|
+
|
|
705
|
+
return this.getExposure() / this._oidnInputScale;
|
|
706
|
+
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Pre-multiplies the colour buffer by the autoexposure scale. */
|
|
710
|
+
_applyColorScale( device, pixelCount ) {
|
|
711
|
+
|
|
712
|
+
const scale = this._oidnInputScale;
|
|
713
|
+
if ( ! this._ensureColorScalePipeline( device ) ) return;
|
|
714
|
+
|
|
715
|
+
device.queue.writeBuffer( this._colorScaleUniform, 0, new Float32Array( [ scale ] ) );
|
|
716
|
+
|
|
717
|
+
const wgTotal = Math.ceil( pixelCount / SCALE_WG_SIZE );
|
|
718
|
+
const wgX = Math.min( wgTotal, SCALE_MAX_WG_X );
|
|
719
|
+
const wgY = Math.ceil( wgTotal / wgX );
|
|
720
|
+
|
|
721
|
+
const encoder = device.createCommandEncoder( { label: 'oidn-color-scale' } );
|
|
722
|
+
const pass = encoder.beginComputePass( { label: 'oidn-color-scale' } );
|
|
723
|
+
pass.setPipeline( this._colorScalePipeline );
|
|
724
|
+
pass.setBindGroup( 0, this._colorScaleBindGroup );
|
|
725
|
+
pass.dispatchWorkgroups( wgX, wgY, 1 );
|
|
726
|
+
pass.end();
|
|
727
|
+
device.queue.submit( [ encoder.finish() ] );
|
|
728
|
+
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** @returns {GPUComputePipeline|null} */
|
|
732
|
+
_ensureColorScalePipeline( device ) {
|
|
733
|
+
|
|
734
|
+
const buffer = this._gpuInputBuffers.color;
|
|
735
|
+
if ( ! device || ! buffer ) return null;
|
|
736
|
+
|
|
737
|
+
if ( ! this._colorScalePipeline ) {
|
|
738
|
+
|
|
739
|
+
this._colorScalePipeline = device.createComputePipeline( {
|
|
740
|
+
label: 'oidn-color-scale',
|
|
741
|
+
layout: 'auto',
|
|
742
|
+
compute: {
|
|
743
|
+
module: device.createShaderModule( { label: 'oidn-color-scale', code: COLOR_SCALE_WGSL } ),
|
|
744
|
+
entryPoint: 'main'
|
|
745
|
+
}
|
|
746
|
+
} );
|
|
747
|
+
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if ( ! this._colorScaleUniform ) {
|
|
751
|
+
|
|
752
|
+
this._colorScaleUniform = device.createBuffer( {
|
|
753
|
+
label: 'oidn-color-scale-uniform',
|
|
754
|
+
size: 16, // min uniform binding size
|
|
755
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
756
|
+
} );
|
|
757
|
+
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if ( ! this._colorScaleBindGroup ) {
|
|
761
|
+
|
|
762
|
+
this._colorScaleBindGroup = device.createBindGroup( {
|
|
763
|
+
label: 'oidn-color-scale',
|
|
764
|
+
layout: this._colorScalePipeline.getBindGroupLayout( 0 ),
|
|
765
|
+
entries: [
|
|
766
|
+
{ binding: 0, resource: { buffer } },
|
|
767
|
+
{ binding: 1, resource: { buffer: this._colorScaleUniform } }
|
|
768
|
+
]
|
|
769
|
+
} );
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return this._colorScalePipeline;
|
|
774
|
+
|
|
775
|
+
}
|
|
776
|
+
|
|
641
777
|
/**
|
|
642
778
|
* Promise wrapper around tileExecute for the GPU path.
|
|
643
779
|
* Outputs a GPUBuffer — copied to a staging buffer then converted to ImageData for the 2D canvas.
|
|
@@ -754,7 +890,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
754
890
|
|
|
755
891
|
const f32 = new Float32Array( staging.getMappedRange() );
|
|
756
892
|
const tileImageData = new ImageData( clampedW, clampedH );
|
|
757
|
-
const exposure = this.
|
|
893
|
+
const exposure = this._outputExposure();
|
|
758
894
|
const saturation = this.getSaturation();
|
|
759
895
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
760
896
|
const alpha = this._cachedAlpha;
|
|
@@ -856,7 +992,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
856
992
|
const float32 = new Float32Array( stagingBuffer.getMappedRange() );
|
|
857
993
|
|
|
858
994
|
const imageData = new ImageData( width, height );
|
|
859
|
-
const exposure = this.
|
|
995
|
+
const exposure = this._outputExposure();
|
|
860
996
|
const saturation = this.getSaturation();
|
|
861
997
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
862
998
|
const alpha = this._cachedAlpha;
|
|
@@ -962,6 +1098,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
962
1098
|
// TFJS tears down the cached backend's buffers/textures.
|
|
963
1099
|
removeOidnTfjsBackend();
|
|
964
1100
|
this._destroyGPUInputBuffers();
|
|
1101
|
+
this._colorScalePipeline = null;
|
|
965
1102
|
|
|
966
1103
|
// Clean up DOM
|
|
967
1104
|
if ( this.output?.parentNode ) {
|
package/src/PathTracerApp.js
CHANGED
|
@@ -442,16 +442,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
442
442
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
|
|
446
|
-
this.denoisingManager?.abort( this.canvas );
|
|
447
|
-
|
|
448
|
-
if ( this.denoisingManager?.restoreBaseResolution() ) {
|
|
449
|
-
|
|
450
|
-
const w = this.denoisingManager._lastRenderWidth;
|
|
451
|
-
const h = this.denoisingManager._lastRenderHeight;
|
|
452
|
-
this.dispatchEvent( { type: 'resolution_changed', width: w, height: h } );
|
|
453
|
-
|
|
454
|
-
}
|
|
445
|
+
this._abortPostProcess();
|
|
455
446
|
|
|
456
447
|
this.completion.reset();
|
|
457
448
|
this.wake();
|
|
@@ -1524,6 +1515,37 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1524
1515
|
|
|
1525
1516
|
}
|
|
1526
1517
|
|
|
1518
|
+
// Aborts any in-flight denoise/upscale and puts the denoiser canvas back at base resolution (the
|
|
1519
|
+
// upscaler leaves it enlarged), so the live canvas is what's on screen again.
|
|
1520
|
+
_abortPostProcess() {
|
|
1521
|
+
|
|
1522
|
+
this.denoisingManager?.abort( this.canvas );
|
|
1523
|
+
|
|
1524
|
+
if ( this.denoisingManager?.restoreBaseResolution() ) {
|
|
1525
|
+
|
|
1526
|
+
const w = this.denoisingManager._lastRenderWidth;
|
|
1527
|
+
const h = this.denoisingManager._lastRenderHeight;
|
|
1528
|
+
this.dispatchEvent( { type: 'resolution_changed', width: w, height: h } );
|
|
1529
|
+
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Re-runs the post-process chain (OIDN → upscaler) against the accumulated image. The chain fires once,
|
|
1536
|
+
* on the frame the render completes, so a denoiser switched on afterwards would otherwise never run.
|
|
1537
|
+
*/
|
|
1538
|
+
requestPostProcessRefresh() {
|
|
1539
|
+
|
|
1540
|
+
if ( ! this.stages.pathTracer?.isReady || this._deviceLost ) return;
|
|
1541
|
+
|
|
1542
|
+
this._abortPostProcess();
|
|
1543
|
+
|
|
1544
|
+
this.completion.renderCompleteDispatched = false;
|
|
1545
|
+
this.wake();
|
|
1546
|
+
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1527
1549
|
// ═══════════════════════════════════════════════════════════════
|
|
1528
1550
|
// Deterministic / headless control
|
|
1529
1551
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2408,6 +2430,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2408
2430
|
|
|
2409
2431
|
this.denoisingManager.setOverlayManager( this.overlayManager );
|
|
2410
2432
|
this.denoisingManager.setResetCallback( () => this.reset() );
|
|
2433
|
+
this.denoisingManager.setPostProcessRefreshCallback( () => this.requestPostProcessRefresh() );
|
|
2411
2434
|
this.denoisingManager.setSettings( this.settings );
|
|
2412
2435
|
|
|
2413
2436
|
// Expose environment manager (lives on pathTracer stage)
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -68,6 +68,10 @@ export class PathTracer extends PathTracerStage {
|
|
|
68
68
|
// of the point-sampled per-frame value that leaks noise. Off for fast/ASVGF (they want the bump normal).
|
|
69
69
|
this._cleanAuxNormalEnabled = false;
|
|
70
70
|
this._cleanAuxNormalUniform = uniform( 0, 'uint' );
|
|
71
|
+
// Aux MRT accumulates on an epoch of its own: enabling the denoiser at colour frame N must not leave
|
|
72
|
+
// the stale aux pinned by the colour's 1/(N+1) alpha.
|
|
73
|
+
this._auxSamples = 0;
|
|
74
|
+
this._auxSeedPending = false;
|
|
71
75
|
|
|
72
76
|
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
73
77
|
this._useDynamicDispatch = true;
|
|
@@ -238,8 +242,17 @@ export class PathTracer extends PathTracerStage {
|
|
|
238
242
|
|
|
239
243
|
if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
|
|
240
244
|
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
// Denoiser switched on after the render finished — spend one more frame to fill the aux MRT.
|
|
246
|
+
if ( this._auxSeedPending ) {
|
|
247
|
+
|
|
248
|
+
this.isComplete = false;
|
|
249
|
+
|
|
250
|
+
} else {
|
|
251
|
+
|
|
252
|
+
if ( ! this.isComplete ) this.isComplete = true;
|
|
253
|
+
return;
|
|
254
|
+
|
|
255
|
+
}
|
|
243
256
|
|
|
244
257
|
}
|
|
245
258
|
|
|
@@ -283,6 +296,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
this._updateAccumulationUniforms( frameValue, renderMode );
|
|
299
|
+
const auxMixes = this._updateAuxAccumulationUniforms( frameValue );
|
|
286
300
|
this.frame.value = frameValue;
|
|
287
301
|
this.seedFrame.value = this._seedTick ++;
|
|
288
302
|
|
|
@@ -354,7 +368,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
354
368
|
|
|
355
369
|
// Tier-2 bounce-0 grid size from the stale settled active-pixel count (single-chunk only; else full).
|
|
356
370
|
// Monotonic freeze ⇒ a stale count is a safe over-estimate; reset/camera-move → _curveSizingValid=false.
|
|
357
|
-
|
|
371
|
+
// An aux seed frame must trace every pixel: Generate/Shade skip the G-buffer for frozen ones.
|
|
372
|
+
const adaptiveFreeze = this.usePixelFreeze.value > 0 && ! this._auxSeedPending;
|
|
358
373
|
const activeBounce0 = ( singleChunk && adaptiveFreeze && this._curveSizingValid && this._lastActivePixelCount > 0 )
|
|
359
374
|
? this._lastActivePixelCount : maxRays;
|
|
360
375
|
|
|
@@ -488,6 +503,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
488
503
|
// Don't count interaction-mode (1-SPP feedback) frames toward completion (megakernel parity Stages/PathTracer.js:1240) — else a continuous orbit "completes" on noise.
|
|
489
504
|
if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
|
|
490
505
|
|
|
506
|
+
if ( this._auxGBufferEnabled ) this._auxSamples = auxMixes ? this._auxSamples + 1 : 1;
|
|
507
|
+
this._auxSeedPending = false;
|
|
508
|
+
|
|
491
509
|
if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
|
|
492
510
|
|
|
493
511
|
this.performanceMonitor?.end();
|
|
@@ -518,6 +536,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
518
536
|
this._convergedFraction = 0;
|
|
519
537
|
this._convergedGeometryFraction = 0;
|
|
520
538
|
this._lastActivePixelCount = 0;
|
|
539
|
+
this._auxSamples = 0;
|
|
540
|
+
this._auxSeedPending = false;
|
|
521
541
|
|
|
522
542
|
}
|
|
523
543
|
|
|
@@ -585,28 +605,62 @@ export class PathTracer extends PathTracerStage {
|
|
|
585
605
|
}
|
|
586
606
|
|
|
587
607
|
// Aux MRT (normalDepth/albedo) is needed only by the denoiser/OIDN; DenoisingManager calls this to
|
|
588
|
-
// turn the wavefront's aux writes on/off. It's a live uniform, so toggling is just a value flip
|
|
589
|
-
//
|
|
608
|
+
// turn the wavefront's aux writes on/off. It's a live uniform, so toggling is just a value flip —
|
|
609
|
+
// no kernel rebuild, no UI freeze.
|
|
590
610
|
setAuxGBufferEnabled( enabled ) {
|
|
591
611
|
|
|
592
612
|
enabled = !! enabled;
|
|
593
613
|
if ( this._auxGBufferEnabled === enabled ) return;
|
|
594
614
|
this._auxGBufferEnabled = enabled;
|
|
595
615
|
this._auxGBufferUniform.value = enabled ? 1 : 0;
|
|
596
|
-
this.
|
|
616
|
+
this.restartAuxAccumulation();
|
|
597
617
|
|
|
598
618
|
}
|
|
599
619
|
|
|
600
620
|
// Clean-aux normal: when a clean-aux OIDN model is active (calb_cnrm/high, alb_nrm/balanced), FinalWrite
|
|
601
621
|
// temporally accumulates + renormalizes the aux normal so the model isn't fed per-frame point-sampled
|
|
602
|
-
// noise. DenoisingManager calls this on OIDN enable + quality change.
|
|
622
|
+
// noise. DenoisingManager calls this on OIDN enable + quality change. The two modes write the normal in
|
|
623
|
+
// incompatible ways, so the aux buffer has to refill.
|
|
603
624
|
setCleanAuxNormal( enabled ) {
|
|
604
625
|
|
|
605
626
|
enabled = !! enabled;
|
|
606
627
|
if ( this._cleanAuxNormalEnabled === enabled ) return;
|
|
607
628
|
this._cleanAuxNormalEnabled = enabled;
|
|
608
629
|
this._cleanAuxNormalUniform.value = enabled ? 1 : 0;
|
|
609
|
-
this.
|
|
630
|
+
this.restartAuxAccumulation();
|
|
631
|
+
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Drops the aux MRT's history, leaving the colour's accumulation intact. */
|
|
635
|
+
restartAuxAccumulation() {
|
|
636
|
+
|
|
637
|
+
this._auxSamples = 0;
|
|
638
|
+
this._auxSeedPending = this._auxGBufferEnabled;
|
|
639
|
+
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* @returns {boolean} whether FinalWrite will MIX the aux this frame rather than seed it.
|
|
644
|
+
*
|
|
645
|
+
* colorAccumulates mirrors FinalWrite's outer gate: when that gate is false the aux is written verbatim
|
|
646
|
+
* regardless, which restarts the epoch. A mismatch costs aux quality only — the shader also reads
|
|
647
|
+
* hasPreviousAux, so it can never mix against textures this epoch has not written.
|
|
648
|
+
*/
|
|
649
|
+
_updateAuxAccumulationUniforms( frameValue ) {
|
|
650
|
+
|
|
651
|
+
const colorAccumulates = this.accumulationEnabled
|
|
652
|
+
&& ! this.cameraIsMoving.value
|
|
653
|
+
&& frameValue > 0
|
|
654
|
+
&& this.hasPreviousAccumulated.value > 0
|
|
655
|
+
&& this.visMode.value !== 11;
|
|
656
|
+
|
|
657
|
+
const mixes = this._auxGBufferEnabled && colorAccumulates
|
|
658
|
+
&& ! this._auxSeedPending && this._auxSamples > 0;
|
|
659
|
+
|
|
660
|
+
this.hasPreviousAux.value = mixes ? 1 : 0;
|
|
661
|
+
this.auxAccumulationAlpha.value = mixes ? 1 / ( this._auxSamples + 1 ) : 1.0;
|
|
662
|
+
|
|
663
|
+
return mixes;
|
|
610
664
|
|
|
611
665
|
}
|
|
612
666
|
|
|
@@ -1483,6 +1537,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
1483
1537
|
enableAccumulation: this.enableAccumulation,
|
|
1484
1538
|
hasPreviousAccumulated: this.hasPreviousAccumulated,
|
|
1485
1539
|
accumulationAlpha: this.accumulationAlpha,
|
|
1540
|
+
hasPreviousAux: this.hasPreviousAux,
|
|
1541
|
+
auxAccumulationAlpha: this.auxAccumulationAlpha,
|
|
1486
1542
|
cameraIsMoving: this.cameraIsMoving,
|
|
1487
1543
|
transparentBackground: this.transparentBackground,
|
|
1488
1544
|
prevAccumTexture: prevColor,
|
|
@@ -37,6 +37,8 @@ export function buildFinalWriteKernel( params ) {
|
|
|
37
37
|
writeColorTex, writeNDTex, writeAlbedoTex,
|
|
38
38
|
resolution, frame,
|
|
39
39
|
enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
|
|
40
|
+
// Aux MRT accumulates on its own epoch; hasPreviousAux=0 → write it verbatim instead of mixing.
|
|
41
|
+
hasPreviousAux, auxAccumulationAlpha,
|
|
40
42
|
transparentBackground,
|
|
41
43
|
prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
|
|
42
44
|
renderWidth,
|
|
@@ -113,10 +115,10 @@ export function buildFinalWriteKernel( params ) {
|
|
|
113
115
|
|
|
114
116
|
// Frozen pixels pass prev colour through unchanged (stale sample); active pixels accumulate.
|
|
115
117
|
finalColor.assign( select( wasFrozen, prevAccumSample.xyz, mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) ) );
|
|
116
|
-
If( auxOn, () => {
|
|
118
|
+
If( auxOn.and( hasPreviousAux ), () => {
|
|
117
119
|
|
|
118
120
|
// Albedo averages cleanly (it's a colour).
|
|
119
|
-
finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo,
|
|
121
|
+
finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, auxAccumulationAlpha ) );
|
|
120
122
|
|
|
121
123
|
// NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
|
|
122
124
|
// which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
|
|
@@ -129,7 +131,7 @@ export function buildFinalWriteKernel( params ) {
|
|
|
129
131
|
|
|
130
132
|
const prevN = texture( prevNormalDepthTexture, prevUV, 0 ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
131
133
|
const curN = finalNormalDepth.xyz.mul( 2.0 ).sub( 1.0 ).toVar();
|
|
132
|
-
const mixedN = mix( prevN, curN,
|
|
134
|
+
const mixedN = mix( prevN, curN, auxAccumulationAlpha ).toVar();
|
|
133
135
|
const len = length( mixedN );
|
|
134
136
|
const avgN = select( len.greaterThan( 1e-4 ), mixedN.div( len ), curN );
|
|
135
137
|
finalNormalDepth.assign( vec4( avgN.mul( 0.5 ).add( 0.5 ), finalNormalDepth.w ) );
|
|
@@ -50,6 +50,9 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
50
50
|
this.denoiser = null;
|
|
51
51
|
this.upscaler = null;
|
|
52
52
|
|
|
53
|
+
this._onReset = null;
|
|
54
|
+
this._onPostProcessRefresh = null;
|
|
55
|
+
|
|
53
56
|
// Resolution tracking — used for canvas restoration on reset
|
|
54
57
|
this._lastRenderWidth = 0;
|
|
55
58
|
this._lastRenderHeight = 0;
|
|
@@ -547,6 +550,13 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
547
550
|
|
|
548
551
|
}
|
|
549
552
|
|
|
553
|
+
/** @param {Function} fn - () => void, re-runs the completion chain on the accumulated image */
|
|
554
|
+
setPostProcessRefreshCallback( fn ) {
|
|
555
|
+
|
|
556
|
+
this._onPostProcessRefresh = fn;
|
|
557
|
+
|
|
558
|
+
}
|
|
559
|
+
|
|
550
560
|
/** @param {import('../RenderSettings.js').RenderSettings} settings */
|
|
551
561
|
setSettings( settings ) {
|
|
552
562
|
|
|
@@ -620,6 +630,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
620
630
|
if ( this.denoiser ) this.denoiser.enabled = enabled;
|
|
621
631
|
// OIDN reads the PathTracer aux MRT; re-sync so the wavefront produces it while OIDN is on.
|
|
622
632
|
this._syncGBufferStages();
|
|
633
|
+
this._onPostProcessRefresh?.();
|
|
623
634
|
|
|
624
635
|
}
|
|
625
636
|
|
|
@@ -629,6 +640,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
629
640
|
this.denoiser?.updateQuality( quality );
|
|
630
641
|
// Clean-aux normal follows the model: balanced/high (clean-aux models) → accumulate; fast → keep bump.
|
|
631
642
|
this._stages.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && quality !== 'fast' );
|
|
643
|
+
this._onPostProcessRefresh?.();
|
|
632
644
|
|
|
633
645
|
}
|
|
634
646
|
|
|
@@ -196,6 +196,10 @@ export class UniformManager {
|
|
|
196
196
|
ub( 'cameraIsMoving', false );
|
|
197
197
|
ub( 'hasPreviousAccumulated', false );
|
|
198
198
|
|
|
199
|
+
// Aux MRT epoch, separate from the colour's so the denoiser can be enabled mid-render.
|
|
200
|
+
u( 'auxAccumulationAlpha', 1.0, 'float' );
|
|
201
|
+
ub( 'hasPreviousAux', false );
|
|
202
|
+
|
|
199
203
|
// Environment
|
|
200
204
|
u( 'environmentIntensity', DEFAULT_STATE.environmentIntensity, 'float' );
|
|
201
205
|
u( 'backgroundIntensity', DEFAULT_STATE.backgroundIntensity, 'float' );
|