rayzee 7.30.1 → 7.31.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.
- package/README.md +113 -2
- package/dist/rayzee.es.js +589 -315
- package/dist/rayzee.umd.js +242 -17
- package/package.json +1 -1
- package/src/EngineDefaults.js +9 -1
- package/src/Passes/OIDNDenoiser.js +497 -347
- package/src/PathTracerApp.js +15 -2
- package/src/Processor/ToneMapWGSL.js +162 -0
- package/src/TSL/BVHTraversal.js +3 -1
- package/src/managers/DenoisingManager.js +251 -22
- package/src/managers/OverlayManager.js +4 -0
- package/src/managers/VideoRenderManager.js +5 -5
package/src/PathTracerApp.js
CHANGED
|
@@ -404,6 +404,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
404
404
|
if ( ! this.stages.pathTracer.isComplete ) {
|
|
405
405
|
|
|
406
406
|
this.completion.updateTime();
|
|
407
|
+
this.denoisingManager?.tickContinuousDenoise( this.stages.pathTracer.frameCount );
|
|
407
408
|
|
|
408
409
|
}
|
|
409
410
|
|
|
@@ -1797,8 +1798,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1797
1798
|
if ( denoiser ) {
|
|
1798
1799
|
|
|
1799
1800
|
denoiser.abort();
|
|
1800
|
-
denoiser.enabled
|
|
1801
|
-
denoiser.
|
|
1801
|
+
// Through the manager both times: `denoiser.enabled` is the union of its two jobs and
|
|
1802
|
+
// `denoiser.quality` dips to a cheaper model between refreshes, so neither is the
|
|
1803
|
+
// record of what the host asked for.
|
|
1804
|
+
this.denoisingManager.applyOIDNEnabled( config.enableOIDN );
|
|
1805
|
+
this.denoisingManager.applyOIDNQuality( config.oidnQuality );
|
|
1802
1806
|
|
|
1803
1807
|
}
|
|
1804
1808
|
|
|
@@ -1933,6 +1937,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1933
1937
|
dynamicDispatch: stage._useDynamicDispatch,
|
|
1934
1938
|
autoFocusMode: this.cameraManager?.autoFocusMode,
|
|
1935
1939
|
autoExposure: this.stages.autoExposure?.enabled,
|
|
1940
|
+
continuousDenoise: this.denoisingManager?.continuousDenoise,
|
|
1936
1941
|
};
|
|
1937
1942
|
|
|
1938
1943
|
}
|
|
@@ -1966,6 +1971,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1966
1971
|
|
|
1967
1972
|
this.cameraManager?.setAutoFocusMode( 'manual' );
|
|
1968
1973
|
if ( this.stages.autoExposure ) this.stages.autoExposure.enabled = false;
|
|
1974
|
+
// Cadence denoising is wall-clock driven, so which frame it lands on is not reproducible.
|
|
1975
|
+
this.denoisingManager?.setContinuousDenoise( false );
|
|
1969
1976
|
|
|
1970
1977
|
// The seed axis free-runs across accumulation resets so a camera drag gets fresh
|
|
1971
1978
|
// sequences; offline rendering needs the opposite. Pinning it makes seedFrame track
|
|
@@ -1990,6 +1997,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1990
1997
|
|
|
1991
1998
|
}
|
|
1992
1999
|
|
|
2000
|
+
if ( prev.continuousDenoise !== undefined ) {
|
|
2001
|
+
|
|
2002
|
+
this.denoisingManager?.setContinuousDenoise( prev.continuousDenoise );
|
|
2003
|
+
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1993
2006
|
stage._pinSeedToFrame = false;
|
|
1994
2007
|
|
|
1995
2008
|
this._deterministicRestore = null;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGSL tone mapping, a transcription of ToneMapCPU.js.
|
|
3
|
+
*
|
|
4
|
+
* The two must stay identical: the same image is produced on the CPU by `toneMapToRGBA8`
|
|
5
|
+
* (renderToBuffer, upscaler) and on the GPU by this (the OIDN output pack). Order is part of
|
|
6
|
+
* the contract — exposure, saturation, clamp-negative, curve, sRGB OETF — and moving any step
|
|
7
|
+
* shifts every mid-tone.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
11
|
+
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
12
|
+
} from 'three';
|
|
13
|
+
|
|
14
|
+
const SUPPORTED = new Set( [
|
|
15
|
+
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
16
|
+
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
17
|
+
] );
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Three.js ToneMapping constant → shader mode. Anything unsupported falls back to ACES,
|
|
21
|
+
* matching `TONE_MAP_FNS.get( x ) || TONE_MAP_FNS.get( ACESFilmicToneMapping )`.
|
|
22
|
+
* @param {number} toneMapping
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function toneMapMode( toneMapping ) {
|
|
26
|
+
|
|
27
|
+
return SUPPORTED.has( toneMapping ) ? toneMapping : ACESFilmicToneMapping;
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prelude defining `toneMapPixel( linear, gain, saturation, mode ) -> vec3f` (sRGB, [0,1]).
|
|
33
|
+
* Concatenate ahead of a kernel that wants it.
|
|
34
|
+
*/
|
|
35
|
+
export const TONE_MAP_WGSL = /* wgsl */`
|
|
36
|
+
fn tmSaturation( c: vec3f, saturation: f32 ) -> vec3f {
|
|
37
|
+
if ( saturation == 1.0 ) { return c; }
|
|
38
|
+
let luma = dot( c, vec3f( 0.2126, 0.7152, 0.0722 ) );
|
|
39
|
+
return vec3f( luma ) + ( c - vec3f( luma ) ) * saturation;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn tmSrgb1( c: f32 ) -> f32 {
|
|
43
|
+
if ( c <= 0.0031308 ) { return 12.92 * c; }
|
|
44
|
+
return 1.055 * pow( c, 1.0 / 2.4 ) - 0.055;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn tmSrgb( c: vec3f ) -> vec3f {
|
|
48
|
+
return vec3f( tmSrgb1( c.r ), tmSrgb1( c.g ), tmSrgb1( c.b ) );
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn tmReinhard( c: vec3f ) -> vec3f {
|
|
52
|
+
return clamp( c / ( c + vec3f( 1.0 ) ), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn tmCineon1( x: f32 ) -> f32 {
|
|
56
|
+
let c = max( x - 0.004, 0.0 );
|
|
57
|
+
return pow( ( c * ( 6.2 * c + 0.5 ) ) / ( c * ( 6.2 * c + 1.7 ) + 0.06 ), 2.2 );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn tmCineon( c: vec3f ) -> vec3f {
|
|
61
|
+
return vec3f( tmCineon1( c.r ), tmCineon1( c.g ), tmCineon1( c.b ) );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn tmAcesFit( c: f32 ) -> f32 {
|
|
65
|
+
return ( c * ( c + 0.0245786 ) - 0.000090537 ) / ( c * ( 0.983729 * c + 0.4329510 ) + 0.238081 );
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn tmAces( cIn: vec3f ) -> vec3f {
|
|
69
|
+
let c = cIn / 0.6;
|
|
70
|
+
var i = vec3f(
|
|
71
|
+
0.59719 * c.r + 0.35458 * c.g + 0.04823 * c.b,
|
|
72
|
+
0.07600 * c.r + 0.90834 * c.g + 0.01566 * c.b,
|
|
73
|
+
0.02840 * c.r + 0.13383 * c.g + 0.83777 * c.b
|
|
74
|
+
);
|
|
75
|
+
i = vec3f( tmAcesFit( i.r ), tmAcesFit( i.g ), tmAcesFit( i.b ) );
|
|
76
|
+
let o = vec3f(
|
|
77
|
+
1.60475 * i.r - 0.53108 * i.g - 0.07367 * i.b,
|
|
78
|
+
-0.10208 * i.r + 1.10813 * i.g - 0.00605 * i.b,
|
|
79
|
+
-0.00327 * i.r - 0.07276 * i.g + 1.07602 * i.b
|
|
80
|
+
);
|
|
81
|
+
return clamp( o, vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn tmAgxApprox( x: f32 ) -> f32 {
|
|
85
|
+
let x2 = x * x;
|
|
86
|
+
let x4 = x2 * x2;
|
|
87
|
+
return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn tmAgx( r: vec3f ) -> vec3f {
|
|
91
|
+
let c = vec3f(
|
|
92
|
+
0.6274 * r.r + 0.3293 * r.g + 0.0433 * r.b,
|
|
93
|
+
0.0691 * r.r + 0.9195 * r.g + 0.0113 * r.b,
|
|
94
|
+
0.0164 * r.r + 0.0880 * r.g + 0.8956 * r.b
|
|
95
|
+
);
|
|
96
|
+
var a = vec3f(
|
|
97
|
+
0.856627153315983 * c.r + 0.0951212405381588 * c.g + 0.0482516061458583 * c.b,
|
|
98
|
+
0.137318972929847 * c.r + 0.761241990602591 * c.g + 0.101439036467562 * c.b,
|
|
99
|
+
0.11189821299995 * c.r + 0.0767994186031903 * c.g + 0.811302368396859 * c.b
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let agxMinEv = -12.47393;
|
|
103
|
+
let agxMaxEv = 4.026069;
|
|
104
|
+
let range = agxMaxEv - agxMinEv;
|
|
105
|
+
a = clamp( ( log2( max( a, vec3f( 1e-10 ) ) ) - vec3f( agxMinEv ) ) / range, vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
106
|
+
a = vec3f( tmAgxApprox( a.r ), tmAgxApprox( a.g ), tmAgxApprox( a.b ) );
|
|
107
|
+
|
|
108
|
+
var o = vec3f(
|
|
109
|
+
1.1271005818144368 * a.r - 0.11060664309660323 * a.g - 0.016493938717834573 * a.b,
|
|
110
|
+
-0.1413297634984383 * a.r + 1.157823702216272 * a.g - 0.016493938717834257 * a.b,
|
|
111
|
+
-0.14132976349843826 * a.r - 0.11060664309660294 * a.g + 1.2519364065950405 * a.b
|
|
112
|
+
);
|
|
113
|
+
o = pow( max( o, vec3f( 0.0 ) ), vec3f( 2.2 ) );
|
|
114
|
+
|
|
115
|
+
return clamp( vec3f(
|
|
116
|
+
1.6605 * o.r - 0.5876 * o.g - 0.0728 * o.b,
|
|
117
|
+
-0.1246 * o.r + 1.1329 * o.g - 0.0083 * o.b,
|
|
118
|
+
-0.0182 * o.r - 0.1006 * o.g + 1.1187 * o.b
|
|
119
|
+
), vec3f( 0.0 ), vec3f( 1.0 ) );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn tmNeutral( cIn: vec3f ) -> vec3f {
|
|
123
|
+
let startCompression = 0.8 - 0.04;
|
|
124
|
+
let desaturation = 0.15;
|
|
125
|
+
|
|
126
|
+
let x = min( cIn.r, min( cIn.g, cIn.b ) );
|
|
127
|
+
var offset = 0.04;
|
|
128
|
+
if ( x < 0.08 ) { offset = x - 6.25 * x * x; }
|
|
129
|
+
|
|
130
|
+
var c = cIn - vec3f( offset );
|
|
131
|
+
let peak = max( c.r, max( c.g, c.b ) );
|
|
132
|
+
if ( peak < startCompression ) { return c; }
|
|
133
|
+
|
|
134
|
+
let d = 1.0 - startCompression;
|
|
135
|
+
let newPeak = 1.0 - d * d / ( peak + d - startCompression );
|
|
136
|
+
c = c * ( newPeak / peak );
|
|
137
|
+
let gFactor = 1.0 - 1.0 / ( desaturation * ( peak - newPeak ) + 1.0 );
|
|
138
|
+
|
|
139
|
+
return c + ( vec3f( newPeak ) - c ) * gFactor;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn tmCurve( cIn: vec3f, mode: u32 ) -> vec3f {
|
|
143
|
+
// Three.js clamps the fragment output before tone mapping ("force unsigned floats"), and
|
|
144
|
+
// AgX/Neutral mix negatives across channels rather than clipping them.
|
|
145
|
+
let c = max( cIn, vec3f( 0.0 ) );
|
|
146
|
+
switch mode {
|
|
147
|
+
case 0u, 1u: { return clamp( c, vec3f( 0.0 ), vec3f( 1.0 ) ); }
|
|
148
|
+
case 2u: { return tmReinhard( c ); }
|
|
149
|
+
case 3u: { return tmCineon( c ); }
|
|
150
|
+
case 6u: { return tmAgx( c ); }
|
|
151
|
+
case 7u: { return tmNeutral( c ); }
|
|
152
|
+
default: { return tmAces( c ); }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn toneMapPixel( linear: vec3f, gain: f32, saturation: f32, mode: u32 ) -> vec3f {
|
|
157
|
+
var c = linear * gain;
|
|
158
|
+
c = tmSaturation( c, saturation );
|
|
159
|
+
c = tmCurve( c, mode );
|
|
160
|
+
return tmSrgb( c );
|
|
161
|
+
}
|
|
162
|
+
`;
|
package/src/TSL/BVHTraversal.js
CHANGED
|
@@ -24,7 +24,9 @@ import { HitInfo } from './Struct.js';
|
|
|
24
24
|
import { getDatafromStorageBuffer } from './Common.js';
|
|
25
25
|
|
|
26
26
|
const MAX_STACK_DEPTH = 32;
|
|
27
|
-
|
|
27
|
+
// Hang guard only — tripping it reports a MISS, which the shade kernel pays out as
|
|
28
|
+
// full-intensity environment. San Miguel needs ~1024; cost is flat from 1024 up.
|
|
29
|
+
const MAX_BVH_ITERATIONS = 4096;
|
|
28
30
|
const BVH_STRIDE = 4;
|
|
29
31
|
const TRI_STRIDE = 8;
|
|
30
32
|
const HUGE_VAL = 1e8;
|
|
@@ -4,6 +4,29 @@ import { AIUpscaler } from '../Passes/AIUpscaler.js';
|
|
|
4
4
|
import { EngineEvents } from '../EngineEvents.js';
|
|
5
5
|
import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS } from '../EngineDefaults.js';
|
|
6
6
|
|
|
7
|
+
// A refresh slower than this is a slideshow, not a live view, so the cadence swaps to a cheaper
|
|
8
|
+
// model and puts the chosen one back for the finished image. Resolution-aware by construction:
|
|
9
|
+
// at 512² every tier is under budget, at 1024² only `high` trips it.
|
|
10
|
+
//
|
|
11
|
+
// This replaced a second, time-based trigger ("downgrade once the render passes 2 s"), which
|
|
12
|
+
// measured identically at 1024² for balance and high because the cost rule had already tripped —
|
|
13
|
+
// but which also downgraded tiers that were affordable, on machines fast enough to run them.
|
|
14
|
+
const CADENCE_COST_BUDGET_MS = 120;
|
|
15
|
+
|
|
16
|
+
// Start-to-start gap as a multiple of what the last denoise actually cost. This is the whole
|
|
17
|
+
// throttle: it spends half the wall clock on denoising at most, at any resolution, on any GPU.
|
|
18
|
+
// A fixed millisecond floor cannot do that — the same tier measures 14 ms at 512², 48 ms at
|
|
19
|
+
// 1024² and 800 ms at 2048².
|
|
20
|
+
//
|
|
21
|
+
// It replaced a sample-growth rule (refresh only once the sample count had grown 1.4x), which
|
|
22
|
+
// was written when a denoise cost 100-330 ms. Measured after the denoise got cheap, that rule
|
|
23
|
+
// only ever cost refreshes: 512² 2/s -> 32/s and 1024² 2/s -> 8.6/s, both at unchanged sample
|
|
24
|
+
// rate, while 1536²/2048² did not move at all because this floor already bound there.
|
|
25
|
+
//
|
|
26
|
+
// 3x measured 97 % of the sample rate at 1536² against 89 % for 2x, but at a quarter of the
|
|
27
|
+
// refreshes — 2x is the better trade while a person is watching the image resolve.
|
|
28
|
+
const CADENCE_DENOISE_PERIODS = 2;
|
|
29
|
+
|
|
7
30
|
/**
|
|
8
31
|
* Orchestrates all denoising, post-processing, and AI upscaling:
|
|
9
32
|
* - Real-time denoiser strategy switching (ASVGF / NRD / EdgeAware / None)
|
|
@@ -50,6 +73,19 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
50
73
|
this.denoiser = null;
|
|
51
74
|
this.upscaler = null;
|
|
52
75
|
|
|
76
|
+
// The tier the finished image uses. The loaded tier is not always this one: while the
|
|
77
|
+
// image is still accumulating we run a cheaper model (see previewQuality).
|
|
78
|
+
this._finalQuality = DEFAULT_STATE.oidnQuality;
|
|
79
|
+
// Two independent decisions. `finalDenoise` is the OIDN pass on the finished image;
|
|
80
|
+
// `continuousDenoise` is OIDN as the live-view denoiser. `denoiser.enabled` means only
|
|
81
|
+
// "OIDN is in use at all", which is what the aux G-buffer wiring needs.
|
|
82
|
+
this.finalDenoise = DEFAULT_STATE.enableOIDN;
|
|
83
|
+
this.continuousDenoise = DEFAULT_STATE.continuousDenoise;
|
|
84
|
+
this.continuousDenoiseInterval = DEFAULT_STATE.continuousDenoiseInterval;
|
|
85
|
+
// -Infinity, not 0: 0 reads as "denoised at time zero", which blocks the first cadence
|
|
86
|
+
// denoise while performance.now() is still below the interval.
|
|
87
|
+
this._lastCadenceAt = - Infinity;
|
|
88
|
+
this._lastCadenceSamples = 0;
|
|
53
89
|
this._onReset = null;
|
|
54
90
|
this._onPostProcessRefresh = null;
|
|
55
91
|
|
|
@@ -57,8 +93,9 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
57
93
|
this._lastRenderWidth = 0;
|
|
58
94
|
this._lastRenderHeight = 0;
|
|
59
95
|
|
|
60
|
-
// Track the current completion-chain
|
|
96
|
+
// Track the current completion-chain listeners so they can be removed on re-trigger
|
|
61
97
|
this._pendingStartUpscaler = null;
|
|
98
|
+
this._pendingCloseDenoise = null;
|
|
62
99
|
|
|
63
100
|
// Bound event forwarding handlers (stored for removal on re-setup / dispose)
|
|
64
101
|
this._denoiserStartHandler = null;
|
|
@@ -97,6 +134,11 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
97
134
|
|
|
98
135
|
this._lastRenderWidth = width;
|
|
99
136
|
this._lastRenderHeight = height;
|
|
137
|
+
// Sticky across resets: the device does not get faster between camera moves, and
|
|
138
|
+
// re-deciding per accumulation cost one slow denoise every time the camera stopped.
|
|
139
|
+
// Cleared only when the tier or the resolution changes — a denoise costs 14 ms at 512²
|
|
140
|
+
// and 800 ms at 2048², so the affordability verdict does not survive a resize.
|
|
141
|
+
this._cadenceDowngraded = false;
|
|
100
142
|
this.denoiser?.setSize( width, height );
|
|
101
143
|
this.upscaler?.setBaseSize( width, height );
|
|
102
144
|
|
|
@@ -162,16 +204,15 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
162
204
|
getToneMapping: () => this._getToneMapping(),
|
|
163
205
|
getSaturation: () => this._getSaturation(),
|
|
164
206
|
getTransparentBackground: () => this._getTransparentBg(),
|
|
165
|
-
getMRTRenderTarget: () => pt?.storageTextures?.readTarget ?? null,
|
|
166
207
|
} );
|
|
167
208
|
|
|
168
|
-
this.
|
|
209
|
+
this._syncOIDNInUse();
|
|
169
210
|
|
|
170
211
|
// Forward lifecycle events (store refs for removal on re-setup / dispose)
|
|
171
|
-
this._denoiserStartHandler =
|
|
172
|
-
this.dispatchEvent( { type: EngineEvents.DENOISING_START } );
|
|
173
|
-
this._denoiserEndHandler =
|
|
174
|
-
this.dispatchEvent( { type: EngineEvents.DENOISING_END } );
|
|
212
|
+
this._denoiserStartHandler = e =>
|
|
213
|
+
this.dispatchEvent( { type: EngineEvents.DENOISING_START, continuous: !! e.continuous } );
|
|
214
|
+
this._denoiserEndHandler = e =>
|
|
215
|
+
this.dispatchEvent( { type: EngineEvents.DENOISING_END, continuous: !! e.continuous } );
|
|
175
216
|
this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
|
|
176
217
|
this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
|
|
177
218
|
|
|
@@ -244,6 +285,10 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
244
285
|
*/
|
|
245
286
|
get denoiserStrategy() {
|
|
246
287
|
|
|
288
|
+
// OIDN refreshing the accumulating image is a live-view denoiser like the others, so it
|
|
289
|
+
// belongs in the same one-of-N choice. Two of these running at once would mean paying for
|
|
290
|
+
// a per-frame denoise whose result the OIDN overlay then covers.
|
|
291
|
+
if ( this.continuousDenoise ) return 'oidn';
|
|
247
292
|
if ( this._stages.asvgf?.enabled ) return 'asvgf';
|
|
248
293
|
if ( this._stages.nrd?.enabled ) return 'nrd';
|
|
249
294
|
if ( this._stages.edgeFilter?.enabled ) return 'edgeaware';
|
|
@@ -269,6 +314,10 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
269
314
|
|
|
270
315
|
this._clearDenoiserTextures();
|
|
271
316
|
|
|
317
|
+
// Which denoiser owns the live view. Deliberately says nothing about the finished image:
|
|
318
|
+
// wanting OIDN on the viewport is not the same as opting into a final pass.
|
|
319
|
+
this.setContinuousDenoise( strategy === 'oidn' );
|
|
320
|
+
|
|
272
321
|
switch ( strategy ) {
|
|
273
322
|
|
|
274
323
|
case 'asvgf':
|
|
@@ -422,7 +471,11 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
422
471
|
|
|
423
472
|
// Clean-aux normal: temporally accumulate the aux normal only for OIDN clean-aux models
|
|
424
473
|
// (balanced/high). 'fast' and the real-time denoisers keep the point-sampled bump normal.
|
|
425
|
-
|
|
474
|
+
// Keyed on the chosen tier, not the loaded one — the loaded tier dips to a cheaper model
|
|
475
|
+
// during refreshes, and following that would restart the aux accumulation mid-render.
|
|
476
|
+
s.pathTracer?.setCleanAuxNormal?.(
|
|
477
|
+
!! this.denoiser?.enabled && !! this.denoiser?.expectsCleanAux( this._finalQuality )
|
|
478
|
+
);
|
|
426
479
|
|
|
427
480
|
// Reclaim VRAM: free the big 2048² StorageTextures of any denoiser/G-buffer stage that ended up
|
|
428
481
|
// disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
|
|
@@ -449,13 +502,99 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
449
502
|
*/
|
|
450
503
|
_cleanupCompletionListener() {
|
|
451
504
|
|
|
452
|
-
if ( this.
|
|
505
|
+
if ( this.denoiser ) {
|
|
453
506
|
|
|
454
|
-
this.denoiser.removeEventListener( 'end', this._pendingStartUpscaler );
|
|
507
|
+
if ( this._pendingStartUpscaler ) this.denoiser.removeEventListener( 'end', this._pendingStartUpscaler );
|
|
508
|
+
if ( this._pendingCloseDenoise ) this.denoiser.removeEventListener( 'end', this._pendingCloseDenoise );
|
|
455
509
|
|
|
456
510
|
}
|
|
457
511
|
|
|
458
512
|
this._pendingStartUpscaler = null;
|
|
513
|
+
this._pendingCloseDenoise = null;
|
|
514
|
+
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Denoises the accumulating mean on a cadence, so a preview shows a clean image while it
|
|
519
|
+
* refines instead of only once it finishes. Driven from the render loop; call every frame.
|
|
520
|
+
*
|
|
521
|
+
* @param {number} sampleCount - accumulated samples (PathTracer.frameCount)
|
|
522
|
+
* @returns {boolean} whether a denoise was started this call
|
|
523
|
+
*/
|
|
524
|
+
tickContinuousDenoise( sampleCount ) {
|
|
525
|
+
|
|
526
|
+
const dn = this.denoiser;
|
|
527
|
+
if ( ! this.continuousDenoise || ! dn ) return false;
|
|
528
|
+
if ( dn.state.isDenoising || dn.state.isLoading ) return false;
|
|
529
|
+
|
|
530
|
+
// While the camera moves, reset() hides the output every frame and a denoise would paint
|
|
531
|
+
// a view that is already stale. Denoise once the motion stops.
|
|
532
|
+
if ( this._stages.pathTracer?.interactionMode ) return false;
|
|
533
|
+
|
|
534
|
+
// Nothing new has landed since the last refresh, so it would repaint the same image.
|
|
535
|
+
if ( sampleCount <= this._lastCadenceSamples ) return false;
|
|
536
|
+
|
|
537
|
+
const now = performance.now();
|
|
538
|
+
// `continuousDenoiseInterval` is only the floor's lower bound: on a cheap denoise it is
|
|
539
|
+
// what binds, and past ~1024² the denoise's own cost is.
|
|
540
|
+
const minGap = Math.max( this.continuousDenoiseInterval, dn.lastDenoiseMs * CADENCE_DENOISE_PERIODS );
|
|
541
|
+
if ( now - this._lastCadenceAt < minGap ) return false;
|
|
542
|
+
|
|
543
|
+
this._lastCadenceAt = now;
|
|
544
|
+
this._lastCadenceSamples = sampleCount;
|
|
545
|
+
|
|
546
|
+
// A tier too slow to be a live view refreshes with a cheaper model and is put back for the
|
|
547
|
+
// finished image. One-way: letting it flip back would reload weights on every tick.
|
|
548
|
+
if ( dn.lastDenoiseMs > CADENCE_COST_BUDGET_MS ) this._cadenceDowngraded = true;
|
|
549
|
+
|
|
550
|
+
// updateQuality flags the load synchronously, and start() below defers itself until the
|
|
551
|
+
// weights land.
|
|
552
|
+
const want = this._cadenceDowngraded ? this.previewQuality() : this._finalQuality;
|
|
553
|
+
if ( dn.quality !== want ) dn.updateQuality( want );
|
|
554
|
+
|
|
555
|
+
dn.start( { continuous: true } );
|
|
556
|
+
return true;
|
|
557
|
+
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The tier used for refreshes while the image is still accumulating: the cheapest model that
|
|
562
|
+
* reads the same kind of aux buffer as the final one.
|
|
563
|
+
*
|
|
564
|
+
* Matching the aux kind is not optional. `setCleanAuxNormal()` throws away the accumulated
|
|
565
|
+
* albedo/normal, so if the refreshes and the final denoise disagreed about it, the final
|
|
566
|
+
* denoise would run against an aux buffer one sample deep.
|
|
567
|
+
*/
|
|
568
|
+
previewQuality() {
|
|
569
|
+
|
|
570
|
+
return this.denoiser?.expectsCleanAux( this._finalQuality ) ? 'fast-clean' : 'fast';
|
|
571
|
+
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
setContinuousDenoise( enabled ) {
|
|
575
|
+
|
|
576
|
+
this.continuousDenoise = !! enabled;
|
|
577
|
+
this._syncOIDNInUse();
|
|
578
|
+
this._lastCadenceAt = - Infinity;
|
|
579
|
+
this._resetCadence();
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Puts the finished image back on the tier the user asked for, after the refreshes ran a
|
|
584
|
+
// cheaper one. A no-op when they match, which is the common case in preview.
|
|
585
|
+
_useFinalQuality() {
|
|
586
|
+
|
|
587
|
+
const dn = this.denoiser;
|
|
588
|
+
if ( dn && dn.quality !== this._finalQuality ) dn.updateQuality( this._finalQuality );
|
|
589
|
+
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Only the sample gate resets, so a fresh accumulation denoises promptly. The clock is
|
|
593
|
+
// deliberately kept: reset() runs every frame of a camera drag, and rearming it there
|
|
594
|
+
// would spend a denoise per frame on views that are already stale.
|
|
595
|
+
_resetCadence() {
|
|
596
|
+
|
|
597
|
+
this._lastCadenceSamples = 0;
|
|
459
598
|
|
|
460
599
|
}
|
|
461
600
|
|
|
@@ -464,9 +603,16 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
464
603
|
// Remove any stale completion-chain listener from a previous render cycle
|
|
465
604
|
this._cleanupCompletionListener();
|
|
466
605
|
|
|
467
|
-
//
|
|
606
|
+
// What closes the render, if anything. A full pass at the chosen tier when that switch is
|
|
607
|
+
// on; otherwise, if OIDN owns the live view, one last cheap refresh — the cadence's last
|
|
608
|
+
// tick lands a few samples short, and the picture should match the render that finished.
|
|
609
|
+
const closing = this.finalDenoise ? 'final' : this.continuousDenoise ? 'refresh' : null;
|
|
610
|
+
|
|
611
|
+
// Registered only once the closing denoise has been launched, so the next 'end' is
|
|
612
|
+
// unambiguously that one — a cadence run's end can look identical otherwise.
|
|
468
613
|
const startUpscaler = () => {
|
|
469
614
|
|
|
615
|
+
this.denoiser?.removeEventListener( 'end', startUpscaler );
|
|
470
616
|
this._pendingStartUpscaler = null;
|
|
471
617
|
|
|
472
618
|
if ( ! isStillComplete() ) return;
|
|
@@ -479,15 +625,58 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
479
625
|
|
|
480
626
|
};
|
|
481
627
|
|
|
482
|
-
if (
|
|
628
|
+
if ( ! closing ) {
|
|
629
|
+
|
|
630
|
+
startUpscaler();
|
|
631
|
+
return;
|
|
632
|
+
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const launchClosing = () => {
|
|
636
|
+
|
|
637
|
+
if ( ! isStillComplete() ) return;
|
|
483
638
|
|
|
484
639
|
this._pendingStartUpscaler = startUpscaler;
|
|
485
|
-
this.denoiser.addEventListener( 'end', startUpscaler
|
|
486
|
-
|
|
640
|
+
this.denoiser.addEventListener( 'end', startUpscaler );
|
|
641
|
+
|
|
642
|
+
if ( closing === 'final' ) {
|
|
643
|
+
|
|
644
|
+
this._useFinalQuality();
|
|
645
|
+
this.denoiser.start();
|
|
646
|
+
|
|
647
|
+
} else {
|
|
648
|
+
|
|
649
|
+
// Whatever model the refreshes have been using — no reload for a picture the user
|
|
650
|
+
// never asked to be denoised at full quality.
|
|
651
|
+
this.denoiser.start( { continuous: true } );
|
|
652
|
+
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
this._resetCadence();
|
|
658
|
+
|
|
659
|
+
if ( this.denoiser.state.isDenoising ) {
|
|
660
|
+
|
|
661
|
+
// A cadence run is mid-flight against a lower sample count. Let it finish first —
|
|
662
|
+
// start() would be refused right now.
|
|
663
|
+
// three.js EventDispatcher.addEventListener takes (type, listener) and silently
|
|
664
|
+
// ignores an options object, so a listener that restarts the denoiser MUST remove
|
|
665
|
+
// itself: `{ once: true }` here re-fired on its own end, forever.
|
|
666
|
+
const onCadenceEnd = () => {
|
|
667
|
+
|
|
668
|
+
this.denoiser?.removeEventListener( 'end', onCadenceEnd );
|
|
669
|
+
if ( this._pendingCloseDenoise === onCadenceEnd ) this._pendingCloseDenoise = null;
|
|
670
|
+
launchClosing();
|
|
671
|
+
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
this._pendingCloseDenoise = onCadenceEnd;
|
|
675
|
+
this.denoiser.addEventListener( 'end', onCadenceEnd );
|
|
487
676
|
|
|
488
677
|
} else {
|
|
489
678
|
|
|
490
|
-
|
|
679
|
+
launchClosing();
|
|
491
680
|
|
|
492
681
|
}
|
|
493
682
|
|
|
@@ -510,9 +699,12 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
510
699
|
|
|
511
700
|
if ( this.denoiser.enabled ) this.denoiser.abort();
|
|
512
701
|
if ( this.denoiser.output ) this.denoiser.output.style.display = 'none';
|
|
702
|
+
this.denoiser.invalidateLatch();
|
|
513
703
|
|
|
514
704
|
}
|
|
515
705
|
|
|
706
|
+
this._resetCadence();
|
|
707
|
+
|
|
516
708
|
}
|
|
517
709
|
|
|
518
710
|
dispose() {
|
|
@@ -647,23 +839,60 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
647
839
|
// ── OIDN ─────────────────────────────────────────────────────
|
|
648
840
|
|
|
649
841
|
/** Enables or disables Intel OIDN denoiser. */
|
|
842
|
+
/**
|
|
843
|
+
* Records the final-pass decision and syncs `enabled`. No host refresh — for callers like
|
|
844
|
+
* configureForMode that drive the whole change themselves.
|
|
845
|
+
*/
|
|
846
|
+
applyOIDNEnabled( enabled ) {
|
|
847
|
+
|
|
848
|
+
this.finalDenoise = !! enabled;
|
|
849
|
+
this._syncOIDNInUse();
|
|
850
|
+
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** The tier the finished image uses — not `denoiser.quality`, which dips during refreshes. */
|
|
854
|
+
get oidnQuality() {
|
|
855
|
+
|
|
856
|
+
return this._finalQuality;
|
|
857
|
+
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Denoise the finished image with OIDN. Independent of which denoiser owns the live view. */
|
|
650
861
|
setOIDNEnabled( enabled ) {
|
|
651
862
|
|
|
652
|
-
|
|
863
|
+
this.applyOIDNEnabled( enabled );
|
|
653
864
|
// OIDN reads the PathTracer aux MRT; re-sync so the wavefront produces it while OIDN is on.
|
|
654
865
|
this._syncGBufferStages();
|
|
655
866
|
this._onPostProcessRefresh?.();
|
|
656
867
|
|
|
657
868
|
}
|
|
658
869
|
|
|
659
|
-
|
|
660
|
-
|
|
870
|
+
// `enabled` on the denoiser is the union of its two jobs — everything downstream (the aux MRT,
|
|
871
|
+
// VRAM retention) only needs to know whether OIDN runs at all.
|
|
872
|
+
_syncOIDNInUse() {
|
|
873
|
+
|
|
874
|
+
if ( this.denoiser ) this.denoiser.enabled = this.finalDenoise || this.continuousDenoise;
|
|
875
|
+
|
|
876
|
+
}
|
|
661
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Records the tier the finished image should use, and loads it. No host refresh — for callers
|
|
880
|
+
* like configureForMode that drive the whole tier change themselves.
|
|
881
|
+
*/
|
|
882
|
+
applyOIDNQuality( quality ) {
|
|
883
|
+
|
|
884
|
+
this._finalQuality = quality;
|
|
885
|
+
this._cadenceDowngraded = false;
|
|
662
886
|
this.denoiser?.updateQuality( quality );
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
887
|
+
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
setOIDNQuality( quality ) {
|
|
891
|
+
|
|
892
|
+
this.applyOIDNQuality( quality );
|
|
893
|
+
// _syncGBufferStages owns the clean-aux decision (it follows the model, not the tier name —
|
|
894
|
+
// see OIDNDenoiser.QUALITY_MODELS). Inlining a second copy here let the two disagree.
|
|
895
|
+
this._syncGBufferStages();
|
|
667
896
|
this._onPostProcessRefresh?.();
|
|
668
897
|
|
|
669
898
|
}
|
|
@@ -179,6 +179,10 @@ export class OverlayManager {
|
|
|
179
179
|
|
|
180
180
|
source.addEventListener( 'tileProgress', ( e ) => {
|
|
181
181
|
|
|
182
|
+
// A cadence denoise of the still-accumulating preview runs many times a second and
|
|
183
|
+
// is a single tile — showing the border for those just strobes it.
|
|
184
|
+
if ( e.continuous ) return;
|
|
185
|
+
|
|
182
186
|
if ( e.tile ) {
|
|
183
187
|
|
|
184
188
|
tileHelper.setRenderSize( e.imageWidth, e.imageHeight );
|
|
@@ -123,7 +123,7 @@ export class VideoRenderManager {
|
|
|
123
123
|
if ( this._cancelled ) break;
|
|
124
124
|
|
|
125
125
|
// 4. Denoise if enabled
|
|
126
|
-
if ( enableOIDN && app.denoisingManager?.
|
|
126
|
+
if ( enableOIDN && app.denoisingManager?.finalDenoise ) {
|
|
127
127
|
|
|
128
128
|
await this._waitForDenoise( app );
|
|
129
129
|
|
|
@@ -280,8 +280,8 @@ export class VideoRenderManager {
|
|
|
280
280
|
transmissiveBounces: app.settings.get( 'transmissiveBounces' ),
|
|
281
281
|
renderMode: app.stages.pathTracer?.renderMode?.value,
|
|
282
282
|
controlsEnabled: app.cameraManager.controls?.enabled,
|
|
283
|
-
oidnEnabled: app.denoisingManager?.
|
|
284
|
-
oidnQuality: app.denoisingManager?.
|
|
283
|
+
oidnEnabled: app.denoisingManager?.finalDenoise,
|
|
284
|
+
oidnQuality: app.denoisingManager?.oidnQuality,
|
|
285
285
|
wasPlaying: app.animationManager?.isPlaying,
|
|
286
286
|
pauseRendering: app.pauseRendering,
|
|
287
287
|
};
|
|
@@ -314,8 +314,8 @@ export class VideoRenderManager {
|
|
|
314
314
|
|
|
315
315
|
if ( app.denoisingManager?.denoiser ) {
|
|
316
316
|
|
|
317
|
-
app.denoisingManager.
|
|
318
|
-
if ( state.oidnQuality ) app.denoisingManager.
|
|
317
|
+
app.denoisingManager.applyOIDNEnabled( state.oidnEnabled ?? false );
|
|
318
|
+
if ( state.oidnQuality ) app.denoisingManager.applyOIDNQuality( state.oidnQuality );
|
|
319
319
|
|
|
320
320
|
}
|
|
321
321
|
|