rayzee 7.10.0 → 7.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.10.0",
3
+ "version": "7.10.2",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -151,11 +151,6 @@ export const ENGINE_DEFAULTS = {
151
151
  asvgfQualityPreset: 'medium',
152
152
  showAsvgfHeatmap: false,
153
153
 
154
- // SSRC settings
155
- ssrcTemporalAlpha: 0.1,
156
- ssrcSpatialRadius: 4,
157
- ssrcSpatialWeight: 0.4,
158
-
159
154
  // Auto-exposure settings
160
155
  autoExposure: false,
161
156
  autoExposureKeyValue: 0.18,
@@ -13,7 +13,6 @@ import { Variance } from './Stages/Variance.js';
13
13
  import { BilateralFilter } from './Stages/BilateralFilter.js';
14
14
  import { EdgeFilter } from './Stages/EdgeFilter.js';
15
15
  import { AutoExposure } from './Stages/AutoExposure.js';
16
- import { SSRC } from './Stages/SSRC.js';
17
16
  import { Compositor } from './Stages/Compositor.js';
18
17
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
19
18
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
@@ -927,6 +926,7 @@ export class PathTracerApp extends EventDispatcher {
927
926
  async _finishRebuildNoReframe( eventPayload ) {
928
927
 
929
928
  await this.loadSceneData(); // emits 'SceneRebuild'
929
+ this._recalibrateControlLimits(); // scene bounds changed — retune zoom limits + near/far (no camera move)
930
930
  this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
931
931
  this.reset();
932
932
  if ( eventPayload ) this.dispatchEvent( eventPayload );
@@ -1676,7 +1676,6 @@ export class PathTracerApp extends EventDispatcher {
1676
1676
  this.pipeline.addStage( this.stages.pathTracer );
1677
1677
  this.pipeline.addStage( this.stages.normalDepth );
1678
1678
  this.pipeline.addStage( this.stages.motionVector );
1679
- this.pipeline.addStage( this.stages.ssrc );
1680
1679
  this.pipeline.addStage( this.stages.asvgf );
1681
1680
  this.pipeline.addStage( this.stages.variance );
1682
1681
  this.pipeline.addStage( this.stages.bilateralFilter );
@@ -1892,7 +1891,6 @@ export class PathTracerApp extends EventDispatcher {
1892
1891
  this.stages.motionVector = new MotionVector( this.renderer, this.cameraManager.camera, {
1893
1892
  pathTracer: this.stages.pathTracer
1894
1893
  } );
1895
- this.stages.ssrc = new SSRC( this.renderer, { enabled: false } );
1896
1894
  this.stages.asvgf = new ASVGF( this.renderer, { enabled: false } );
1897
1895
  this.stages.variance = new Variance( this.renderer, { enabled: false } );
1898
1896
  this.stages.bilateralFilter = new BilateralFilter( this.renderer, { enabled: false } );
@@ -1920,7 +1918,6 @@ export class PathTracerApp extends EventDispatcher {
1920
1918
  variance: this.stages.variance,
1921
1919
  bilateralFilter: this.stages.bilateralFilter,
1922
1920
  edgeFilter: this.stages.edgeFilter,
1923
- ssrc: this.stages.ssrc,
1924
1921
  autoExposure: this.stages.autoExposure,
1925
1922
  compositor: this.stages.compositor,
1926
1923
  },
@@ -2023,6 +2020,63 @@ export class PathTracerApp extends EventDispatcher {
2023
2020
 
2024
2021
  }
2025
2022
 
2023
+ /**
2024
+ * Recompute OrbitControls zoom limits (+ default-camera near/far) from the CURRENT
2025
+ * model bounds without moving the camera or its target. Called after a dynamic
2026
+ * add/remove (the reframe-free path) so an enlarged scene stays reachable and
2027
+ * unclipped, and a shrunken one re-tightens. The replace-load (reframe) path owns
2028
+ * this via onModelLoad(). Bounds cover only the loaded model roots
2029
+ * (__rayzeeSceneObject) so the oversized, usually-hidden Ground plane can't inflate them.
2030
+ */
2031
+ _recalibrateControlLimits() {
2032
+
2033
+ if ( ! this.meshScene || ! this.cameraManager ) return;
2034
+
2035
+ const bounds = new Box3();
2036
+ const tmp = new Box3();
2037
+ for ( const child of this.meshScene.children ) {
2038
+
2039
+ if ( ! child.userData?.__rayzeeSceneObject ) continue;
2040
+ tmp.setFromObject( child );
2041
+ if ( ! tmp.isEmpty() ) bounds.union( tmp );
2042
+
2043
+ }
2044
+
2045
+ if ( bounds.isEmpty() ) return;
2046
+
2047
+ const maxDim = Math.max(
2048
+ bounds.max.x - bounds.min.x,
2049
+ bounds.max.y - bounds.min.y,
2050
+ bounds.max.z - bounds.min.z,
2051
+ );
2052
+ if ( ! Number.isFinite( maxDim ) || maxDim <= 0 ) return;
2053
+
2054
+ const { camera, controls } = this.cameraManager;
2055
+
2056
+ // Same framing distance onModelLoad() uses for the initial reframe.
2057
+ const fov = camera.fov * ( Math.PI / 180 );
2058
+ const cameraDistance = Math.abs( maxDim / Math.sin( fov / 2 ) / 2 );
2059
+
2060
+ // Keep the (grown/shrunken) scene inside the frustum. Only touch near/far when the
2061
+ // default orbit camera is active — don't stomp an authored model camera's frustum.
2062
+ if ( this.cameraManager.currentCameraIndex === 0 ) {
2063
+
2064
+ camera.near = maxDim / 100;
2065
+ camera.far = maxDim * 100;
2066
+ camera.updateProjectionMatrix();
2067
+
2068
+ }
2069
+
2070
+ // Reframe-free: never clamp past where the camera currently sits, so the rebuild
2071
+ // can't yank it (e.g. when the scene shrinks after a removal).
2072
+ const currentDist = camera.position.distanceTo( controls.target );
2073
+ controls.minDistance = Math.min( maxDim / 1000, currentDist );
2074
+ controls.maxDistance = Math.max( cameraDistance * 10, currentDist * 1.1 );
2075
+
2076
+ controls.update();
2077
+
2078
+ }
2079
+
2026
2080
  /**
2027
2081
  * Forwards events from a source EventDispatcher to this app instance.
2028
2082
  */
@@ -366,6 +366,21 @@ class TextureCache {
366
366
 
367
367
  if ( this.cache.has( key ) ) {
368
368
 
369
+ const texture = this.cache.get( key );
370
+
371
+ // A pool-backed texture disposed out-of-band (e.g. SceneProcessor._disposeBucketTextures
372
+ // on a scene rebuild) has returned its backing buffer to the SmartBufferPool — a later
373
+ // build may have already reused/overwritten it, so its pixels are now garbage. Never hand
374
+ // it back: drop the stale entry and force a fresh rebuild. (dispose() nulls userData.buffer.)
375
+ if ( texture?.userData && texture.userData.buffer === null ) {
376
+
377
+ this.cache.delete( key );
378
+ const stale = this.accessOrder.indexOf( key );
379
+ if ( stale > - 1 ) this.accessOrder.splice( stale, 1 );
380
+ return null;
381
+
382
+ }
383
+
369
384
  // Move to end (most recently used)
370
385
  const index = this.accessOrder.indexOf( key );
371
386
  if ( index > - 1 ) {
@@ -379,7 +394,7 @@ class TextureCache {
379
394
  // Return cached texture directly — clone() fails on large DataArrayTextures
380
395
  // because Three.js's copy() calls JSON.stringify on the data array.
381
396
  // Each map type stores its own reference so shared instances are safe.
382
- return this.cache.get( key );
397
+ return texture;
383
398
 
384
399
  }
385
400
 
@@ -59,7 +59,6 @@ export class Compositor extends RenderStage {
59
59
  || context.getTexture( 'edgeFiltering:output' )
60
60
  || context.getTexture( 'bilateralFiltering:output' )
61
61
  || context.getTexture( 'asvgf:output' )
62
- || context.getTexture( 'ssrc:output' )
63
62
  || context.getTexture( 'pathtracer:color' );
64
63
 
65
64
  }
package/src/TSL/Common.js CHANGED
@@ -490,20 +490,3 @@ export const getShadowMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
490
490
  } );
491
491
 
492
492
  } );
493
-
494
- // ── Edge-stopping weight (normal + depth) ──────────────────────────────────
495
- // Used by ASVGF and SSRC for temporal/spatial reprojection edge-stopping.
496
-
497
- export const normalDepthWeight = /*@__PURE__*/ wgslFn( `
498
- fn normalDepthWeight(
499
- n1: vec3f, n2: vec3f,
500
- d1: f32, d2: f32,
501
- phiN: f32, phiD: f32
502
- ) -> f32 {
503
-
504
- let normalW = pow( clamp( dot( n1, n2 ), 0.0, 1.0 ), phiN );
505
- let depthW = exp( -abs( d1 - d2 ) / max( phiD, 0.001 ) );
506
- return normalW * depthW;
507
-
508
- }
509
- ` );
@@ -6,7 +6,7 @@ import { ENGINE_DEFAULTS as DEFAULT_STATE, ASVGF_QUALITY_PRESETS } from '../Engi
6
6
 
7
7
  /**
8
8
  * Orchestrates all denoising, post-processing, and AI upscaling:
9
- * - Real-time denoiser strategy switching (ASVGF / SSRC / EdgeAware / None)
9
+ * - Real-time denoiser strategy switching (ASVGF / EdgeAware / None)
10
10
  * - OIDN (offline denoise on render completion)
11
11
  * - AI Upscaler
12
12
  * - Auto-exposure coordination
@@ -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, edgeFilter, ssrc, autoExposure, compositor }
44
+ this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, edgeFilter, autoExposure, compositor }
45
45
 
46
46
  this._getExposure = getExposure;
47
47
  this._getSaturation = getSaturation;
@@ -222,7 +222,7 @@ export class DenoisingManager extends EventDispatcher {
222
222
 
223
223
  /**
224
224
  * Switches the real-time denoiser strategy.
225
- * @param {string} strategy - 'none' | 'asvgf' | 'ssrc' | 'edgeaware'
225
+ * @param {string} strategy - 'none' | 'asvgf' | 'edgeaware'
226
226
  * @param {string} [asvgfPreset] - ASVGF quality preset when strategy is 'asvgf'
227
227
  */
228
228
  setDenoiserStrategy( strategy, asvgfPreset ) {
@@ -234,7 +234,6 @@ export class DenoisingManager extends EventDispatcher {
234
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
- if ( s.ssrc ) s.ssrc.enabled = false;
238
237
 
239
238
  this._clearDenoiserTextures();
240
239
 
@@ -248,10 +247,6 @@ export class DenoisingManager extends EventDispatcher {
248
247
  this._applyASVGFPreset( asvgfPreset || 'medium' );
249
248
  break;
250
249
 
251
- case 'ssrc':
252
- if ( s.ssrc ) s.ssrc.enabled = true;
253
- break;
254
-
255
250
  case 'edgeaware':
256
251
  if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( true );
257
252
  break;
@@ -325,7 +320,7 @@ export class DenoisingManager extends EventDispatcher {
325
320
  * navigation and frees their textures. Call after any consumer toggle.
326
321
  *
327
322
  * MotionVector requires NormalDepth (reads pathtracer:normalDepth) and its
328
- * consumers (ASVGF, SSRC) are a subset of NormalDepth's, so NormalDepth is
323
+ * consumers (ASVGF) are a subset of NormalDepth's, so NormalDepth is
329
324
  * always enabled whenever MotionVector is. Adaptive sampling / Variance / OIDN
330
325
  * do NOT read these signals, so they don't keep the G-buffer alive.
331
326
  */
@@ -335,9 +330,9 @@ export class DenoisingManager extends EventDispatcher {
335
330
  const nd = s.normalDepth;
336
331
  const mv = s.motionVector;
337
332
 
338
- // motionVector:* consumed by ASVGF + SSRC
339
- const motionNeeded = !! ( s.asvgf?.enabled || s.ssrc?.enabled );
340
- // pathtracer:normalDepth consumed by ASVGF, SSRC, EdgeFilter, BilateralFilter
333
+ // motionVector:* consumed by ASVGF
334
+ const motionNeeded = !! ( s.asvgf?.enabled );
335
+ // pathtracer:normalDepth consumed by ASVGF, EdgeFilter, BilateralFilter
341
336
  const normalNeeded = motionNeeded || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
342
337
 
343
338
  if ( nd ) {
@@ -366,7 +361,7 @@ export class DenoisingManager extends EventDispatcher {
366
361
  }
367
362
 
368
363
  // PathTracer's aux MRT (normalDepth + albedo) is consumed by the real-time denoisers (ASVGF/
369
- // BilateralFilter read albedo; ASVGF/SSRC/EdgeFilter read normalDepth) and by OIDN (reads the
364
+ // BilateralFilter read albedo; ASVGF/EdgeFilter read normalDepth) and by OIDN (reads the
370
365
  // MRT read-targets directly). When none are active the wavefront skips those writes entirely.
371
366
  s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
372
367
 
@@ -374,7 +369,7 @@ export class DenoisingManager extends EventDispatcher {
374
369
  // disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
375
370
  // toggle funnels through here after the enabled flags above are settled, so this is the one
376
371
  // choke point. dispose() is idempotent, so re-running it for an already-released stage is a no-op.
377
- for ( const stage of [ s.asvgf, s.variance, s.bilateralFilter, s.ssrc, s.edgeFilter, nd, mv ] ) {
372
+ for ( const stage of [ s.asvgf, s.variance, s.bilateralFilter, s.edgeFilter, nd, mv ] ) {
378
373
 
379
374
  if ( stage && ! stage.enabled ) stage.releaseGPUMemory?.();
380
375
 
@@ -583,13 +578,6 @@ export class DenoisingManager extends EventDispatcher {
583
578
 
584
579
  }
585
580
 
586
- /** Updates SSRC stage parameters. */
587
- setSSRCParams( params ) {
588
-
589
- this._stages.ssrc?.updateParameters( params );
590
-
591
- }
592
-
593
581
  /** Updates edge-aware filtering parameters. */
594
582
  setEdgeAwareParams( params ) {
595
583
 
@@ -667,7 +655,7 @@ export class DenoisingManager extends EventDispatcher {
667
655
 
668
656
  /**
669
657
  * Switches strategy with automatic reset (convenience wrapper).
670
- * @param {'none'|'asvgf'|'ssrc'|'edgeaware'} strategy
658
+ * @param {'none'|'asvgf'|'edgeaware'} strategy
671
659
  * @param {string} [asvgfPreset]
672
660
  */
673
661
  setStrategy( strategy, asvgfPreset ) {
@@ -713,7 +701,7 @@ export class DenoisingManager extends EventDispatcher {
713
701
  const keys = [
714
702
  'asvgf:output', 'asvgf:demodulated', 'asvgf:gradient',
715
703
  'variance:output', 'bilateralFiltering:output',
716
- 'edgeFiltering:output', 'ssrc:output',
704
+ 'edgeFiltering:output',
717
705
  ];
718
706
  keys.forEach( k => ctx.removeTexture( k ) );
719
707
 
@@ -1,328 +0,0 @@
1
- // Screen-Space Radiance Cache (SSRC) Stage
2
- //
3
- // Two compute passes per frame:
4
- // Pass 1 (Temporal): Reproject previous cache via motion vectors + EMA blend
5
- // Pass 2 (Spatial): 8-tap neighbor reuse weighted by normal/depth similarity
6
- //
7
- // Execution: PER_CYCLE
8
- //
9
- // Events listened: pipeline:reset, camera:moved
10
- //
11
- // Textures published: ssrc:output
12
- // Textures read: pathtracer:color, pathtracer:normalDepth, motionVector:screenSpace
13
-
14
- import { uniform } from 'three/tsl';
15
- import { StorageTexture, TextureNode, RenderTarget } from 'three/webgpu';
16
- import { HalfFloatType, RGBAFormat, NearestFilter, LinearFilter, Box2, Vector2 } from 'three';
17
- import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
18
- import { buildTemporalPass, buildSpatialPass } from '../TSL/SSRC.js';
19
- import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
20
-
21
- export class SSRC extends RenderStage {
22
-
23
- constructor( renderer, options = {} ) {
24
-
25
- super( 'SSRC', {
26
- ...options,
27
- executionMode: StageExecutionMode.PER_CYCLE,
28
- } );
29
-
30
- this.renderer = renderer;
31
-
32
- // ─── Uniforms ───
33
- this.resW = uniform( 1 );
34
- this.resH = uniform( 1 );
35
- this.temporalAlpha = uniform( options.temporalAlpha ?? 0.1 );
36
- this.phiNormal = uniform( options.phiNormal ?? 128.0 );
37
- this.phiDepth = uniform( options.phiDepth ?? 0.5 );
38
- this.maxHistory = uniform( options.maxHistory ?? 128.0 );
39
- this.spatialRadius = uniform( options.spatialRadius ?? 4, 'int' );
40
- this.spatialWeight = uniform( options.spatialWeight ?? 0.4 );
41
- // 0 on reset → temporal pass skips cache; incremented each render frame
42
- this._framesSinceReset = uniform( 0, 'int' );
43
-
44
- // ─── Input TextureNodes (set from pipeline context each frame) ───
45
- this._colorTexNode = new TextureNode();
46
- this._ndTexNode = new TextureNode();
47
- this._motionTexNode = new TextureNode();
48
-
49
- // ─── Read-side wrappers for ping-pong StorageTextures ───
50
- this._readCacheTexNode = new TextureNode(); // prev cache (for temporal pass)
51
- this._readPrevNDTexNode = new TextureNode(); // prev normalDepth (for edge-stopping)
52
- this._readPass1CacheTexNode = new TextureNode(); // current cache (for spatial pass)
53
-
54
- // ─── StorageTextures (5 total) ───
55
- // StorageTextures stay at max alloc — see resize crash fix (three.js #33061).
56
- const w = 1, h = 1; // RTs/uniforms resized on first render
57
-
58
- // Ping-pong temporal cache: .rgb = radiance, .w = history count
59
- this._cacheTexA = this._createStorageTex( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE, NearestFilter );
60
- this._cacheTexB = this._createStorageTex( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE, NearestFilter );
61
-
62
- // Ping-pong previous-frame normalDepth (for edge-stopping in temporal pass)
63
- this._prevNDTexA = this._createStorageTex( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE, NearestFilter );
64
- this._prevNDTexB = this._createStorageTex( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE, NearestFilter );
65
-
66
- // Final output (LinearFilter for Display fragment shader sampling)
67
- this._outputTex = this._createStorageTex( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE, LinearFilter );
68
-
69
- // Active-region copy target — published downstream (storage tex is over-allocated)
70
- this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
71
- this.outputTarget = new RenderTarget( w, h, {
72
- type: HalfFloatType,
73
- format: RGBAFormat,
74
- minFilter: LinearFilter,
75
- magFilter: LinearFilter,
76
- depthBuffer: false,
77
- stencilBuffer: false
78
- } );
79
-
80
- // ─── State ───
81
- this._currentPingPong = 0; // 0: read B, write A; 1: read A, write B
82
- this._dispatchX = 1;
83
- this._dispatchY = 1;
84
-
85
- // ─── Compute nodes ───
86
- this._buildComputeNodes();
87
-
88
- }
89
-
90
- // ──────────────────────────────────────────────────
91
- // Lifecycle
92
- // ──────────────────────────────────────────────────
93
-
94
- setupEventListeners() {
95
-
96
- this.on( 'pipeline:reset', () => this._resetCache() );
97
- this.on( 'camera:moved', () => this._resetCache() );
98
-
99
- }
100
-
101
- render( context ) {
102
-
103
- if ( ! this.enabled ) {
104
-
105
- context.removeTexture( 'ssrc:output' );
106
- return;
107
-
108
- }
109
-
110
- // Auto-resize if render resolution changed
111
- const colorTex = context.getTexture( 'pathtracer:color' );
112
- if ( colorTex?.image ) {
113
-
114
- const { width, height } = colorTex.image;
115
- if ( width !== this.outputTarget.width || height !== this.outputTarget.height ) {
116
-
117
- this.setSize( width, height );
118
-
119
- }
120
-
121
- }
122
-
123
- // Bind current-frame inputs from context
124
- const normalDepthTex = context.getTexture( 'pathtracer:normalDepth' );
125
- if ( ! normalDepthTex || ! colorTex ) return;
126
-
127
- this._colorTexNode.value = colorTex;
128
- this._ndTexNode.value = normalDepthTex;
129
-
130
- const motionTex = context.getTexture( 'motionVector:screenSpace' );
131
- if ( motionTex ) this._motionTexNode.value = motionTex;
132
-
133
- // ─── Ping-pong assignment ───
134
- // _currentPingPong 0: pass1 reads B, writes A; pass2 reads A
135
- // _currentPingPong 1: pass1 reads A, writes B; pass2 reads B
136
- const [ readCache, writeCache, readPrevND ] =
137
- this._currentPingPong === 0
138
- ? [ this._cacheTexB, this._cacheTexA, this._prevNDTexB ]
139
- : [ this._cacheTexA, this._cacheTexB, this._prevNDTexA ];
140
-
141
- // ─── Pass 1: Temporal ───
142
- this._readCacheTexNode.value = readCache;
143
- this._readPrevNDTexNode.value = readPrevND;
144
-
145
- // patch the write-side storages by recreating nodes is NOT needed — the pass was built
146
- // with the actual StorageTexture references (not TextureNode wrappers), so we swap them
147
- // via the ping-pong writeCacheTex / writePrevNDTex references in the closure.
148
- // Since TSL captures StorageTexture directly for writes, we must rebuild or use a flag.
149
- // Instead we use the swappable-write pattern: build two pass1 nodes (one per write target).
150
- this.renderer.compute( this._currentPingPong === 0 ? this._pass1NodeA : this._pass1NodeB );
151
-
152
- // ─── Pass 2: Spatial ───
153
- // Spatial pass reads the just-written cache from pass 1
154
- this._readPass1CacheTexNode.value = writeCache;
155
-
156
- this.renderer.compute( this._pass2Node );
157
-
158
- // Advance frames-since-reset counter (capped to avoid overflow)
159
- this._framesSinceReset.value = Math.min( this._framesSinceReset.value + 1, 9999 );
160
-
161
- // Copy active region out of the over-allocated StorageTexture into the
162
- // right-sized RenderTarget; downstream stages UV-sample the latter.
163
- this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
164
- this.renderer.copyTextureToTexture( this._outputTex, this.outputTarget.texture, this._srcRegion );
165
-
166
- // Publish final output
167
- context.setTexture( 'ssrc:output', this.outputTarget.texture );
168
-
169
- // Advance ping-pong
170
- this._currentPingPong = 1 - this._currentPingPong;
171
-
172
- }
173
-
174
- // Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
175
- // after re-enable, and reset() clears the temporal cache. See ASVGF.releaseGPUMemory.
176
- releaseGPUMemory() {
177
-
178
- this._cacheTexA?.dispose();
179
- this._cacheTexB?.dispose();
180
- this._prevNDTexA?.dispose();
181
- this._prevNDTexB?.dispose();
182
- this._outputTex?.dispose();
183
- // Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
184
- this.context?.removeTexture( 'ssrc:output' );
185
- this.outputTarget?.texture?.dispose();
186
- this.reset();
187
-
188
- }
189
-
190
- reset() {
191
-
192
- this._resetCache();
193
-
194
- }
195
-
196
- setSize( width, height ) {
197
-
198
- if ( width < 1 || height < 1 ) return;
199
-
200
- // StorageTextures stay at their max allocation (see constructor).
201
- this.outputTarget.setSize( width, height );
202
- this.outputTarget.texture.needsUpdate = true;
203
-
204
- this.resW.value = width;
205
- this.resH.value = height;
206
-
207
- this._dispatchX = Math.ceil( width / 8 );
208
- this._dispatchY = Math.ceil( height / 8 );
209
-
210
- const dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
211
- if ( this._pass1NodeA ) this._pass1NodeA.dispatchSize = dispatchSize;
212
- if ( this._pass1NodeB ) this._pass1NodeB.dispatchSize = dispatchSize;
213
- if ( this._pass2Node ) this._pass2Node.dispatchSize = dispatchSize;
214
-
215
- this._resetCache();
216
-
217
- }
218
-
219
- dispose() {
220
-
221
- this._pass1NodeA?.dispose();
222
- this._pass1NodeB?.dispose();
223
- this._pass2Node?.dispose();
224
- this._cacheTexA.dispose();
225
- this._cacheTexB.dispose();
226
- this._prevNDTexA.dispose();
227
- this._prevNDTexB.dispose();
228
- this._outputTex.dispose();
229
- this.outputTarget?.dispose();
230
- this._colorTexNode?.dispose();
231
- this._ndTexNode?.dispose();
232
- this._motionTexNode?.dispose();
233
- this._readCacheTexNode?.dispose();
234
- this._readPrevNDTexNode?.dispose();
235
- this._readPass1CacheTexNode?.dispose();
236
-
237
- }
238
-
239
- updateParameters( params ) {
240
-
241
- if ( params.temporalAlpha !== undefined ) this.temporalAlpha.value = params.temporalAlpha;
242
- if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
243
- if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
244
- if ( params.maxHistory !== undefined ) this.maxHistory.value = params.maxHistory;
245
- if ( params.spatialRadius !== undefined ) this.spatialRadius.value = params.spatialRadius;
246
- if ( params.spatialWeight !== undefined ) this.spatialWeight.value = params.spatialWeight;
247
-
248
- }
249
-
250
- // ──────────────────────────────────────────────────
251
- // Private
252
- // ──────────────────────────────────────────────────
253
-
254
- _createStorageTex( w, h, filter ) {
255
-
256
- const tex = new StorageTexture( w, h );
257
- tex.type = HalfFloatType;
258
- tex.format = RGBAFormat;
259
- tex.minFilter = filter;
260
- tex.magFilter = filter;
261
- return tex;
262
-
263
- }
264
-
265
- _resetCache() {
266
-
267
- this._currentPingPong = 0;
268
- this._framesSinceReset.value = 0;
269
-
270
- }
271
-
272
- _buildComputeNodes() {
273
-
274
- const commonArgs = {
275
- colorTexNode: this._colorTexNode,
276
- ndTexNode: this._ndTexNode,
277
- motionTexNode: this._motionTexNode,
278
- readCacheTexNode: this._readCacheTexNode,
279
- readPrevNDTexNode: this._readPrevNDTexNode,
280
- resW: this.resW,
281
- resH: this.resH,
282
- temporalAlpha: this.temporalAlpha,
283
- phiNormal: this.phiNormal,
284
- phiDepth: this.phiDepth,
285
- maxHistory: this.maxHistory,
286
- };
287
-
288
- // Build two temporal nodes — one writing to cacheTexA, one to cacheTexB.
289
- // This is required because StorageTexture write targets are fixed at compile time.
290
- const pass1FnA = buildTemporalPass( {
291
- ...commonArgs,
292
- writeCacheTex: this._cacheTexA,
293
- writePrevNDTex: this._prevNDTexA,
294
- framesSinceReset: this._framesSinceReset,
295
- } );
296
-
297
- const pass1FnB = buildTemporalPass( {
298
- ...commonArgs,
299
- writeCacheTex: this._cacheTexB,
300
- writePrevNDTex: this._prevNDTexB,
301
- framesSinceReset: this._framesSinceReset,
302
- } );
303
-
304
- const dispatchCount = [ this._dispatchX, this._dispatchY, 1 ];
305
- const wgSize = [ 8, 8, 1 ];
306
-
307
- this._pass1NodeA = pass1FnA().compute( dispatchCount, wgSize );
308
- this._pass1NodeB = pass1FnB().compute( dispatchCount, wgSize );
309
-
310
- // Spatial pass: reads from _readPass1CacheTexNode (assigned per-frame to the just-written cache)
311
- const pass2Fn = buildSpatialPass( {
312
- colorTexNode: this._colorTexNode,
313
- ndTexNode: this._ndTexNode,
314
- readCacheTexNode: this._readPass1CacheTexNode,
315
- outputTex: this._outputTex,
316
- resW: this.resW,
317
- resH: this.resH,
318
- spatialRadius: this.spatialRadius,
319
- spatialWeight: this.spatialWeight,
320
- phiNormal: this.phiNormal,
321
- phiDepth: this.phiDepth,
322
- } );
323
-
324
- this._pass2Node = pass2Fn().compute( dispatchCount, wgSize );
325
-
326
- }
327
-
328
- }