rayzee 11.5.0 → 11.7.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 (41) hide show
  1. package/README.md +14 -14
  2. package/dist/{ArchiveImporter-29Nl1567.js → ArchiveImporter-DYcRXeKE.js} +2 -2
  3. package/dist/{BidirectionalIntegrator-CJay8tXK.js → BidirectionalIntegrator-BvjRpbTP.js} +289 -289
  4. package/dist/{EngineEvents-Ci8dVWCP.js → EngineEvents-CLckhYbL.js} +1 -0
  5. package/dist/{NeuralRetouch-KqGVPV7u.js → NeuralRetouch-ClaMiKYg.js} +1 -1
  6. package/dist/{NeuralSuperRes-BHxhYzlD.js → NeuralSuperRes-WNEXun9s.js} +2 -2
  7. package/dist/{PathTracer-Cldt0mXi.js → PathTracer-B9wZPPVL.js} +3197 -3167
  8. package/dist/{PhysicalSky-CUWrTcmT.js → PhysicalSky-DcjItqKi.js} +1 -1
  9. package/dist/{RayzeeRenderer-Dd-Dit0v.js → RayzeeRenderer-p-NeE6E2.js} +405 -437
  10. package/dist/addons/archives.es.js +1 -1
  11. package/dist/addons/bidirectional.es.js +1 -1
  12. package/dist/addons/physical-sky.es.js +1 -1
  13. package/dist/rayzee-core.es.js +5 -5
  14. package/dist/rayzee.es.js +1003 -1686
  15. package/dist/rayzee.umd.js +104 -143
  16. package/dist/{sceneCachePolicy--ta6moJu.js → sceneCachePolicy-B9w3BnaP.js} +2 -2
  17. package/package.json +2 -2
  18. package/src/EngineEvents.js +2 -0
  19. package/src/Passes/OIDNDenoiser.js +106 -100
  20. package/src/PathTracerApp.js +8 -19
  21. package/src/Pipeline/BindingAudit.js +2 -3
  22. package/src/Pipeline/PipelineContext.js +0 -1
  23. package/src/Pipeline/RenderPipeline.js +1 -2
  24. package/src/Pipeline/RenderStage.js +1 -1
  25. package/src/Processor/PackedRayBuffer.js +2 -2
  26. package/src/Processor/StorageTexturePool.js +1 -1
  27. package/src/Processor/TextureReadback.js +18 -1
  28. package/src/RayzeeRenderer.js +5 -5
  29. package/src/Stages/DenoiserSettings.js +5 -81
  30. package/src/Stages/MotionVector.js +21 -215
  31. package/src/Stages/NormalDepth.js +5 -5
  32. package/src/Stages/PathTracer.js +3 -11
  33. package/src/Stages/PathTracerStage.js +4 -17
  34. package/src/TSL/FinalWriteKernel.js +2 -2
  35. package/src/TSL/ShadeKernel.js +3 -3
  36. package/src/index.js +1 -1
  37. package/src/managers/DenoisingManager.js +93 -187
  38. package/src/Stages/ASVGF.js +0 -932
  39. package/src/Stages/BilateralFilter.js +0 -486
  40. package/src/Stages/EdgeFilter.js +0 -545
  41. package/src/Stages/Variance.js +0 -443
@@ -48,7 +48,7 @@ export function buildFinalWriteKernel( params ) {
48
48
  // on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
49
49
  auxGBufferEnabled, gBuffer = true, // compiled in only when requested (PathTracer.requestOutput( 'gBuffer' ))
50
50
  // Clean-aux normal (1 = temporally accumulate + renormalize the aux normal). On only for clean-aux
51
- // OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
51
+ // OIDN models (calb_cnrm/high, alb_nrm/balanced); off otherwise, as the fast model wants the bump normal.
52
52
  cleanAuxNormalEnabled,
53
53
  // Tier-1 convergence early-stop: per-pixel Welford luminance second moment + converged bit (stamped
54
54
  // into streak bit 31; counted with 3×3 erosion by countConvergedDilated at the next frame start).
@@ -126,7 +126,7 @@ export function buildFinalWriteKernel( params ) {
126
126
  finalHitDist.assign( mix( prevAlbedoSample.w, finalHitDist, auxAccumulationAlpha ) );
127
127
 
128
128
  // NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
129
- // which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
129
+ // which the fast model wants to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
130
130
  // alb_nrm/balanced) trusts the aux and is fed per-frame point-sampled NOISE → leaked noise
131
131
  // (high) / over-smoothing (balanced). When cleanAuxNormalOn, temporally accumulate it like
132
132
  // the colour. Average the RAW unit normals (decode 0.5+0.5 → [-1,1]) and RENORMALIZE — a
@@ -125,7 +125,7 @@ export function buildShadeKernel( params ) {
125
125
  emissiveBoost, enableEmissiveTriangleSampling,
126
126
  lightBVHNodeCount, reverseMapVec4Offset,
127
127
  maxRayCount,
128
- // Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Compiled in only when
128
+ // Aux G-buffer (normal/depth/albedo) feeds only the denoiser/OIDN MRT. Compiled in only when
129
129
  // requested (PathTracer.requestOutput( 'gBuffer' )); then a live uniform (1 = denoiser on) skips the writes
130
130
  // while nothing consumes them.
131
131
  auxGBufferEnabled, gBuffer = true,
@@ -289,7 +289,7 @@ export function buildShadeKernel( params ) {
289
289
  // This bounce's block of sampler dimensions — see the budget table in Random.js.
290
290
  const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
291
291
 
292
- // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
292
+ // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/NRD
293
293
  // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
294
294
  // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
295
295
  const featCarry = aux ? readFeatureThroughput( rayBufferRW, rayID ).toVar() : null;
@@ -424,7 +424,7 @@ export function buildShadeKernel( params ) {
424
424
 
425
425
  // The catcher is a real ground surface for the denoiser — write the plane's normal/depth
426
426
  // + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
427
- // valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
427
+ // valid surface, so OIDN/NRD don't smear the caught shadow as a background miss.
428
428
  if ( aux ) If( aux, () => {
429
429
 
430
430
  const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
package/src/index.js CHANGED
@@ -63,7 +63,7 @@ export {
63
63
  } from './EngineDefaults.js';
64
64
  export { MAX_RESERVABLE_RENDER_SIZE } from './Processor/StorageTexturePool.js';
65
65
  export {
66
- DENOISER_DEFAULTS, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS, NRD_HIT_DIST_A,
66
+ DENOISER_DEFAULTS, DENOISER_STRATEGIES, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS, NRD_HIT_DIST_A,
67
67
  NRD_HIT_DIST_B,
68
68
  } from './Stages/DenoiserSettings.js';
69
69
  export { SKY_DEFAULTS, DEFAULT_SUN_PATH } from './managers/EnvironmentManager.js';
@@ -11,7 +11,7 @@ import { createLogger } from '../utils/Logger.js';
11
11
  const neuralLog = createLogger( 'neural' );
12
12
  import { CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
13
13
  import {
14
- DENOISER_DEFAULTS, ASVGF_QUALITY_PRESETS, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS,
14
+ DENOISER_DEFAULTS, DENOISER_STRATEGIES, NRD_DEFAULTS, NRD_QUALITY_PRESETS, NRD_PRESET_KEYS,
15
15
  } from '../Stages/DenoiserSettings.js';
16
16
 
17
17
  // A refresh slower than this is a slideshow, not a live view, so the cadence swaps to a cheaper
@@ -85,7 +85,7 @@ const historyFade = ( samples ) => Math.max( 0, 1 - samples / HISTORY_HANDOFF_SA
85
85
 
86
86
  /**
87
87
  * Orchestrates all denoising, post-processing, and AI upscaling:
88
- * - Real-time denoiser strategy switching (ASVGF / NRD / EdgeAware / None)
88
+ * - Real-time denoiser strategy switching (OIDN / NRD / None)
89
89
  * - OIDN (offline denoise on render completion)
90
90
  * - AI Upscaler
91
91
  * - Auto-exposure coordination
@@ -118,7 +118,7 @@ export class DenoisingManager extends EventDispatcher {
118
118
  this.pipeline = pipeline;
119
119
 
120
120
  // Stage references — only used internally for orchestration
121
- this._stages = stages; // { pathTracer, asvgf, nrd, variance, bilateralFilter, edgeFilter, autoExposure, compositor }
121
+ this._stages = stages; // { pathTracer, normalDepth, motionVector, nrd, autoExposure, compositor }
122
122
 
123
123
  this._getExposure = getExposure;
124
124
  this._getSaturation = getSaturation;
@@ -197,6 +197,7 @@ export class DenoisingManager extends EventDispatcher {
197
197
  this._denoiserEndHandler = null;
198
198
  this._denoiserTileHandler = null;
199
199
  this._denoiserErrorHandler = null;
200
+ this._denoiserLoadHandler = null;
200
201
  this._upscalerResChangedHandler = null;
201
202
  this._upscalerStartHandler = null;
202
203
  this._upscalerProgressHandler = null;
@@ -348,7 +349,9 @@ export class DenoisingManager extends EventDispatcher {
348
349
  this.dispatchEvent( { type: EngineEvents.DENOISING_START, continuous: !! e.continuous } );
349
350
  this._denoiserEndHandler = e => {
350
351
 
351
- if ( this.denoiser?.hasOutput ) this._publishOutput();
352
+ // A refresh that lands after the live view stopped wanting one would cover what replaced it.
353
+ const current = ! e.continuous || ( this.continuousDenoise && ! this._cadenceSuspended );
354
+ if ( this.denoiser?.hasOutput && current ) this._publishOutput();
352
355
  this.dispatchEvent( { type: EngineEvents.DENOISING_END, continuous: !! e.continuous } );
353
356
 
354
357
  };
@@ -377,6 +380,12 @@ export class DenoisingManager extends EventDispatcher {
377
380
  this.denoiser.addEventListener( 'tileProgress', this._denoiserTileHandler );
378
381
  this.denoiser.addEventListener( 'error', this._denoiserErrorHandler );
379
382
 
383
+ // 'error' too: a failed load never sends 'loaded'. The first load starts inside the constructor, before these listen.
384
+ this._denoiserLoadHandler = e =>
385
+ this.dispatchEvent( { type: EngineEvents.DENOISER_LOADING, loading: e.type === 'loading' } );
386
+ for ( const type of [ 'loading', 'loaded', 'error' ] ) this.denoiser.addEventListener( type, this._denoiserLoadHandler );
387
+ if ( this.denoiser.state?.isLoading ) this.dispatchEvent( { type: EngineEvents.DENOISER_LOADING, loading: true } );
388
+
380
389
  }
381
390
 
382
391
  /**
@@ -440,9 +449,8 @@ export class DenoisingManager extends EventDispatcher {
440
449
  // ── Denoiser Strategy ─────────────────────────────────────────
441
450
 
442
451
  /**
443
- * Active real-time denoiser, derived from stage state so it can't drift from
444
- * setDenoiserStrategy / setASVGFEnabled.
445
- * @returns {'asvgf'|'nrd'|'edgeaware'|'none'}
452
+ * Active real-time denoiser, derived from stage state so it can't drift from setDenoiserStrategy.
453
+ * @returns {'none'|'nrd'|'oidn'}
446
454
  */
447
455
  get denoiserStrategy() {
448
456
 
@@ -450,105 +458,55 @@ export class DenoisingManager extends EventDispatcher {
450
458
  // belongs in the same one-of-N choice. Two of these running at once would mean paying for
451
459
  // a per-frame denoise whose result the OIDN overlay then covers.
452
460
  if ( this.continuousDenoise ) return 'oidn';
453
- if ( this._stages.asvgf?.enabled ) return 'asvgf';
454
461
  if ( this._stages.nrd?.enabled ) return 'nrd';
455
- if ( this._stages.edgeFilter?.enabled ) return 'edgeaware';
456
462
  return 'none';
457
463
 
458
464
  }
459
465
 
460
466
  /**
461
467
  * Switches the real-time denoiser strategy.
462
- * @param {string} strategy - 'none' | 'asvgf' | 'nrd' | 'edgeaware'
463
- * @param {string} [preset] - quality preset for 'asvgf' (ASVGF_QUALITY_PRESETS) or 'nrd' (NRD_QUALITY_PRESETS)
468
+ * @param {string} strategy - one of DENOISER_STRATEGIES; any other name is recorded and runs 'none'
469
+ * @param {string} [preset] - NRD quality preset (NRD_QUALITY_PRESETS)
464
470
  */
465
471
  setDenoiserStrategy( strategy, preset ) {
466
472
 
467
473
  const s = this._stages;
468
474
 
469
- // Disable all real-time denoisers first
470
- if ( s.asvgf ) s.asvgf.enabled = false;
471
- if ( s.nrd ) s.nrd.enabled = false;
472
- if ( s.variance ) s.variance.enabled = false;
473
- if ( s.bilateralFilter ) s.bilateralFilter.enabled = false;
474
- if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( false );
475
-
476
- this._clearDenoiserTextures();
477
-
478
- // Which denoiser owns the live view. Deliberately says nothing about the finished image:
479
- // wanting OIDN on the viewport is not the same as opting into a final pass.
480
- this.setContinuousDenoise( strategy === 'oidn' );
481
-
482
- switch ( strategy ) {
475
+ if ( ! DENOISER_STRATEGIES.includes( strategy ) ) {
483
476
 
484
- case 'asvgf':
485
- s.asvgf.enabled = true;
486
- if ( s.variance ) s.variance.enabled = true;
487
- if ( s.bilateralFilter ) s.bilateralFilter.enabled = true;
488
- s.asvgf.setTemporalEnabled?.( true );
489
- this._applyASVGFPreset( preset || 'medium' );
490
- break;
477
+ this._issues?.record(
478
+ ISSUE_CODES.DENOISER_UNAVAILABLE,
479
+ `'${strategy}' is not a live denoiser (${DENOISER_STRATEGIES.join( ', ' )}) — none is used`,
480
+ { strategy }
481
+ );
482
+ strategy = 'none';
491
483
 
492
- case 'nrd':
493
- if ( s.nrd ) {
494
-
495
- s.nrd.enabled = true;
496
- // Stale history from the last time it ran describes another view.
497
- s.nrd.resetHistory?.();
498
- this._applyNRDPreset( preset || 'medium' );
499
-
500
- }
484
+ }
501
485
 
502
- break;
486
+ // NRD reprojects through motion vectors, which mean nothing once every pixel is its own direction.
487
+ if ( strategy === 'nrd' && this._isPanorama() ) {
503
488
 
504
- case 'edgeaware':
505
- // EdgeAware is a spatial-only SVGF à-trous — it consumes the Variance
506
- // stage's per-pixel variance to drive its luminance edge-stop.
507
- if ( s.variance ) s.variance.enabled = true;
508
- if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( true );
509
- break;
489
+ this._issues?.record( ISSUE_CODES.DENOISER_UNAVAILABLE, 'NRD cannot run in a panorama — oidn is used', { strategy } );
490
+ strategy = 'oidn';
510
491
 
511
492
  }
512
493
 
513
- this._syncGBufferStages();
514
-
515
- }
516
-
517
- /**
518
- * Enables/disables ASVGF denoising with coordination of related stages.
519
- * @param {boolean} enabled
520
- * @param {string} [qualityPreset]
521
- */
522
- setASVGFEnabled( enabled, qualityPreset ) {
494
+ this._clearDenoiserTextures();
523
495
 
524
- const s = this._stages;
525
- if ( s.asvgf ) s.asvgf.enabled = enabled;
526
- if ( s.variance ) s.variance.enabled = enabled;
527
- if ( s.bilateralFilter ) s.bilateralFilter.enabled = enabled;
496
+ if ( s.nrd ) s.nrd.enabled = strategy === 'nrd';
528
497
 
529
- if ( enabled ) {
498
+ if ( s.nrd?.enabled ) {
530
499
 
531
- // One real-time denoiser at a time.
532
- if ( s.nrd ) s.nrd.enabled = false;
533
- s.asvgf?.setTemporalEnabled?.( true );
534
- this._applyASVGFPreset( qualityPreset || 'medium' );
500
+ // Stale history from the last time it ran describes another view.
501
+ s.nrd.resetHistory?.();
502
+ this._applyNRDPreset( preset || 'medium' );
535
503
 
536
504
  }
537
505
 
538
- // Coordinate with EdgeAware filtering
539
- if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( ! enabled );
540
-
541
- this._syncGBufferStages();
542
-
543
- }
544
-
545
- /**
546
- * Applies an ASVGF quality preset.
547
- * @param {string} presetName - 'low' | 'medium' | 'high'
548
- */
549
- applyASVGFPreset( presetName ) {
550
-
551
- this._applyASVGFPreset( presetName );
506
+ // Which denoiser owns the live view. Deliberately says nothing about the finished image:
507
+ // wanting OIDN on the viewport is not the same as opting into a final pass. Also re-syncs
508
+ // the G-buffer stages, so NRD must already be set.
509
+ this.setContinuousDenoise( strategy === 'oidn' );
552
510
 
553
511
  }
554
512
 
@@ -562,6 +520,13 @@ export class DenoisingManager extends EventDispatcher {
562
520
 
563
521
  }
564
522
 
523
+ /** True while a strategy that reprojects through motion vectors is active. */
524
+ get requiresMotionVectors() {
525
+
526
+ return !! this._stages.nrd?.enabled;
527
+
528
+ }
529
+
565
530
  /**
566
531
  * Gate the G-buffer stages (NormalDepth, MotionVector) on demand: they only
567
532
  * need to run when a real-time denoiser consumes their output. Idling them
@@ -569,17 +534,10 @@ export class DenoisingManager extends EventDispatcher {
569
534
  * navigation and frees their textures. Call after any consumer toggle.
570
535
  *
571
536
  * MotionVector requires NormalDepth (reads pathtracer:normalDepth) and its
572
- * consumers (ASVGF, NRD) are a subset of NormalDepth's, so NormalDepth is
573
- * always enabled whenever MotionVector is. Adaptive sampling / Variance / OIDN
574
- * do NOT read these signals, so they don't keep the G-buffer alive.
537
+ * consumer (NRD) also reads NormalDepth, so NormalDepth is always enabled
538
+ * whenever MotionVector is. Adaptive sampling and OIDN's own pass do NOT read
539
+ * these signals; only OIDN's motion history keeps NormalDepth alive.
575
540
  */
576
- /** True while a strategy that reprojects through motion vectors is active. */
577
- get requiresMotionVectors() {
578
-
579
- return !! ( this._stages.asvgf?.enabled || this._stages.nrd?.enabled );
580
-
581
- }
582
-
583
541
  _syncGBufferStages() {
584
542
 
585
543
  const s = this._stages;
@@ -587,8 +545,8 @@ export class DenoisingManager extends EventDispatcher {
587
545
  const mv = s.motionVector;
588
546
 
589
547
  const motionNeeded = this.requiresMotionVectors;
590
- // pathtracer:normalDepth consumed by ASVGF, NRD, EdgeFilter, BilateralFilter, the OIDN history
591
- const normalNeeded = motionNeeded || this.historyWanted || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
548
+ // pathtracer:normalDepth consumed by NRD and the OIDN history
549
+ const normalNeeded = motionNeeded || this.historyWanted;
592
550
  if ( ! this.historyWanted ) this._releaseHistory();
593
551
 
594
552
  if ( nd ) {
@@ -605,21 +563,14 @@ export class DenoisingManager extends EventDispatcher {
605
563
 
606
564
  // On re-enable, force a camera-history reseed (matricesInitialized survives
607
565
  // normal resets) so the first frame reports zero motion, not a spike.
608
- if ( motionNeeded && ! mv.enabled ) {
609
-
610
- mv.matricesInitialized = false;
611
- mv.isFirstFrame = true;
612
- mv.frameCount = 0;
613
-
614
- }
566
+ if ( motionNeeded && ! mv.enabled ) mv.matricesInitialized = false;
615
567
 
616
568
  mv.enabled = motionNeeded;
617
569
 
618
570
  }
619
571
 
620
- // PathTracer's aux MRT (normalDepth + albedo) is consumed by the real-time denoisers (ASVGF/
621
- // BilateralFilter read albedo; ASVGF/EdgeFilter read normalDepth) and by OIDN (reads the
622
- // MRT read-targets directly). When none are active the wavefront skips those writes entirely.
572
+ // PathTracer's aux MRT (normalDepth + albedo) is consumed by NRD (albedo, hit distance) and by
573
+ // OIDN (reads the MRT read-targets directly). When neither is active the wavefront skips those writes.
623
574
  s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
624
575
 
625
576
  // Clean-aux normal: temporally accumulate the aux normal only for OIDN clean-aux models
@@ -634,7 +585,7 @@ export class DenoisingManager extends EventDispatcher {
634
585
  // disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
635
586
  // toggle funnels through here after the enabled flags above are settled, so this is the one
636
587
  // choke point. dispose() is idempotent, so re-running it for an already-released stage is a no-op.
637
- for ( const stage of [ s.asvgf, s.nrd, s.variance, s.bilateralFilter, s.edgeFilter, nd, mv ] ) {
588
+ for ( const stage of [ s.nrd, nd, mv ] ) {
638
589
 
639
590
  if ( stage && ! stage.enabled ) stage.releaseGPUMemory?.();
640
591
 
@@ -842,21 +793,29 @@ export class DenoisingManager extends EventDispatcher {
842
793
 
843
794
  this._cadenceSuspended = !! suspended;
844
795
  this._historyDirty = true;
845
- if ( ! this._cadenceSuspended ) return;
846
796
 
847
- // The accumulation is what a final render shows; a held preview frame would otherwise sit
848
- // over it until the render finished.
849
- this.denoiser?.abort();
850
- this._unpublishOutput();
797
+ if ( this._cadenceSuspended ) {
798
+
799
+ // The accumulation is what a final render shows; a held preview frame would otherwise sit
800
+ // over it until the render finished.
801
+ this.denoiser?.abort();
802
+ this._unpublishOutput();
803
+
804
+ }
805
+
806
+ this._syncGBufferStages();
851
807
 
852
808
  }
853
809
 
854
810
  setContinuousDenoise( enabled ) {
855
811
 
812
+ // Once nothing else uses OIDN it turns off, and a disabled denoiser can no longer be aborted.
813
+ if ( ! enabled && ! this.finalDenoise ) this.denoiser?.abort();
856
814
  this.continuousDenoise = !! enabled;
857
815
  this._syncOIDNInUse();
858
816
  this._lastCadenceAt = - Infinity;
859
817
  this._resetCadence();
818
+ this._syncGBufferStages();
860
819
 
861
820
  }
862
821
 
@@ -888,19 +847,24 @@ export class DenoisingManager extends EventDispatcher {
888
847
 
889
848
  }
890
849
 
891
- // Not at a size proven too slow to denoise while moving: the raw render owns the view there.
850
+ // Not at a size proven too slow to denoise while moving (the raw render owns the view there), nor
851
+ // through a final render or a panorama, which never reproject it.
892
852
  get historyWanted() {
893
853
 
894
- return this.temporalHistory && this.continuousDenoise && !! this.denoiser?.enabled && ! this._movingHopeless;
854
+ return this.temporalHistory && this.continuousDenoise && !! this.denoiser?.enabled && ! this._movingHopeless
855
+ && ! this._cadenceSuspended && ! this._isPanorama();
895
856
 
896
857
  }
897
858
 
898
859
  get historyActive() {
899
860
 
900
- const pt = this._stages.pathTracer;
901
- return this.historyWanted && ! this._cadenceSuspended && !! this._stages.normalDepth?.enabled
902
- && pt?.uniforms?.get( 'cameraProjection' )?.value !== CAMERA_PROJECTION_IDS.equirectangular
903
- && ! ( pt?.visMode?.value > 0 );
861
+ return this.historyWanted && !! this._stages.normalDepth?.enabled && ! ( this._stages.pathTracer?.visMode?.value > 0 );
862
+
863
+ }
864
+
865
+ _isPanorama() {
866
+
867
+ return this._stages.pathTracer?.uniforms?.get( 'cameraProjection' )?.value === CAMERA_PROJECTION_IDS.equirectangular;
904
868
 
905
869
  }
906
870
 
@@ -1436,7 +1400,9 @@ export class DenoisingManager extends EventDispatcher {
1436
1400
  // What closes the render, if anything. A full pass at the chosen tier when that switch is
1437
1401
  // on; otherwise, if OIDN owns the live view, one last cheap refresh — the cadence's last
1438
1402
  // tick lands a few samples short, and the picture should match the render that finished.
1439
- const closing = this.finalDenoise ? 'final' : this.continuousDenoise ? 'refresh' : null;
1403
+ // A final render has no live view to match, so with the switch off it stays undenoised.
1404
+ const live = this.continuousDenoise && ! this._cadenceSuspended;
1405
+ const closing = this.finalDenoise ? 'final' : live ? 'refresh' : null;
1440
1406
 
1441
1407
  // Registered only once the closing denoise has been launched, so the next 'end' is
1442
1408
  // unambiguously that one — a cadence run's end can look identical otherwise.
@@ -1584,6 +1550,13 @@ export class DenoisingManager extends EventDispatcher {
1584
1550
  if ( this._denoiserEndHandler ) this.denoiser.removeEventListener( 'end', this._denoiserEndHandler );
1585
1551
  if ( this._denoiserTileHandler ) this.denoiser.removeEventListener( 'tileProgress', this._denoiserTileHandler );
1586
1552
  if ( this._denoiserErrorHandler ) this.denoiser.removeEventListener( 'error', this._denoiserErrorHandler );
1553
+ if ( this._denoiserLoadHandler ) {
1554
+
1555
+ for ( const type of [ 'loading', 'loaded', 'error' ] ) this.denoiser.removeEventListener( type, this._denoiserLoadHandler );
1556
+ if ( this.denoiser.state?.isLoading ) this.dispatchEvent( { type: EngineEvents.DENOISER_LOADING, loading: false } );
1557
+
1558
+ }
1559
+
1587
1560
  this.denoiser.dispose();
1588
1561
  this.denoiser = null;
1589
1562
 
@@ -1608,6 +1581,7 @@ export class DenoisingManager extends EventDispatcher {
1608
1581
  this._denoiserEndHandler = null;
1609
1582
  this._denoiserTileHandler = null;
1610
1583
  this._denoiserErrorHandler = null;
1584
+ this._denoiserLoadHandler = null;
1611
1585
  this._upscalerResChangedHandler = null;
1612
1586
  this._upscalerStartHandler = null;
1613
1587
  this._upscalerProgressHandler = null;
@@ -1671,53 +1645,6 @@ export class DenoisingManager extends EventDispatcher {
1671
1645
  // These methods match the DenoisingAPI surface so call sites need
1672
1646
  // zero or minimal changes after facade removal.
1673
1647
 
1674
- /** Updates ASVGF stage parameters. */
1675
- setASVGFParams( params ) {
1676
-
1677
- this._stages.asvgf?.updateParameters( params );
1678
-
1679
- }
1680
-
1681
- /**
1682
- * Toggle the ASVGF heatmap compute pass. When enabled, the stage writes
1683
- * the heatmap to its public `heatmapTarget` RenderTarget — the host is
1684
- * responsible for rendering it.
1685
- */
1686
- toggleASVGFHeatmap( enabled ) {
1687
-
1688
- this._stages.asvgf?.setHeatmapEnabled?.( enabled );
1689
-
1690
- }
1691
-
1692
- /**
1693
- * Configures ASVGF for a specific render mode (multi-stage coordination).
1694
- * @param {Object} config - { enabled, temporalAlpha, atrousIterations, ... }
1695
- */
1696
- configureASVGFForMode( config ) {
1697
-
1698
- if ( ! this._stages.asvgf ) return;
1699
-
1700
- this._stages.asvgf.enabled = config.enabled;
1701
- if ( this._stages.variance ) this._stages.variance.enabled = config.enabled;
1702
- if ( this._stages.bilateralFilter ) this._stages.bilateralFilter.enabled = config.enabled;
1703
-
1704
- if ( config.enabled ) {
1705
-
1706
- this._stages.asvgf.updateParameters( config );
1707
-
1708
- }
1709
-
1710
- this._syncGBufferStages();
1711
-
1712
- }
1713
-
1714
- /** Updates edge-aware filtering parameters. */
1715
- setEdgeAwareParams( params ) {
1716
-
1717
- this._stages.edgeFilter?.updateUniforms( params );
1718
-
1719
- }
1720
-
1721
1648
  /** Updates auto-exposure stage parameters. */
1722
1649
  setAutoExposureParams( params ) {
1723
1650
 
@@ -1835,7 +1762,7 @@ export class DenoisingManager extends EventDispatcher {
1835
1762
 
1836
1763
  /**
1837
1764
  * Switches strategy with automatic reset (convenience wrapper).
1838
- * @param {'none'|'asvgf'|'nrd'|'edgeaware'} strategy
1765
+ * @param {'none'|'nrd'|'oidn'} strategy
1839
1766
  * @param {string} [preset]
1840
1767
  */
1841
1768
  setStrategy( strategy, preset ) {
@@ -1904,12 +1831,8 @@ export class DenoisingManager extends EventDispatcher {
1904
1831
  const ctx = this.pipeline?.context;
1905
1832
  if ( ! ctx ) return;
1906
1833
 
1907
- const keys = [
1908
- 'asvgf:output', 'asvgf:demodulated', 'asvgf:gradient',
1909
- 'variance:output', 'bilateralFiltering:output',
1910
- 'edgeFiltering:output', 'nrd:output', 'oidn:output',
1911
- ];
1912
- keys.forEach( k => ctx.removeTexture( k ) );
1834
+ ctx.removeTexture( 'nrd:output' );
1835
+ ctx.removeTexture( 'oidn:output' );
1913
1836
 
1914
1837
  }
1915
1838
 
@@ -1924,21 +1847,4 @@ export class DenoisingManager extends EventDispatcher {
1924
1847
 
1925
1848
  }
1926
1849
 
1927
- _applyASVGFPreset( presetName ) {
1928
-
1929
- const preset = ASVGF_QUALITY_PRESETS[ presetName ];
1930
- if ( ! preset ) return;
1931
- // ASVGF consumes temporalAlpha / gradientStrength / maxAccumFrames.
1932
- // BilateralFilter consumes phi* edge-stopping params and atrousIterations.
1933
- // Variance consumes varianceBoost. Each stage cherry-picks what it needs.
1934
- this._stages.asvgf?.updateParameters( preset );
1935
- this._stages.bilateralFilter?.updateParameters( preset );
1936
- if ( this._stages.variance && preset.varianceBoost !== undefined ) {
1937
-
1938
- this._stages.variance.varianceBoost.value = preset.varianceBoost;
1939
-
1940
- }
1941
-
1942
- }
1943
-
1944
1850
  }