rayzee 6.4.0 → 7.0.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.
Files changed (58) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +4953 -4225
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +29 -13
  8. package/src/PathTracerApp.js +119 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +22 -1
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -316
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/TextureCreator.js +6 -0
  21. package/src/Processor/VRAMTracker.js +169 -0
  22. package/src/Processor/utils.js +11 -110
  23. package/src/RenderSettings.js +1 -3
  24. package/src/Stages/ASVGF.js +76 -20
  25. package/src/Stages/BilateralFilter.js +34 -10
  26. package/src/Stages/EdgeFilter.js +2 -3
  27. package/src/Stages/MotionVector.js +16 -9
  28. package/src/Stages/NormalDepth.js +17 -5
  29. package/src/Stages/PathTracer.js +671 -1456
  30. package/src/Stages/PathTracerStage.js +1451 -0
  31. package/src/Stages/SSRC.js +32 -15
  32. package/src/Stages/Variance.js +35 -12
  33. package/src/TSL/BVHTraversal.js +7 -1
  34. package/src/TSL/Common.js +12 -2
  35. package/src/TSL/CompactKernel.js +110 -0
  36. package/src/TSL/DebugKernel.js +98 -0
  37. package/src/TSL/Environment.js +13 -11
  38. package/src/TSL/ExtendKernel.js +75 -0
  39. package/src/TSL/FinalWriteKernel.js +121 -0
  40. package/src/TSL/GenerateKernel.js +109 -0
  41. package/src/TSL/LightsSampling.js +2 -2
  42. package/src/TSL/MaterialTransmission.js +32 -2
  43. package/src/TSL/PathTracerCore.js +43 -912
  44. package/src/TSL/ShadeKernel.js +873 -0
  45. package/src/TSL/Struct.js +5 -0
  46. package/src/TSL/Subsurface.js +232 -0
  47. package/src/TSL/patches.js +81 -4
  48. package/src/index.js +3 -0
  49. package/src/managers/CameraManager.js +1 -1
  50. package/src/managers/DenoisingManager.js +40 -75
  51. package/src/managers/EnvironmentManager.js +30 -39
  52. package/src/managers/MaterialDataManager.js +60 -1
  53. package/src/managers/OverlayManager.js +7 -22
  54. package/src/managers/UniformManager.js +1 -3
  55. package/src/managers/helpers/TileHelper.js +2 -2
  56. package/src/Stages/AdaptiveSampling.js +0 -483
  57. package/src/TSL/PathTracer.js +0 -384
  58. package/src/managers/TileManager.js +0 -298
package/src/TSL/Struct.js CHANGED
@@ -50,6 +50,11 @@ export const RayTracingMaterial = struct( {
50
50
  iridescence: 'float',
51
51
  iridescenceIOR: 'float',
52
52
  iridescenceThicknessRange: 'vec2',
53
+ subsurface: 'float', // 0 = off, blends opaque BRDF → random-walk SSS
54
+ subsurfaceColor: 'vec3', // single-scatter albedo (tint light picks up inside)
55
+ subsurfaceRadius: 'vec3', // per-channel mean free path
56
+ subsurfaceRadiusScale: 'float', // scalar multiplier on radius
57
+ subsurfaceAnisotropy: 'float', // Henyey-Greenstein g (-1..1)
53
58
  } );
54
59
 
55
60
  // Lightweight material for shadow ray evaluation — only the fields needed
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Subsurface.js - Random-walk subsurface scattering.
3
+ *
4
+ * Reuses the refraction interface + medium stack (PathTracerCore.js). The new physics
5
+ * is inside the medium: a ray collides mid-flight (sigma_s > 0) and scatters via a
6
+ * Henyey-Greenstein phase function instead of flying straight + absorbing (glass).
7
+ */
8
+
9
+ import {
10
+ Fn,
11
+ float,
12
+ vec2,
13
+ vec3,
14
+ If,
15
+ select,
16
+ abs,
17
+ dot,
18
+ clamp,
19
+ max,
20
+ exp,
21
+ log,
22
+ sqrt,
23
+ cos,
24
+ sin,
25
+ normalize,
26
+ reflect,
27
+ refract,
28
+ } from 'three/tsl';
29
+
30
+ import { struct } from './patches.js';
31
+ import { TWO_PI, EPSILON, constructTBN } from './Common.js';
32
+ import { RandomValue } from './Random.js';
33
+ import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
34
+ import { ImportanceSampleGGX } from './MaterialSampling.js';
35
+
36
+ // ================================================================================
37
+ // STRUCTS
38
+ // ================================================================================
39
+
40
+ export const CollisionSample = struct( {
41
+ didScatter: 'bool',
42
+ t: 'float', // collision distance (clamped to surfaceDist when no scatter)
43
+ weight: 'vec3', // throughput multiplier for the segment (chromatic-MIS)
44
+ } );
45
+
46
+ export const MediumCoeffs = struct( {
47
+ sigmaT: 'vec3',
48
+ sigmaS: 'vec3',
49
+ sigmaA: 'vec3',
50
+ } );
51
+
52
+ export const SubsurfaceEntryResult = struct( {
53
+ direction: 'vec3',
54
+ throughput: 'vec3',
55
+ didReflect: 'bool',
56
+ } );
57
+
58
+ // ================================================================================
59
+ // HENYEY-GREENSTEIN PHASE SAMPLING
60
+ // ================================================================================
61
+
62
+ // Returns a scattered direction (unit). cosTheta is relative to the propagation dir
63
+ // `wi`, so g > 0 is forward scattering. Inverse-CDF sampling is exact, so the brute-force
64
+ // walk needs no extra weight at the vertex (hence no pdf is returned).
65
+ export const sampleHenyeyGreenstein = Fn( ( [ wi, g, xi ] ) => {
66
+
67
+ const cosTheta = float( 0.0 ).toVar();
68
+
69
+ If( abs( g ).lessThan( 0.001 ), () => {
70
+
71
+ cosTheta.assign( float( 1.0 ).sub( xi.x.mul( 2.0 ) ) ); // isotropic; avoids 1/(2g)
72
+
73
+ } ).Else( () => {
74
+
75
+ const denom = max( float( 1.0 ).sub( g ).add( g.mul( 2.0 ).mul( xi.x ) ), 1e-4 );
76
+ const sqrTerm = float( 1.0 ).sub( g.mul( g ) ).div( denom );
77
+ cosTheta.assign( float( 1.0 ).add( g.mul( g ) ).sub( sqrTerm.mul( sqrTerm ) ).div( g.mul( 2.0 ) ) );
78
+
79
+ } );
80
+
81
+ cosTheta.assign( clamp( cosTheta, - 1.0, 1.0 ) );
82
+ const sinTheta = sqrt( max( float( 0.0 ), float( 1.0 ).sub( cosTheta.mul( cosTheta ) ) ) );
83
+ const phi = float( TWO_PI ).mul( xi.y );
84
+
85
+ // Basis with wi as 3rd column → result is already unit length.
86
+ const TBN = constructTBN( { N: wi } );
87
+ return TBN.mul( vec3( sinTheta.mul( cos( phi ) ), sinTheta.mul( sin( phi ) ), cosTheta ) );
88
+
89
+ } );
90
+
91
+ // ================================================================================
92
+ // CHROMATIC COLLISION-DISTANCE SAMPLING (hero-channel spectral MIS)
93
+ // ================================================================================
94
+
95
+ // Per-channel sigma_t can't be represented by one scalar distance. Pick a channel
96
+ // ∝ throughput, sample t against it, and weight by the balance-heuristic combined pdf
97
+ // p̄ = Σ pmf_c·p_c — the shared scalar p̄ is what suppresses color fireflies.
98
+ export const sampleChromaticCollision = Fn( ( [ sigmaT, sigmaS, beta, surfaceDist, rngState ] ) => {
99
+
100
+ const w = max( beta, vec3( 1e-4 ) ); // floor so no channel goes unsampled
101
+ const pmf = w.div( w.x.add( w.y ).add( w.z ) ).toVar();
102
+
103
+ // .toVar() pins the single RNG draw (else it re-executes per comparison → state drift).
104
+ const u = RandomValue( rngState ).toVar();
105
+ const cSigmaT = float( 0.0 ).toVar();
106
+ If( u.lessThan( pmf.x ), () => {
107
+
108
+ cSigmaT.assign( sigmaT.x );
109
+
110
+ } ).ElseIf( u.lessThan( pmf.x.add( pmf.y ) ), () => {
111
+
112
+ cSigmaT.assign( sigmaT.y );
113
+
114
+ } ).Else( () => {
115
+
116
+ cSigmaT.assign( sigmaT.z );
117
+
118
+ } );
119
+
120
+ const xi = RandomValue( rngState ).toVar();
121
+ const t = log( max( float( 1.0 ).sub( xi ), 1e-6 ) ).negate().div( max( cSigmaT, 1e-6 ) ).toVar();
122
+
123
+ const didScatter = t.lessThan( surfaceDist ).toVar();
124
+ const tOut = t.toVar();
125
+ const weight = vec3( 0.0 ).toVar();
126
+
127
+ If( didScatter, () => {
128
+
129
+ const Tr = exp( sigmaT.mul( t ).negate() ).toVar();
130
+ const pBar = dot( pmf, sigmaT.mul( Tr ) );
131
+ weight.assign( sigmaS.mul( Tr ).div( max( pBar, 1e-6 ) ) );
132
+
133
+ } ).Else( () => {
134
+
135
+ const Tr = exp( sigmaT.mul( surfaceDist ).negate() ).toVar();
136
+ const pBar = dot( pmf, Tr );
137
+ weight.assign( Tr.div( max( pBar, 1e-6 ) ) );
138
+ tOut.assign( surfaceDist );
139
+
140
+ } );
141
+
142
+ return CollisionSample( { didScatter, t: tOut, weight } );
143
+
144
+ } );
145
+
146
+ // ================================================================================
147
+ // PARAMETER → COEFFICIENT MAPPING (Cycles-style)
148
+ // ================================================================================
149
+
150
+ // sigma_t = 1/(radius·scale), sigma_s = albedo·sigma_t, sigma_a = sigma_t - sigma_s.
151
+ // subsurfaceColor is the single-scatter albedo, so the per-event weight carries the tint.
152
+ export const subsurfaceCoefficients = Fn( ( [ subsurfaceColor, subsurfaceRadius, radiusScale ] ) => {
153
+
154
+ const r = max( subsurfaceRadius.mul( radiusScale ), vec3( 1e-4 ) );
155
+ const sigmaT = vec3( 1.0 ).div( r );
156
+ const sigmaS = subsurfaceColor.mul( sigmaT );
157
+ const sigmaA = max( sigmaT.sub( sigmaS ), vec3( 0.0 ) );
158
+
159
+ return MediumCoeffs( { sigmaT, sigmaS, sigmaA } );
160
+
161
+ } );
162
+
163
+ // ================================================================================
164
+ // DIELECTRIC BOUNDARY (enter / exit the SSS medium)
165
+ // ================================================================================
166
+
167
+ // Dielectric interface driven by material.ior: reflect (Fresnel/TIR) or refract across
168
+ // the boundary. No color tint — the scattering color lives in sigma_s.
169
+ export const handleSubsurfaceEntry = Fn( ( [
170
+ rayDir, normal, material, entering, rngState, currentMediumIOR, previousMediumIOR,
171
+ ] ) => {
172
+
173
+ const result = SubsurfaceEntryResult( {
174
+ direction: vec3( 0.0 ),
175
+ throughput: vec3( 1.0 ),
176
+ didReflect: false,
177
+ } ).toVar();
178
+
179
+ const N = select( entering, normal, normal.negate() ).toVar();
180
+ const n1 = select( entering, currentMediumIOR, material.ior ).toVar();
181
+ const n2 = select( entering, material.ior, previousMediumIOR ).toVar();
182
+
183
+ const cosThetaI = abs( dot( N, rayDir ) );
184
+ const sinThetaT2 = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
185
+ const tir = sinThetaT2.greaterThan( 1.0 ).toVar();
186
+
187
+ const F0 = iorToFresnel0( n2, n1 );
188
+ const Fr = select( tir, float( 1.0 ), fresnelSchlickFloat( cosThetaI, F0 ) ).toVar();
189
+ const reflectProb = clamp( Fr, 0.02, 0.98 ).toVar();
190
+
191
+ const doReflect = tir.or( RandomValue( rngState ).lessThan( reflectProb ) ).toVar();
192
+ result.didReflect.assign( doReflect );
193
+
194
+ If( doReflect, () => {
195
+
196
+ // GGX-sampled reflection: a perfect mirror here makes SSS surfaces read as polished ceramic.
197
+ const xiR = vec2( RandomValue( rngState ), RandomValue( rngState ) );
198
+ const H = ImportanceSampleGGX( { N, roughness: material.roughness, Xi: xiR } );
199
+ const reflDir = reflect( rayDir, H ).toVar();
200
+ If( dot( reflDir, N ).lessThanEqual( 0.0 ), () => {
201
+
202
+ reflDir.assign( reflect( rayDir, N ) ); // rough sample dipped below surface
203
+
204
+ } );
205
+ result.direction.assign( reflDir );
206
+ result.throughput.assign( vec3( Fr.div( max( reflectProb, 0.02 ) ) ) );
207
+
208
+ } ).Else( () => {
209
+
210
+ const refrDir = refract( rayDir, N, n1.div( max( n2, EPSILON ) ) ).toVar();
211
+
212
+ If( dot( refrDir, refrDir ).lessThan( 0.0001 ), () => {
213
+
214
+ result.direction.assign( reflect( rayDir, N ) );
215
+ result.didReflect.assign( true );
216
+
217
+ } ).Else( () => {
218
+
219
+ result.direction.assign( normalize( refrDir ) );
220
+ // (1-Fr) transmission + (n1/n2)² radiance scale (cancels round-trip).
221
+ const radianceScale = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) );
222
+ result.throughput.assign( vec3(
223
+ float( 1.0 ).sub( Fr ).div( max( float( 1.0 ).sub( reflectProb ), 0.02 ) ).mul( radianceScale )
224
+ ) );
225
+
226
+ } );
227
+
228
+ } );
229
+
230
+ return result;
231
+
232
+ } );
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Rayzee patches for Three.js / TSL.
3
3
  *
4
- * Side-effect on import: installs `WebGPUBackend.createNodeBuilder` override
5
- * (restores r183 function-scoped `var` emission for compute shaders — prevents
6
- * a register-allocation regression in the path tracer's hot loop).
4
+ * Side-effect on import: installs two `WebGPUBackend.prototype` overrides —
5
+ * `createNodeBuilder` (restores r183 function-scoped `var` emission for compute
6
+ * shaders, preventing a register-allocation regression in the path tracer's hot
7
+ * loop) and `initTimestampQuery` (enlarges the stats-gl timestamp query pool so
8
+ * the wavefront tracer's high per-frame compute-pass count doesn't overflow it).
7
9
  *
8
10
  * Export: `struct()` — drop-in replacement for TSL's `struct()` returning
9
11
  * a proxy factory that supports GLSL-style dot-notation field access.
@@ -64,7 +66,82 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
64
66
  };
65
67
 
66
68
  // ---------------------------------------------------------------------------
67
- // 2. TSL struct proxy enables GLSL-style dot-notation field access
69
+ // 2. Larger timestamp query pool (stats-gl GPU/compute timing)
70
+ // ---------------------------------------------------------------------------
71
+ // Three.js lazily creates each timestamp query pool with a hardcoded 2048
72
+ // queries (= 1024 passes) — `WebGPUBackend.initTimestampQuery` / the upstream
73
+ // `// TODO: Variable maxQueries?`. The wavefront tracer issues hundreds of
74
+ // compute passes per frame (peak right after a maxBounces change: the survivor
75
+ // curve is invalid, so the bounce loop runs the full `loopBound` at full
76
+ // dispatch with no early-exit — ~560 passes / 1124 queries at production
77
+ // settings). stats-gl resolves once per frame, but the resolve is async and
78
+ // `mapAsync` lags several frames under that GPU load, so the counter isn't reset
79
+ // before it overflows → "Maximum number of queries exceeded" + dropped timings.
80
+ //
81
+ // Two parts:
82
+ // a) Grow the pool to 4096 queries on first use. 4096 is the WebGPU hard cap on
83
+ // a query set's count ("Query count exceeds the maximum query count (4096)")
84
+ // — 2× the upstream default, the most a single pool can hold (~2048 passes).
85
+ // That alone fully covers the interactive case (~200 passes/frame); anything
86
+ // larger fails CreateQuerySet validation.
87
+ // b) When even 4096 isn't enough (production spike), degrade gracefully: skip
88
+ // tracking the overflow passes silently instead of `warnOnce` + an invalid
89
+ // descriptor. The reported compute ms briefly undercounts during the spike;
90
+ // rendering is never affected (timestamps don't gate compute correctness).
91
+ const TIMESTAMP_POOL_MAX_QUERIES = 4096;
92
+
93
+ // Drop-in for the pool's allocateQueriesForContext minus the warnOnce on overflow
94
+ // — returns null silently when full so the pass is cleanly skipped (see below).
95
+ function _allocateQueriesSilently( uid ) {
96
+
97
+ if ( ! this.trackTimestamp || this.isDisposed ) return null;
98
+ if ( this.currentQueryIndex + 2 > this.maxQueries ) return null; // full: skip, no warn
99
+ const baseOffset = this.currentQueryIndex;
100
+ this.currentQueryIndex += 2;
101
+ this.queryOffsets.set( uid, baseOffset );
102
+ return baseOffset;
103
+
104
+ }
105
+
106
+ const _origInitTimestampQuery = WebGPUBackend.prototype.initTimestampQuery;
107
+
108
+ WebGPUBackend.prototype.initTimestampQuery = function ( type, uid, descriptor ) {
109
+
110
+ const poolWasMissing = this.trackTimestamp && ! this.timestampQueryPool[ type ];
111
+
112
+ _origInitTimestampQuery.call( this, type, uid, descriptor );
113
+
114
+ // (a) First use: replace the fresh 2048 pool with a 4096 one of the same class,
115
+ // migrating the single allocation just made (offset 0) and re-pointing this
116
+ // pass's descriptor. Safe — first pass of the first tracked frame, nothing in
117
+ // flight. Swap in the silent allocator so future overflows don't warn.
118
+ if ( poolWasMissing ) {
119
+
120
+ const pool = this.timestampQueryPool[ type ];
121
+ if ( pool && pool.maxQueries < TIMESTAMP_POOL_MAX_QUERIES && descriptor.timestampWrites ) {
122
+
123
+ const Pool = pool.constructor;
124
+ const bigPool = new Pool( this.device, type, TIMESTAMP_POOL_MAX_QUERIES );
125
+ bigPool.allocateQueriesForContext = _allocateQueriesSilently;
126
+ bigPool.allocateQueriesForContext( uid ); // re-take offset 0 for this pass
127
+ this.timestampQueryPool[ type ] = bigPool;
128
+ descriptor.timestampWrites.querySet = bigPool.querySet; // offsets 0/1 unchanged
129
+ pool.dispose(); // nothing in flight — first pass of the first tracked frame
130
+
131
+ }
132
+
133
+ }
134
+
135
+ // (b) On overflow the (silent) allocator returns null, but upstream still wrote
136
+ // a descriptor with a null begin index — which both collides on slot 1 and is
137
+ // invalid. Drop it so the pass is cleanly untimed.
138
+ const tw = descriptor.timestampWrites;
139
+ if ( tw && tw.beginningOfPassWriteIndex == null ) descriptor.timestampWrites = undefined;
140
+
141
+ };
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // 3. TSL struct proxy — enables GLSL-style dot-notation field access
68
145
  // ---------------------------------------------------------------------------
69
146
  // TSL structs require `.get('fieldName')` for member access, but GLSL-style
70
147
  // dot notation (`.fieldName`) is more natural and matches ported code.
package/src/index.js CHANGED
@@ -44,6 +44,9 @@ export { IESManager } from './managers/IESManager.js';
44
44
  export { DenoisingManager } from './managers/DenoisingManager.js';
45
45
  export { OverlayManager } from './managers/OverlayManager.js';
46
46
 
47
+ // VRAM accounting
48
+ export { VRAMTracker, bufferBytes, textureBytes } from './Processor/VRAMTracker.js';
49
+
47
50
  // Pipeline infrastructure (for advanced consumers building custom stages)
48
51
  export { RenderPipeline } from './Pipeline/RenderPipeline.js';
49
52
  export { RenderStage, StageExecutionMode } from './Pipeline/RenderStage.js';
@@ -261,7 +261,7 @@ export class CameraManager extends EventDispatcher {
261
261
  * @param {Object} params.assetLoader
262
262
  * @param {import('three').Mesh} params.floorPlane
263
263
  * @param {number} params.currentFocusDistance
264
- * @param {import('./PathTracer.js').PathTracer} params.pathTracer
264
+ * @param {import('../Stages/PathTracer.js').PathTracer} params.pathTracer
265
265
  * @param {Function} params.setFocusDistance - Callback to update uniform + settings
266
266
  * @param {Function} params.softReset - Callback for soft accumulation reset
267
267
  * @param {Function} params.hardReset - Callback for hard accumulation reset
@@ -41,7 +41,7 @@ export class DenoisingManager extends EventDispatcher {
41
41
  this.pipeline = pipeline;
42
42
 
43
43
  // Stage references — only used internally for orchestration
44
- this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, adaptiveSampling, edgeFilter, ssrc, autoExposure, compositor }
44
+ this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, edgeFilter, ssrc, autoExposure, compositor }
45
45
 
46
46
  this._getExposure = getExposure;
47
47
  this._getSaturation = getSaturation;
@@ -231,7 +231,7 @@ export class DenoisingManager extends EventDispatcher {
231
231
 
232
232
  // Disable all real-time denoisers first
233
233
  if ( s.asvgf ) s.asvgf.enabled = false;
234
- if ( s.variance && ! this._isAdaptiveSamplingActive() ) s.variance.enabled = false;
234
+ if ( s.variance ) s.variance.enabled = false;
235
235
  if ( s.bilateralFilter ) s.bilateralFilter.enabled = false;
236
236
  if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( false );
237
237
  if ( s.ssrc ) s.ssrc.enabled = false;
@@ -258,6 +258,8 @@ export class DenoisingManager extends EventDispatcher {
258
258
 
259
259
  }
260
260
 
261
+ this._syncGBufferStages();
262
+
261
263
  }
262
264
 
263
265
  /**
@@ -282,6 +284,8 @@ export class DenoisingManager extends EventDispatcher {
282
284
  // Coordinate with EdgeAware filtering
283
285
  if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( ! enabled );
284
286
 
287
+ this._syncGBufferStages();
288
+
285
289
  }
286
290
 
287
291
  /**
@@ -315,35 +319,49 @@ export class DenoisingManager extends EventDispatcher {
315
319
  }
316
320
 
317
321
  /**
318
- * Enables/disables adaptive sampling with proper stage and context cleanup.
319
- * @param {boolean} enabled
322
+ * Gate the G-buffer stages (NormalDepth, MotionVector) on demand: they only
323
+ * need to run when a real-time denoiser consumes their output. Idling them
324
+ * otherwise skips MotionVector's per-frame compute + copies during preview
325
+ * navigation and frees their textures. Call after any consumer toggle.
326
+ *
327
+ * MotionVector requires NormalDepth (reads pathtracer:normalDepth) and its
328
+ * consumers (ASVGF, SSRC) are a subset of NormalDepth's, so NormalDepth is
329
+ * always enabled whenever MotionVector is. Adaptive sampling / Variance / OIDN
330
+ * do NOT read these signals, so they don't keep the G-buffer alive.
320
331
  */
321
- setAdaptiveSamplingEnabled( enabled ) {
332
+ _syncGBufferStages() {
322
333
 
323
334
  const s = this._stages;
335
+ const nd = s.normalDepth;
336
+ const mv = s.motionVector;
324
337
 
325
- if ( s.adaptiveSampling ) {
326
-
327
- s.adaptiveSampling.enabled = enabled;
328
- s.adaptiveSampling.setHeatmapEnabled( false );
338
+ // motionVector:* consumed by ASVGF + SSRC
339
+ const motionNeeded = !! ( s.asvgf?.enabled || s.ssrc?.enabled );
340
+ // pathtracer:normalDepth consumed by ASVGF, SSRC, EdgeFilter, BilateralFilter
341
+ const normalNeeded = motionNeeded || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
329
342
 
330
- }
343
+ if ( nd ) {
331
344
 
332
- // Variance stage is shared by both ASVGF and adaptive sampling
333
- if ( enabled ) {
345
+ // On disabled→enabled, re-arm dirty/history so the first frame recomputes
346
+ // (not the stale static fast-path) and seeds prev = current.
347
+ if ( normalNeeded && ! nd.enabled ) nd.reset();
348
+ nd.enabled = normalNeeded;
334
349
 
335
- if ( s.variance ) s.variance.enabled = true;
350
+ }
336
351
 
337
- } else if ( ! s.asvgf?.enabled ) {
352
+ if ( mv ) {
338
353
 
339
- if ( s.variance ) s.variance.enabled = false;
354
+ // On re-enable, force a camera-history reseed (matricesInitialized survives
355
+ // normal resets) so the first frame reports zero motion, not a spike.
356
+ if ( motionNeeded && ! mv.enabled ) {
340
357
 
341
- }
358
+ mv.matricesInitialized = false;
359
+ mv.isFirstFrame = true;
360
+ mv.frameCount = 0;
342
361
 
343
- // Clean up stale variance context when disabling
344
- if ( ! enabled && this.pipeline?.context && ! s.asvgf?.enabled ) {
362
+ }
345
363
 
346
- this.pipeline.context.removeTexture( 'variance:output' );
364
+ mv.enabled = motionNeeded;
347
365
 
348
366
  }
349
367
 
@@ -546,6 +564,8 @@ export class DenoisingManager extends EventDispatcher {
546
564
 
547
565
  }
548
566
 
567
+ this._syncGBufferStages();
568
+
549
569
  }
550
570
 
551
571
  /** Updates SSRC stage parameters. */
@@ -569,29 +589,6 @@ export class DenoisingManager extends EventDispatcher {
569
589
 
570
590
  }
571
591
 
572
- /**
573
- * Updates adaptive sampling parameters (with settings bridge).
574
- * @param {Object} params
575
- */
576
- setAdaptiveSamplingParams( params ) {
577
-
578
- if ( params.min !== undefined ) this._stages.pathTracer?.setUniform( 'adaptiveSamplingMin', params.min );
579
- if ( params.adaptiveSamplingMax !== undefined ) this._settings?.set( 'adaptiveSamplingMax', params.adaptiveSamplingMax );
580
- this._stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
581
-
582
- }
583
-
584
- /**
585
- * Toggle the AdaptiveSampling heatmap compute pass. When enabled, the
586
- * stage writes the heatmap to its public `heatmapTarget` RenderTarget —
587
- * the host is responsible for rendering it.
588
- */
589
- toggleAdaptiveSamplingHelper( enabled ) {
590
-
591
- this._stages.adaptiveSampling?.setHeatmapEnabled( enabled );
592
-
593
- }
594
-
595
592
  // ── OIDN ─────────────────────────────────────────────────────
596
593
 
597
594
  /** Enables or disables Intel OIDN denoiser. */
@@ -608,27 +605,13 @@ export class DenoisingManager extends EventDispatcher {
608
605
 
609
606
  }
610
607
 
611
- /** Enables or disables the OIDN tile helper overlay. */
612
- setOIDNTileHelper( enabled ) {
613
-
614
- this._setTileHelper( enabled );
615
-
616
- }
617
-
618
- /** Enables or disables the tile helper overlay. */
608
+ /** Enables or disables the denoise/upscale progress overlay. */
619
609
  setTileHelperEnabled( enabled ) {
620
610
 
621
611
  this._setTileHelper( enabled );
622
612
 
623
613
  }
624
614
 
625
- /** Enables or disables tile highlight. */
626
- setTileHighlightEnabled( enabled ) {
627
-
628
- this._setTileHelper( enabled );
629
-
630
- }
631
-
632
615
  // ── AI Upscaler ──────────────────────────────────────────────
633
616
 
634
617
  /** Enables or disables the AI upscaler. */
@@ -665,17 +648,6 @@ export class DenoisingManager extends EventDispatcher {
665
648
 
666
649
  }
667
650
 
668
- /**
669
- * Enables or disables adaptive sampling (convenience wrapper with settings bridge).
670
- * @param {boolean} enabled
671
- */
672
- setAdaptiveSampling( enabled ) {
673
-
674
- this._settings?.set( 'useAdaptiveSampling', enabled );
675
- this.setAdaptiveSamplingEnabled( enabled );
676
-
677
- }
678
-
679
651
  /**
680
652
  * Switches strategy with automatic reset (convenience wrapper).
681
653
  * @param {'none'|'asvgf'|'ssrc'|'edgeaware'} strategy
@@ -702,7 +674,6 @@ export class DenoisingManager extends EventDispatcher {
702
674
 
703
675
  }
704
676
 
705
-
706
677
  _getEffectiveExposure() {
707
678
 
708
679
  return this._stages.autoExposure?.enabled
@@ -717,12 +688,6 @@ export class DenoisingManager extends EventDispatcher {
717
688
 
718
689
  }
719
690
 
720
- _isAdaptiveSamplingActive() {
721
-
722
- return this._stages.adaptiveSampling?.enabled ?? false;
723
-
724
- }
725
-
726
691
  _clearDenoiserTextures() {
727
692
 
728
693
  const ctx = this.pipeline?.context;