rayzee 5.7.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.7.1",
3
+ "version": "5.8.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -90,8 +90,8 @@ export const ENGINE_DEFAULTS = {
90
90
  directionalLightPosition: [ 1, 1, 1 ],
91
91
  directionalLightAngle: 0.0,
92
92
 
93
- pixelEdgeSharpness: 0.75,
94
- edgeSharpenSpeed: 0.05,
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: 'edgeaware',
118
+ denoiserStrategy: 'none',
119
119
 
120
120
  enableASVGF: false,
121
121
  asvgfTemporalAlpha: 0.1,
@@ -1,27 +1,24 @@
1
- import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
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
- * Edge-preserving temporal filtering with progressive edge sharpening.
12
- * Uses a large directional sampling kernel for edge-guided smoothing,
13
- * then a smaller kernel for edge-detail refinement.
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
- * Features:
16
- * - Scene-adaptive: aggressive filtering for dynamic, progressive for static
17
- * - Edge sharpening increases over iterations
18
- * - Fast path during interaction mode (direct copy, no filtering cost)
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
- // Parameters
38
- this.pixelEdgeSharpness = uniform( options.pixelEdgeSharpness ?? 0.75 );
39
- this.edgeSharpenSpeed = uniform( options.edgeSharpenSpeed ?? 0.05 );
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
- // Output StorageTexture (compute writes here)
52
- // Pre-allocated at max size NEVER resize/dispose after this.
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 sharpness = this.pixelEdgeSharpness;
92
- const sharpenSpeed = this.edgeSharpenSpeed;
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 center = textureLoad( inputTex, ivec2( gx, gy ) ).xyz;
108
- const centerLum = dot( center, vec3( 0.2126, 0.7152, 0.0722 ) );
104
+ const coord = ivec2( gx, gy );
105
+ const center = textureLoad( inputTex, coord ).xyz;
106
+ const centerLum = dot( center, REC709_LUMINANCE_COEFFICIENTS );
109
107
 
110
- // Progressive edge sharpening factor
111
- const edgeFactor = sharpness.add( iterCount.mul( sharpenSpeed ) ).clamp( 0.0, 0.95 );
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 sColor = textureLoad( inputTex, ivec2( sx, sy ) ).xyz;
132
- const sLum = dot( sColor, vec3( 0.2126, 0.7152, 0.0722 ) );
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
- // Luminance-based edge weight
135
- const lumDiff = abs( centerLum.sub( sLum ) );
136
- const edgeWeight = lumDiff.div( max( threshold, float( 0.001 ) ) ).negate().exp();
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
- // Blend between filtered and original based on edge sharpening factor
152
- const finalColor = mix( filtered, center, edgeFactor );
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
- if ( ! inputTex ) return;
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
- // Fast path during interaction mode — direct copy
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 StorageTexture RenderTarget for downstream readability
224
- // Use Box2 srcRegion since StorageTexture is pre-allocated at max size
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.pixelEdgeSharpness !== undefined ) this.pixelEdgeSharpness.value = params.pixelEdgeSharpness;
244
- if ( params.edgeSharpenSpeed !== undefined ) this.edgeSharpenSpeed.value = params.edgeSharpenSpeed;
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
- // Only resize the RenderTarget — StorageTexture stays at max allocation
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