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/README.md +2 -3
- package/dist/rayzee.es.js +1188 -1350
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +34 -46
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +0 -5
- package/src/PathTracerApp.js +58 -4
- package/src/Processor/TextureCreator.js +16 -1
- package/src/Stages/Compositor.js +0 -1
- package/src/TSL/Common.js +0 -17
- package/src/managers/DenoisingManager.js +11 -23
- package/src/Stages/SSRC.js +0 -328
- package/src/TSL/SSRC.js +0 -266
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -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,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -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
|
|
397
|
+
return texture;
|
|
383
398
|
|
|
384
399
|
}
|
|
385
400
|
|
package/src/Stages/Compositor.js
CHANGED
|
@@ -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 /
|
|
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,
|
|
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' | '
|
|
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
|
|
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
|
|
339
|
-
const motionNeeded = !! ( s.asvgf?.enabled
|
|
340
|
-
// pathtracer:normalDepth consumed by ASVGF,
|
|
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/
|
|
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.
|
|
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'|'
|
|
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',
|
|
704
|
+
'edgeFiltering:output',
|
|
717
705
|
];
|
|
718
706
|
keys.forEach( k => ctx.removeTexture( k ) );
|
|
719
707
|
|
package/src/Stages/SSRC.js
DELETED
|
@@ -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
|
-
}
|