rayzee 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rayzee.es.js +2296 -2299
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +4 -4
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +3 -3
- package/src/PathTracerApp.js +15 -11
- package/src/RenderSettings.js +10 -8
- package/src/Stages/Compositor.js +101 -0
- package/src/Stages/EdgeFilter.js +89 -69
- package/src/Stages/NormalDepth.js +37 -20
- package/src/managers/DenoisingManager.js +8 -18
- package/src/Stages/Display.js +0 -120
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -90,8 +90,8 @@ export const ENGINE_DEFAULTS = {
|
|
|
90
90
|
directionalLightPosition: [ 1, 1, 1 ],
|
|
91
91
|
directionalLightAngle: 0.0,
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
filterStrength: 0.75,
|
|
94
|
+
strengthDecaySpeed: 0.05,
|
|
95
95
|
edgeThreshold: 1.0,
|
|
96
96
|
|
|
97
97
|
enableOIDN: false,
|
|
@@ -115,7 +115,7 @@ export const ENGINE_DEFAULTS = {
|
|
|
115
115
|
debugVisScale: 100,
|
|
116
116
|
|
|
117
117
|
// Denoising strategy
|
|
118
|
-
denoiserStrategy: '
|
|
118
|
+
denoiserStrategy: 'none',
|
|
119
119
|
|
|
120
120
|
enableASVGF: false,
|
|
121
121
|
asvgfTemporalAlpha: 0.1,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebGPURenderer, RectAreaLightNode } from 'three/webgpu';
|
|
1
|
+
import { WebGPURenderer, RectAreaLightNode, LinearSRGBColorSpace, SRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
4
|
ACESFilmicToneMapping, Scene, EventDispatcher, TimestampQuery
|
|
@@ -16,7 +16,7 @@ import { AdaptiveSampling } from './Stages/AdaptiveSampling.js';
|
|
|
16
16
|
import { EdgeFilter } from './Stages/EdgeFilter.js';
|
|
17
17
|
import { AutoExposure } from './Stages/AutoExposure.js';
|
|
18
18
|
import { SSRC } from './Stages/SSRC.js';
|
|
19
|
-
import {
|
|
19
|
+
import { Compositor } from './Stages/Compositor.js';
|
|
20
20
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
21
21
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
22
22
|
import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
|
|
@@ -286,7 +286,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
286
286
|
if ( this._needsDisplayRefresh ) {
|
|
287
287
|
|
|
288
288
|
this._needsDisplayRefresh = false;
|
|
289
|
-
this.stages.
|
|
289
|
+
this.stages.compositor.render( this.pipeline.context );
|
|
290
290
|
this._renderHelperOverlay();
|
|
291
291
|
|
|
292
292
|
}
|
|
@@ -775,7 +775,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
775
775
|
// Apply all settings to stages in one shot
|
|
776
776
|
timer.start( 'Apply settings' );
|
|
777
777
|
this.settings.applyAll();
|
|
778
|
-
this.stages.
|
|
778
|
+
this.stages.compositor.setTransparentBackground( this.settings.get( 'transparentBackground' ) );
|
|
779
779
|
timer.end( 'Apply settings' );
|
|
780
780
|
|
|
781
781
|
timer.print();
|
|
@@ -1006,10 +1006,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1006
1006
|
|
|
1007
1007
|
if ( usePostProcess ) return dm.denoiserCanvas;
|
|
1008
1008
|
|
|
1009
|
-
// Re-render
|
|
1010
|
-
if ( this.stages.
|
|
1009
|
+
// Re-render compositor stage so the WebGPU canvas has valid content
|
|
1010
|
+
if ( this.stages.compositor && this.pipeline?.context ) {
|
|
1011
1011
|
|
|
1012
|
-
this.stages.
|
|
1012
|
+
this.stages.compositor.render( this.pipeline.context );
|
|
1013
1013
|
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
@@ -1208,6 +1208,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1208
1208
|
|
|
1209
1209
|
RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
|
|
1210
1210
|
|
|
1211
|
+
this.renderer.workingColorSpace = SRGBColorSpace;
|
|
1212
|
+
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
1211
1213
|
this.renderer.toneMapping = ACESFilmicToneMapping;
|
|
1212
1214
|
this.renderer.toneMappingExposure = 1.0;
|
|
1213
1215
|
this.renderer.setPixelRatio( 1.0 );
|
|
@@ -1261,7 +1263,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1261
1263
|
this.pipeline.addStage( this.stages.adaptiveSampling );
|
|
1262
1264
|
this.pipeline.addStage( this.stages.edgeFilter );
|
|
1263
1265
|
this.pipeline.addStage( this.stages.autoExposure );
|
|
1264
|
-
this.pipeline.addStage( this.stages.
|
|
1266
|
+
this.pipeline.addStage( this.stages.compositor );
|
|
1265
1267
|
|
|
1266
1268
|
const initRenderW = this.canvas.clientWidth || 1;
|
|
1267
1269
|
const initRenderH = this.canvas.clientHeight || 1;
|
|
@@ -1367,10 +1369,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1367
1369
|
// Bind settings to pipeline stages
|
|
1368
1370
|
this.settings.bind( {
|
|
1369
1371
|
stages: this.stages,
|
|
1372
|
+
renderer: this.renderer,
|
|
1370
1373
|
resetCallback: () => this.reset(),
|
|
1371
1374
|
reconcileCompletion: () => this._reconcileCompletion(),
|
|
1372
1375
|
} );
|
|
1373
1376
|
|
|
1377
|
+
this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
|
|
1378
|
+
|
|
1374
1379
|
// Resize handling
|
|
1375
1380
|
this.onResize();
|
|
1376
1381
|
this.resizeHandler = () => this.onResize();
|
|
@@ -1474,8 +1479,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1474
1479
|
this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
|
|
1475
1480
|
this.stages.autoExposure = new AutoExposure( this.renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
|
|
1476
1481
|
|
|
1477
|
-
this.stages.
|
|
1478
|
-
exposure: ( DEFAULT_STATE.autoExposure ) ? 1.0 : ( this.settings.get( 'exposure' ) ?? 1.0 ),
|
|
1482
|
+
this.stages.compositor = new Compositor( this.renderer, {
|
|
1479
1483
|
saturation: this.settings.get( 'saturation' ) ?? DEFAULT_STATE.saturation,
|
|
1480
1484
|
} );
|
|
1481
1485
|
|
|
@@ -1497,7 +1501,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1497
1501
|
edgeFilter: this.stages.edgeFilter,
|
|
1498
1502
|
ssrc: this.stages.ssrc,
|
|
1499
1503
|
autoExposure: this.stages.autoExposure,
|
|
1500
|
-
|
|
1504
|
+
compositor: this.stages.compositor,
|
|
1501
1505
|
},
|
|
1502
1506
|
pipeline: this.pipeline,
|
|
1503
1507
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
package/src/RenderSettings.js
CHANGED
|
@@ -104,16 +104,16 @@ export class RenderSettings extends EventDispatcher {
|
|
|
104
104
|
* Wires internal references. Called by PathTracerApp after init().
|
|
105
105
|
*
|
|
106
106
|
* @param {Object} params
|
|
107
|
-
* @param {Object} params.stages - Pipeline stages { pathTracer,
|
|
107
|
+
* @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
|
|
108
108
|
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
109
109
|
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
110
110
|
*/
|
|
111
|
-
bind( { stages, resetCallback, reconcileCompletion } ) {
|
|
111
|
+
bind( { stages, renderer, resetCallback, reconcileCompletion } ) {
|
|
112
112
|
|
|
113
113
|
this._pathTracer = stages.pathTracer;
|
|
114
114
|
this._resetCallback = resetCallback;
|
|
115
115
|
this._delegates = {};
|
|
116
|
-
this._handlers = this._buildHandlers( stages, reconcileCompletion );
|
|
116
|
+
this._handlers = this._buildHandlers( stages, renderer, reconcileCompletion );
|
|
117
117
|
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -121,22 +121,24 @@ export class RenderSettings extends EventDispatcher {
|
|
|
121
121
|
* Builds handler functions for multi-stage settings that can't
|
|
122
122
|
* be routed with a simple uniform forward.
|
|
123
123
|
*/
|
|
124
|
-
_buildHandlers( stages, reconcileCompletion ) {
|
|
124
|
+
_buildHandlers( stages, renderer, reconcileCompletion ) {
|
|
125
125
|
|
|
126
126
|
return {
|
|
127
127
|
|
|
128
128
|
handleTransparentBackground: ( value ) => {
|
|
129
129
|
|
|
130
130
|
stages.pathTracer?.setUniform( 'transparentBackground', value );
|
|
131
|
-
stages.
|
|
131
|
+
stages.compositor?.setTransparentBackground( value );
|
|
132
132
|
|
|
133
133
|
},
|
|
134
134
|
|
|
135
135
|
handleExposure: ( value ) => {
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// Three.js applies toneMappingExposure inside the tone-mapping branch,
|
|
138
|
+
// so this has no effect when renderer.toneMapping === NoToneMapping.
|
|
139
|
+
if ( ! stages.autoExposure?.enabled && renderer ) {
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
renderer.toneMappingExposure = value;
|
|
140
142
|
|
|
141
143
|
}
|
|
142
144
|
|
|
@@ -144,7 +146,7 @@ export class RenderSettings extends EventDispatcher {
|
|
|
144
146
|
|
|
145
147
|
handleSaturation: ( value ) => {
|
|
146
148
|
|
|
147
|
-
stages.
|
|
149
|
+
stages.compositor?.setSaturation( value );
|
|
148
150
|
|
|
149
151
|
},
|
|
150
152
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { vec4, vec3, uv, uniform, select, dot, mix } from 'three/tsl';
|
|
2
|
+
import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
|
|
3
|
+
import { NoBlending } from 'three';
|
|
4
|
+
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
5
|
+
import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compositor — Terminal pipeline stage.
|
|
9
|
+
*
|
|
10
|
+
* Selects the latest upstream texture via a priority fallback chain, applies
|
|
11
|
+
* a saturation grade, sets alpha, and hands the linear HDR result to the
|
|
12
|
+
* renderer's output pass (tone mapping + sRGB gamma happen there).
|
|
13
|
+
*
|
|
14
|
+
* Exposure is not applied here — `renderer.toneMappingExposure` owns it,
|
|
15
|
+
* and Three.js applies it inside the tone-mapping branch of the output pass
|
|
16
|
+
* (so it has no effect when `renderer.toneMapping === NoToneMapping`).
|
|
17
|
+
*/
|
|
18
|
+
export class Compositor extends RenderStage {
|
|
19
|
+
|
|
20
|
+
constructor( renderer, options = {} ) {
|
|
21
|
+
|
|
22
|
+
super( 'Compositor', {
|
|
23
|
+
...options,
|
|
24
|
+
executionMode: StageExecutionMode.ALWAYS
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
this.renderer = renderer;
|
|
28
|
+
|
|
29
|
+
// 1.0 = neutral; >1 boosts to compensate for ACES/AgX desaturation.
|
|
30
|
+
this.saturation = uniform( options.saturation ?? 1.0 );
|
|
31
|
+
|
|
32
|
+
this._transparentBackground = uniform( 0, 'int' );
|
|
33
|
+
|
|
34
|
+
// TextureNode reused across frames — only `.value` mutates, shader doesn't recompile.
|
|
35
|
+
this._sourceTexNode = new TextureNode();
|
|
36
|
+
|
|
37
|
+
const texSample = this._sourceTexNode.sample( uv() );
|
|
38
|
+
|
|
39
|
+
const luma = dot( texSample.xyz, REC709_LUMINANCE_COEFFICIENTS );
|
|
40
|
+
const gradedColor = mix( vec3( luma ), texSample.xyz, this.saturation );
|
|
41
|
+
|
|
42
|
+
const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
|
|
43
|
+
|
|
44
|
+
this.compositorMaterial = new MeshBasicNodeMaterial();
|
|
45
|
+
this.compositorMaterial.colorNode = vec4( gradedColor, outputAlpha );
|
|
46
|
+
this.compositorMaterial.blending = NoBlending;
|
|
47
|
+
|
|
48
|
+
this.compositorQuad = new QuadMesh( this.compositorMaterial );
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Later stages in the chain take priority; `pathtracer:color` is the
|
|
54
|
+
* baseline fallback that is always present.
|
|
55
|
+
*/
|
|
56
|
+
_resolveSourceTexture( context ) {
|
|
57
|
+
|
|
58
|
+
return context.getTexture( 'bloom:output' )
|
|
59
|
+
|| context.getTexture( 'edgeFiltering:output' )
|
|
60
|
+
|| context.getTexture( 'asvgf:output' )
|
|
61
|
+
|| context.getTexture( 'ssrc:output' )
|
|
62
|
+
|| context.getTexture( 'pathtracer:color' );
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render( context ) {
|
|
67
|
+
|
|
68
|
+
if ( ! this.enabled ) return;
|
|
69
|
+
|
|
70
|
+
const sourceTexture = this._resolveSourceTexture( context );
|
|
71
|
+
if ( ! sourceTexture ) return;
|
|
72
|
+
|
|
73
|
+
this._sourceTexNode.value = sourceTexture;
|
|
74
|
+
|
|
75
|
+
this.renderer.setRenderTarget( null );
|
|
76
|
+
this.compositorQuad.render( this.renderer );
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setSaturation( value ) {
|
|
81
|
+
|
|
82
|
+
this.saturation.value = value;
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setTransparentBackground( enabled ) {
|
|
87
|
+
|
|
88
|
+
this._transparentBackground.value = enabled ? 1 : 0;
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dispose() {
|
|
93
|
+
|
|
94
|
+
this._sourceTexNode?.dispose();
|
|
95
|
+
this.compositorMaterial?.dispose();
|
|
96
|
+
// QuadMesh extends Mesh — no dispose method; material already released above.
|
|
97
|
+
this.compositorQuad = null;
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
package/src/Stages/EdgeFilter.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
import { Fn,
|
|
2
|
-
If, dot, max, abs, mix,
|
|
1
|
+
import { Fn, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
+
If, dot, max, abs, mix, pow, step,
|
|
3
3
|
textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
5
|
import { HalfFloatType, RGBAFormat, NearestFilter, Box2, Vector2 } from 'three';
|
|
6
6
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
|
+
import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* WebGPU Edge-Aware Filtering Stage (Compute Shader)
|
|
10
|
+
* WebGPU Edge-Aware Filtering Stage (Compute Shader).
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Geometry-guided bilateral filter (8-dir × 2-dist kernel). Edge weights
|
|
13
|
+
* combine luminance, surface normal, and ray distance — see Dammertz 2010
|
|
14
|
+
* "Edge-Avoiding À-Trous" for the structure. Strength decays over iterations
|
|
15
|
+
* so the filter is strongest on early frames and fades as accumulation
|
|
16
|
+
* converges. Single-pass; no temporal reuse.
|
|
14
17
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - Firefly reduction for high-luminance outliers
|
|
20
|
-
*
|
|
21
|
-
* Execution: PER_CYCLE
|
|
22
|
-
*
|
|
23
|
-
* Textures published: edgeFiltering:output
|
|
24
|
-
* Textures read: asvgf:output (fallback: pathtracer:color)
|
|
18
|
+
* Reads: pathtracer:color (or asvgf:output / bilateralFiltering:output)
|
|
19
|
+
* and pathtracer:normalDepth
|
|
20
|
+
* Publishes: edgeFiltering:output
|
|
21
|
+
* Mode: PER_CYCLE
|
|
25
22
|
*/
|
|
26
23
|
export class EdgeFilter extends RenderStage {
|
|
27
24
|
|
|
@@ -34,25 +31,25 @@ export class EdgeFilter extends RenderStage {
|
|
|
34
31
|
|
|
35
32
|
this.renderer = renderer;
|
|
36
33
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
// filterStrength: 0 = passthrough, 1 = fully filtered.
|
|
35
|
+
// strengthDecaySpeed: per-iteration falloff toward passthrough.
|
|
36
|
+
// edgeThreshold: luminance σ. phiNormal: dot(n,n) exponent. phiDepth: depth σ.
|
|
37
|
+
this.filterStrength = uniform( options.filterStrength ?? 0.75 );
|
|
38
|
+
this.strengthDecaySpeed = uniform( options.strengthDecaySpeed ?? 0.05 );
|
|
40
39
|
this.edgeThreshold = uniform( options.edgeThreshold ?? 1.0 );
|
|
40
|
+
this.phiNormal = uniform( options.phiNormal ?? 128.0 );
|
|
41
|
+
this.phiDepth = uniform( options.phiDepth ?? 1.0 );
|
|
41
42
|
this.iterationCount = uniform( 0.0 );
|
|
42
43
|
this.resW = uniform( options.width || 1 );
|
|
43
44
|
this.resH = uniform( options.height || 1 );
|
|
44
45
|
|
|
45
|
-
// Internal counter
|
|
46
46
|
this._iterations = 0;
|
|
47
47
|
|
|
48
|
-
// Input texture node
|
|
49
48
|
this._inputTexNode = new TextureNode();
|
|
49
|
+
this._ndTexNode = new TextureNode();
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// Kept as a defensive pattern: bug #32969 (setSize bind-group staleness)
|
|
54
|
-
// was fixed in r184 (PR #33028), but #33061 (TSL compute pipeline
|
|
55
|
-
// re-compile returns zeros) is still open.
|
|
51
|
+
// Pre-allocate StorageTexture at max — defensive against three.js #33061
|
|
52
|
+
// (TSL compute pipeline re-compile returns zeros after resize).
|
|
56
53
|
const MAX_STORAGE_SIZE = 2048;
|
|
57
54
|
const w = options.width || 1;
|
|
58
55
|
const h = options.height || 1;
|
|
@@ -63,10 +60,8 @@ export class EdgeFilter extends RenderStage {
|
|
|
63
60
|
this._outputStorageTex.minFilter = NearestFilter;
|
|
64
61
|
this._outputStorageTex.magFilter = NearestFilter;
|
|
65
62
|
|
|
66
|
-
// Reusable Box2 for srcRegion in copyTextureToTexture
|
|
67
63
|
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
68
64
|
|
|
69
|
-
// Output RenderTarget (readable copy for downstream stages)
|
|
70
65
|
this.outputTarget = new RenderTarget( w, h, {
|
|
71
66
|
type: HalfFloatType,
|
|
72
67
|
format: RGBAFormat,
|
|
@@ -76,7 +71,6 @@ export class EdgeFilter extends RenderStage {
|
|
|
76
71
|
stencilBuffer: false
|
|
77
72
|
} );
|
|
78
73
|
|
|
79
|
-
// Dispatch dimensions
|
|
80
74
|
this._dispatchX = Math.ceil( w / 16 );
|
|
81
75
|
this._dispatchY = Math.ceil( h / 16 );
|
|
82
76
|
|
|
@@ -87,10 +81,13 @@ export class EdgeFilter extends RenderStage {
|
|
|
87
81
|
_buildCompute() {
|
|
88
82
|
|
|
89
83
|
const inputTex = this._inputTexNode;
|
|
84
|
+
const ndTex = this._ndTexNode;
|
|
90
85
|
const outputStorageTex = this._outputStorageTex;
|
|
91
|
-
const
|
|
92
|
-
const
|
|
86
|
+
const filterStrength = this.filterStrength;
|
|
87
|
+
const decaySpeed = this.strengthDecaySpeed;
|
|
93
88
|
const threshold = this.edgeThreshold;
|
|
89
|
+
const phiNormal = this.phiNormal;
|
|
90
|
+
const phiDepth = this.phiDepth;
|
|
94
91
|
const iterCount = this.iterationCount;
|
|
95
92
|
const resW = this.resW;
|
|
96
93
|
const resH = this.resH;
|
|
@@ -104,41 +101,64 @@ export class EdgeFilter extends RenderStage {
|
|
|
104
101
|
|
|
105
102
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
106
103
|
|
|
107
|
-
const
|
|
108
|
-
const
|
|
104
|
+
const coord = ivec2( gx, gy );
|
|
105
|
+
const center = textureLoad( inputTex, coord ).xyz;
|
|
106
|
+
const centerLum = dot( center, REC709_LUMINANCE_COEFFICIENTS );
|
|
109
107
|
|
|
110
|
-
//
|
|
111
|
-
|
|
108
|
+
// NormalDepth writes (0,0,0, 1e6) for miss rays. Decoded normal
|
|
109
|
+
// (-1,-1,-1) is non-unit and explodes pow(dot, phi); use the depth
|
|
110
|
+
// sentinel as a 0/1 hit flag and zero out cross-kind weights.
|
|
111
|
+
const MISS_THRESHOLD = 1e5;
|
|
112
|
+
const centerND = textureLoad( ndTex, coord );
|
|
113
|
+
const centerNormal = centerND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
114
|
+
const centerDepth = centerND.w;
|
|
115
|
+
const centerIsHit = step( float( MISS_THRESHOLD ), centerDepth ).oneMinus();
|
|
116
|
+
|
|
117
|
+
const effectiveStrength = filterStrength.sub( iterCount.mul( decaySpeed ) ).clamp( 0.0, 1.0 );
|
|
112
118
|
|
|
113
|
-
// Sample 8-direction cross pattern for edge-aware filtering
|
|
114
|
-
// 8 directions x 2 distances + centre
|
|
115
119
|
const colorSum = center.toVar();
|
|
116
120
|
const weightSum = float( 1.0 ).toVar();
|
|
117
121
|
|
|
118
|
-
// Directions: right, up, left, down, and diagonals
|
|
119
122
|
const dirs = [
|
|
120
123
|
[ 1, 0 ], [ 0, 1 ], [ - 1, 0 ], [ 0, - 1 ],
|
|
121
124
|
[ 1, 1 ], [ - 1, 1 ], [ - 1, - 1 ], [ 1, - 1 ],
|
|
122
125
|
];
|
|
123
126
|
|
|
124
|
-
// Sample along each direction at distances 1, 2
|
|
125
127
|
for ( const [ dx, dy ] of dirs ) {
|
|
126
128
|
|
|
127
129
|
for ( const dist of [ 1, 2 ] ) {
|
|
128
130
|
|
|
129
131
|
const sx = gx.add( dx * dist ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
130
132
|
const sy = gy.add( dy * dist ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
+
const sCoord = ivec2( sx, sy );
|
|
134
|
+
|
|
135
|
+
const sColor = textureLoad( inputTex, sCoord ).xyz;
|
|
136
|
+
const sLum = dot( sColor, REC709_LUMINANCE_COEFFICIENTS );
|
|
137
|
+
|
|
138
|
+
const sND = textureLoad( ndTex, sCoord );
|
|
139
|
+
const sNormal = sND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
140
|
+
const sDepth = sND.w;
|
|
141
|
+
const sampleIsHit = step( float( MISS_THRESHOLD ), sDepth ).oneMinus();
|
|
142
|
+
|
|
143
|
+
const lumW = abs( centerLum.sub( sLum ) ).div( max( threshold, float( 0.001 ) ) ).negate().exp();
|
|
144
|
+
|
|
145
|
+
const bothHit = centerIsHit.mul( sampleIsHit );
|
|
146
|
+
const bothMiss = centerIsHit.oneMinus().mul( sampleIsHit.oneMinus() );
|
|
147
|
+
const sameKind = bothHit.add( bothMiss );
|
|
133
148
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
// Clamp dot to [0,1] before pow — miss-ray normals decode to
|
|
150
|
+
// non-unit (-1,-1,-1) with dot=3, which would saturate pow(.,phi)
|
|
151
|
+
// to +inf and poison downstream via inf*0 = NaN.
|
|
152
|
+
const cosTheta = dot( centerNormal, sNormal ).clamp( 0.0, 1.0 );
|
|
153
|
+
const normW = pow( cosTheta, phiNormal );
|
|
154
|
+
const depW = abs( centerDepth.sub( sDepth ) ).div( max( phiDepth, float( 0.001 ) ) ).negate().exp();
|
|
155
|
+
|
|
156
|
+
// Geometric weights only meaningful for hit-vs-hit pairs.
|
|
157
|
+
const geomW = mix( float( 1.0 ), normW.mul( depW ), bothHit );
|
|
137
158
|
|
|
138
|
-
// Distance falloff
|
|
139
159
|
const distWeight = float( 1.0 ).div( float( dist ).add( 0.5 ) );
|
|
160
|
+
const w = lumW.mul( geomW ).mul( sameKind ).mul( distWeight );
|
|
140
161
|
|
|
141
|
-
const w = edgeWeight.mul( distWeight );
|
|
142
162
|
colorSum.addAssign( sColor.mul( w ) );
|
|
143
163
|
weightSum.addAssign( w );
|
|
144
164
|
|
|
@@ -147,12 +167,10 @@ export class EdgeFilter extends RenderStage {
|
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
|
|
170
|
+
const finalColor = mix( center, filtered, effectiveStrength );
|
|
150
171
|
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
// Firefly suppression for very high luminance
|
|
155
|
-
const finalLum = dot( finalColor, vec3( 0.2126, 0.7152, 0.0722 ) );
|
|
172
|
+
// Firefly clamp on output luminance.
|
|
173
|
+
const finalLum = dot( finalColor, REC709_LUMINANCE_COEFFICIENTS );
|
|
156
174
|
const clampedColor = finalColor.toVar();
|
|
157
175
|
If( finalLum.greaterThan( 10.0 ), () => {
|
|
158
176
|
|
|
@@ -181,23 +199,28 @@ export class EdgeFilter extends RenderStage {
|
|
|
181
199
|
|
|
182
200
|
if ( ! this.enabled ) return;
|
|
183
201
|
|
|
184
|
-
// Resolve input with fallback chain
|
|
185
202
|
const inputTex = context.getTexture( 'asvgf:output' )
|
|
186
203
|
|| context.getTexture( 'bilateralFiltering:output' )
|
|
187
204
|
|| context.getTexture( 'pathtracer:color' );
|
|
188
205
|
|
|
189
|
-
|
|
206
|
+
const ndTex = context.getTexture( 'pathtracer:normalDepth' );
|
|
207
|
+
|
|
208
|
+
// Without the G-buffer there's no edge guidance — pass input through
|
|
209
|
+
// rather than producing a uniform blur.
|
|
210
|
+
if ( ! inputTex || ! ndTex ) {
|
|
211
|
+
|
|
212
|
+
if ( inputTex ) context.setTexture( 'edgeFiltering:output', inputTex );
|
|
213
|
+
return;
|
|
214
|
+
|
|
215
|
+
}
|
|
190
216
|
|
|
191
|
-
|
|
192
|
-
const interactionMode = context.getState( 'interactionMode' );
|
|
193
|
-
if ( interactionMode ) {
|
|
217
|
+
if ( context.getState( 'interactionMode' ) ) {
|
|
194
218
|
|
|
195
219
|
context.setTexture( 'edgeFiltering:output', inputTex );
|
|
196
220
|
return;
|
|
197
221
|
|
|
198
222
|
}
|
|
199
223
|
|
|
200
|
-
// Auto-size
|
|
201
224
|
const img = inputTex.image;
|
|
202
225
|
if ( img && img.width > 0 && img.height > 0 ) {
|
|
203
226
|
|
|
@@ -210,23 +233,20 @@ export class EdgeFilter extends RenderStage {
|
|
|
210
233
|
|
|
211
234
|
}
|
|
212
235
|
|
|
213
|
-
// Update input texture
|
|
214
236
|
this._inputTexNode.value = inputTex;
|
|
237
|
+
this._ndTexNode.value = ndTex;
|
|
215
238
|
|
|
216
|
-
// Update iteration count for progressive sharpening
|
|
217
239
|
this._iterations ++;
|
|
218
240
|
this.iterationCount.value = this._iterations;
|
|
219
241
|
|
|
220
|
-
// Dispatch compute
|
|
221
242
|
this.renderer.compute( this._computeNode );
|
|
222
243
|
|
|
223
|
-
// Copy
|
|
224
|
-
//
|
|
244
|
+
// Copy out of the over-allocated StorageTexture into the right-sized
|
|
245
|
+
// RenderTarget; downstream stages can sample the latter.
|
|
225
246
|
this._srcRegion.min.set( 0, 0 );
|
|
226
247
|
this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
|
|
227
248
|
this.renderer.copyTextureToTexture( this._outputStorageTex, this.outputTarget.texture, this._srcRegion );
|
|
228
249
|
|
|
229
|
-
// Publish RenderTarget texture (NOT StorageTexture)
|
|
230
250
|
context.setTexture( 'edgeFiltering:output', this.outputTarget.texture );
|
|
231
251
|
|
|
232
252
|
}
|
|
@@ -240,9 +260,11 @@ export class EdgeFilter extends RenderStage {
|
|
|
240
260
|
updateUniforms( params ) {
|
|
241
261
|
|
|
242
262
|
if ( ! params ) return;
|
|
243
|
-
if ( params.
|
|
244
|
-
if ( params.
|
|
263
|
+
if ( params.filterStrength !== undefined ) this.filterStrength.value = params.filterStrength;
|
|
264
|
+
if ( params.strengthDecaySpeed !== undefined ) this.strengthDecaySpeed.value = params.strengthDecaySpeed;
|
|
245
265
|
if ( params.edgeThreshold !== undefined ) this.edgeThreshold.value = params.edgeThreshold;
|
|
266
|
+
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
267
|
+
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
246
268
|
|
|
247
269
|
}
|
|
248
270
|
|
|
@@ -255,15 +277,12 @@ export class EdgeFilter extends RenderStage {
|
|
|
255
277
|
|
|
256
278
|
setSize( width, height ) {
|
|
257
279
|
|
|
258
|
-
//
|
|
259
|
-
// (see constructor note: pre-allocation is a defensive pattern, retained
|
|
260
|
-
// after r184 fixed #32969, because #33061 is still open.)
|
|
280
|
+
// StorageTexture stays at its max allocation (see constructor).
|
|
261
281
|
this.outputTarget.setSize( width, height );
|
|
262
282
|
this.outputTarget.texture.needsUpdate = true;
|
|
263
283
|
this.resW.value = width;
|
|
264
284
|
this.resH.value = height;
|
|
265
285
|
|
|
266
|
-
// Update dispatch dimensions
|
|
267
286
|
this._dispatchX = Math.ceil( width / 16 );
|
|
268
287
|
this._dispatchY = Math.ceil( height / 16 );
|
|
269
288
|
this._computeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
@@ -276,6 +295,7 @@ export class EdgeFilter extends RenderStage {
|
|
|
276
295
|
this._outputStorageTex?.dispose();
|
|
277
296
|
this.outputTarget?.dispose();
|
|
278
297
|
this._inputTexNode?.dispose();
|
|
298
|
+
this._ndTexNode?.dispose();
|
|
279
299
|
|
|
280
300
|
}
|
|
281
301
|
|