rayzee 7.20.3 → 7.22.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/README.md +75 -4
- package/dist/rayzee.es.js +2821 -2854
- package/dist/rayzee.umd.js +60 -58
- package/package.json +1 -1
- package/src/EngineDefaults.js +9 -2
- package/src/PathTracerApp.js +83 -0
- package/src/Pipeline/BindingAudit.js +178 -0
- package/src/Pipeline/RenderPipeline.js +5 -2
- package/src/Processor/BVHBuilder.js +1 -4
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/KernelManager.js +17 -70
- package/src/Processor/PackedRayBuffer.js +33 -8
- package/src/Processor/QueueManager.js +4 -3
- package/src/Processor/SceneProcessor.js +10 -4
- package/src/Processor/TreeletOptimizer.js +0 -7
- package/src/RenderSettings.js +41 -6
- package/src/Stages/ASVGF.js +10 -10
- package/src/Stages/BilateralFilter.js +70 -11
- package/src/Stages/EdgeFilter.js +44 -6
- package/src/Stages/NormalDepth.js +26 -15
- package/src/Stages/PathTracer.js +36 -34
- package/src/Stages/PathTracerStage.js +15 -46
- package/src/TSL/BVHTraversal.js +1 -67
- package/src/TSL/CameraRay.js +141 -0
- package/src/TSL/Common.js +1 -47
- package/src/TSL/DebugKernel.js +4 -5
- package/src/TSL/EmissiveSampling.js +40 -44
- package/src/TSL/ExtendKernel.js +11 -3
- package/src/TSL/FinalWriteKernel.js +1 -14
- package/src/TSL/GenerateKernel.js +11 -8
- package/src/TSL/LightsCore.js +3 -0
- package/src/TSL/LightsDirect.js +4 -4
- package/src/TSL/LightsSampling.js +138 -327
- package/src/TSL/PathTracerCore.js +2 -2
- package/src/TSL/Random.js +2 -2
- package/src/TSL/ShadeKernel.js +39 -17
- package/src/TSL/SortGlobalKernels.js +52 -7
- package/src/TSL/Struct.js +0 -10
- package/src/index.js +5 -0
- package/src/managers/DenoisingManager.js +13 -0
- package/src/managers/EnvironmentManager.js +6 -8
- package/src/managers/UniformManager.js +12 -2
package/src/RenderSettings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventDispatcher, Color } from 'three';
|
|
1
|
+
import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
|
|
2
2
|
import { ENGINE_DEFAULTS } from './EngineDefaults.js';
|
|
3
3
|
import { EngineEvents } from './EngineEvents.js';
|
|
4
4
|
|
|
@@ -32,6 +32,7 @@ const SETTING_ROUTES = {
|
|
|
32
32
|
enableGroundCatcher: { uniform: 'enableGroundCatcher', reset: true },
|
|
33
33
|
groundCatcherHeight: { uniform: 'groundCatcherHeight', reset: true },
|
|
34
34
|
globalIlluminationIntensity: { uniform: 'globalIlluminationIntensity', reset: true },
|
|
35
|
+
panoramaLevelHorizon: { uniform: 'panoLevelHorizon', reset: true },
|
|
35
36
|
enableDOF: { uniform: 'enableDOF', reset: true },
|
|
36
37
|
focusDistance: { uniform: 'focusDistance', reset: false },
|
|
37
38
|
focalLength: { uniform: 'focalLength', reset: true },
|
|
@@ -56,6 +57,9 @@ const SETTING_ROUTES = {
|
|
|
56
57
|
|
|
57
58
|
// ── Multi-stage / special handling ────────────────────────────
|
|
58
59
|
|
|
60
|
+
cameraProjection: { handler: 'handleCameraProjection', reset: true },
|
|
61
|
+
panoramaLonRange: { handler: 'handlePanoramaLonRange', reset: true },
|
|
62
|
+
panoramaLatRange: { handler: 'handlePanoramaLatRange', reset: true },
|
|
59
63
|
interactionModeEnabled: { handler: 'handleInteractionModeEnabled', reset: false },
|
|
60
64
|
maxSamples: { handler: 'handleMaxSamples', reset: false },
|
|
61
65
|
transparentBackground: { handler: 'handleTransparentBackground' },
|
|
@@ -122,13 +126,15 @@ export class RenderSettings extends EventDispatcher {
|
|
|
122
126
|
* @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
|
|
123
127
|
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
124
128
|
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
129
|
+
* @param {Object} [params.denoisingManager] - Needed to force ASVGF off under panorama
|
|
130
|
+
* @param {Object} [params.cameraManager] - Needed to force auto-focus manual under panorama
|
|
125
131
|
*/
|
|
126
|
-
bind(
|
|
132
|
+
bind( params ) {
|
|
127
133
|
|
|
128
|
-
this._pathTracer = stages.pathTracer;
|
|
129
|
-
this._resetCallback = resetCallback;
|
|
134
|
+
this._pathTracer = params.stages.pathTracer;
|
|
135
|
+
this._resetCallback = params.resetCallback;
|
|
130
136
|
this._delegates = {};
|
|
131
|
-
this._handlers = this._buildHandlers(
|
|
137
|
+
this._handlers = this._buildHandlers( params );
|
|
132
138
|
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -136,10 +142,39 @@ export class RenderSettings extends EventDispatcher {
|
|
|
136
142
|
* Builds handler functions for multi-stage settings that can't
|
|
137
143
|
* be routed with a simple uniform forward.
|
|
138
144
|
*/
|
|
139
|
-
_buildHandlers( stages, renderer, reconcileCompletion ) {
|
|
145
|
+
_buildHandlers( { stages, renderer, reconcileCompletion, denoisingManager, cameraManager } ) {
|
|
146
|
+
|
|
147
|
+
// UniformManager copies into the existing node, so one scratch vector serves every write.
|
|
148
|
+
const panoScratch = new Vector2();
|
|
149
|
+
const setPanoRange = ( uniform, value ) => {
|
|
150
|
+
|
|
151
|
+
panoScratch.set( value?.[ 0 ] ?? 0, value?.[ 1 ] ?? 0 ).multiplyScalar( MathUtils.DEG2RAD );
|
|
152
|
+
stages.pathTracer?.setUniform( uniform, panoScratch );
|
|
153
|
+
|
|
154
|
+
};
|
|
140
155
|
|
|
141
156
|
return {
|
|
142
157
|
|
|
158
|
+
handleCameraProjection: ( value ) => {
|
|
159
|
+
|
|
160
|
+
const isPanorama = value === 'equirectangular';
|
|
161
|
+
stages.pathTracer?.setUniform( 'cameraProjection', isPanorama ? 1 : 0 );
|
|
162
|
+
|
|
163
|
+
if ( ! isPanorama ) return;
|
|
164
|
+
|
|
165
|
+
// ASVGF is driven entirely by MotionVector, which unprojects through
|
|
166
|
+
// projectionMatrixInverse — meaningless once every pixel is its own direction.
|
|
167
|
+
// Fall back to the spatial-only denoiser rather than leaving no strategy.
|
|
168
|
+
if ( denoisingManager?.denoiserStrategy === 'asvgf' ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
|
|
169
|
+
// Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
|
|
170
|
+
cameraManager?.setAutoFocusMode( 'manual' );
|
|
171
|
+
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
handlePanoramaLonRange: ( value ) => setPanoRange( 'panoLonRange', value ),
|
|
175
|
+
|
|
176
|
+
handlePanoramaLatRange: ( value ) => setPanoRange( 'panoLatRange', value ),
|
|
177
|
+
|
|
143
178
|
handleTransparentBackground: ( value ) => {
|
|
144
179
|
|
|
145
180
|
stages.pathTracer?.setUniform( 'transparentBackground', value );
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -40,7 +40,8 @@ export class ASVGF extends RenderStage {
|
|
|
40
40
|
this.renderer = renderer;
|
|
41
41
|
|
|
42
42
|
this.temporalAlpha = uniform( options.temporalAlpha ?? 0.0 );
|
|
43
|
-
|
|
43
|
+
// > 0 is required for firefly rejection, not just anti-lag — see ENGINE_DEFAULTS.
|
|
44
|
+
this.gradientStrength = uniform( options.gradientStrength ?? 1.0 );
|
|
44
45
|
// σ multiplier for the per-pixel noise floor (NRD luminanceSigmaScale ≈ 2).
|
|
45
46
|
this.gradientSigmaScale = uniform( options.gradientSigmaScale ?? 2.0 );
|
|
46
47
|
// Secondary relative floor on the normalised gradient (0 = rely on σ alone).
|
|
@@ -380,7 +381,12 @@ export class ASVGF extends RenderStage {
|
|
|
380
381
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
381
382
|
|
|
382
383
|
const coord = ivec2( gx, gy );
|
|
383
|
-
const
|
|
384
|
+
const currentSample = textureLoad( colorTex, coord );
|
|
385
|
+
const currentColor = currentSample.xyz;
|
|
386
|
+
// Coverage from the path tracer — 0 on background rays when
|
|
387
|
+
// transparentBackground is on. Forcing 1.0 here made the whole frame opaque,
|
|
388
|
+
// so a transparent render composited as black.
|
|
389
|
+
const currentAlpha = currentSample.w;
|
|
384
390
|
const currentAlbedo = textureLoad( albedoTex, coord ).xyz;
|
|
385
391
|
|
|
386
392
|
// Same safeAlbedo on both demod and re-mod sides → exact
|
|
@@ -390,7 +396,7 @@ export class ASVGF extends RenderStage {
|
|
|
390
396
|
|
|
391
397
|
// Defaults = fresh sample (no temporal blend).
|
|
392
398
|
const demodResult = vec4( currentLighting, 1.0 ).toVar();
|
|
393
|
-
const modulatedResult = vec4( currentColor,
|
|
399
|
+
const modulatedResult = vec4( currentColor, currentAlpha ).toVar();
|
|
394
400
|
|
|
395
401
|
// forceResetU skips the blend for one frame after asvgf:reset → re-anchors
|
|
396
402
|
// history to the current (post-reset) scene instead of the stale ping-pong.
|
|
@@ -491,7 +497,7 @@ export class ASVGF extends RenderStage {
|
|
|
491
497
|
const newHistory = min( prevHistory.add( 1.0 ), maxAccumFrames );
|
|
492
498
|
|
|
493
499
|
demodResult.assign( vec4( blendedLighting, newHistory ) );
|
|
494
|
-
modulatedResult.assign( vec4( blendedLighting.mul( safeAlbedo ),
|
|
500
|
+
modulatedResult.assign( vec4( blendedLighting.mul( safeAlbedo ), currentAlpha ) );
|
|
495
501
|
|
|
496
502
|
} );
|
|
497
503
|
|
|
@@ -643,12 +649,6 @@ export class ASVGF extends RenderStage {
|
|
|
643
649
|
|
|
644
650
|
this.on( 'asvgf:reset', () => this.resetTemporalData() );
|
|
645
651
|
|
|
646
|
-
this.on( 'asvgf:setTemporal', ( data ) => {
|
|
647
|
-
|
|
648
|
-
if ( data && data.enabled !== undefined ) this.setTemporalEnabled( data.enabled );
|
|
649
|
-
|
|
650
|
-
} );
|
|
651
|
-
|
|
652
652
|
this.on( 'asvgf:updateParameters', ( data ) => this.updateParameters( data ) );
|
|
653
653
|
|
|
654
654
|
}
|
|
@@ -24,7 +24,9 @@ const bilateralWeight = /*@__PURE__*/ wgslFn( `
|
|
|
24
24
|
let lumW = exp( -abs( centerLum - sLum ) / sigmaL );
|
|
25
25
|
// clamp dot to [0,1]: miss-ray normals decode to (-1,-1,-1) with
|
|
26
26
|
// dot=3 → pow saturates to +inf → inf*0 = NaN. See project_tsl_pitfalls.
|
|
27
|
-
|
|
27
|
+
// max(phiNorm, 1e-6): pow(0.0, 0.0) is undefined in WGSL and returned NaN on ~12%
|
|
28
|
+
// of pixels when phiNormal was set to 0.
|
|
29
|
+
let normW = pow( clamp( dot( centerNormal, sNormal ), 0.0, 1.0 ), max( phiNorm, 1e-6 ) );
|
|
28
30
|
let depW = exp( -abs( centerDepth - sDepth ) / max( centerDepth * phiDep, 0.001 ) );
|
|
29
31
|
let maxDiff = max( max( abs( centerColor.x - sColor.x ),
|
|
30
32
|
abs( centerColor.y - sColor.y ) ),
|
|
@@ -79,6 +81,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
79
81
|
this.stepSizeU = uniform( 1, 'int' );
|
|
80
82
|
// 1 on the final iteration → multiply by albedo to remodulate.
|
|
81
83
|
this.isLastIterationU = uniform( 0, 'int' );
|
|
84
|
+
// 1 on pass 0 → seed variance from variance:output; later passes read the carried value.
|
|
85
|
+
this.isFirstIterationU = uniform( 1, 'int' );
|
|
82
86
|
this.resW = uniform( options.width || 1 );
|
|
83
87
|
this.resH = uniform( options.height || 1 );
|
|
84
88
|
|
|
@@ -87,6 +91,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
87
91
|
this._shadingNormalTexNode = new TextureNode();
|
|
88
92
|
this._albedoTexNode = new TextureNode();
|
|
89
93
|
this._varianceTexNode = new TextureNode();
|
|
94
|
+
// pathtracer:color, read only for its alpha (coverage) on the final pass.
|
|
95
|
+
this._srcAlphaTexNode = new TextureNode();
|
|
90
96
|
|
|
91
97
|
const w = options.width || 1;
|
|
92
98
|
const h = options.height || 1;
|
|
@@ -144,6 +150,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
144
150
|
const snTexNode = this._shadingNormalTexNode;
|
|
145
151
|
const albedoTexNode = this._albedoTexNode;
|
|
146
152
|
const varTexNode = this._varianceTexNode;
|
|
153
|
+
const srcAlphaTexNode = this._srcAlphaTexNode;
|
|
147
154
|
const phiColor = this.phiColor;
|
|
148
155
|
const phiNormal = this.phiNormal;
|
|
149
156
|
const phiDepth = this.phiDepth;
|
|
@@ -151,6 +158,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
151
158
|
const spatialVarianceWeight = this.spatialVarianceWeight;
|
|
152
159
|
const stepSize = this.stepSizeU;
|
|
153
160
|
const isLastIterationU = this.isLastIterationU;
|
|
161
|
+
const isFirstIterationU = this.isFirstIterationU;
|
|
154
162
|
const resW = this.resW;
|
|
155
163
|
const resH = this.resH;
|
|
156
164
|
|
|
@@ -173,7 +181,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
173
181
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
174
182
|
|
|
175
183
|
const coord = ivec2( gx, gy );
|
|
176
|
-
const
|
|
184
|
+
const centerSample = textureLoad( readTexNode, coord );
|
|
185
|
+
const centerColor = centerSample.xyz;
|
|
177
186
|
const centerND = textureLoad( ndTexNode, coord );
|
|
178
187
|
// Normal edge-stop reads the mapped (shading) normal; depth gate stays geometric.
|
|
179
188
|
const centerNormal = textureLoad( snTexNode, coord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
@@ -182,22 +191,43 @@ export class BilateralFilter extends RenderStage {
|
|
|
182
191
|
const centerSafeAlbedo = max( textureLoad( albedoTexNode, coord ).xyz, vec3( ALBEDO_EPS ) );
|
|
183
192
|
const centerAlbedoLum = max( luminance( centerSafeAlbedo ), float( ALBEDO_EPS ) ).toVar();
|
|
184
193
|
|
|
194
|
+
// Variance is filtered alongside colour and carried between passes in the
|
|
195
|
+
// ping-pong's spare alpha, so sigma_l tightens as the signal smooths. Without
|
|
196
|
+
// it every pass re-read the unfiltered variance:output and filter strength
|
|
197
|
+
// never backed off as the render converged.
|
|
198
|
+
//
|
|
199
|
+
// Stored as the ROOT: converged variance reaches ~1e-6, a half-float subnormal
|
|
200
|
+
// that flushes to zero on some drivers. select() rather than If() — an If
|
|
201
|
+
// around a texture read lets the inactive branch contaminate the result.
|
|
202
|
+
// .z = temporal, .w = spatial 3×3; blending toward max widens sigma_l where
|
|
203
|
+
// history is thin.
|
|
204
|
+
const isFirst = isFirstIterationU.equal( int( 1 ) );
|
|
205
|
+
const seedVariance = ( at ) => {
|
|
206
|
+
|
|
207
|
+
const v = textureLoad( varTexNode, at );
|
|
208
|
+
return mix( v.z, max( v.z, v.w ), spatialVarianceWeight );
|
|
209
|
+
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const centerVariance = isFirst.select(
|
|
213
|
+
seedVariance( coord ),
|
|
214
|
+
centerSample.w.mul( centerSample.w )
|
|
215
|
+
).toVar();
|
|
216
|
+
|
|
185
217
|
// sigma_l = phiLum * √variance / albedoLum + ε. Dividing by
|
|
186
218
|
// albedoLum compensates for the 1/albedo noise amplification
|
|
187
219
|
// from demodulation — otherwise dark materials get an
|
|
188
220
|
// under-estimated sigma → over-strict luminance gate → no
|
|
189
221
|
// blending → silhouette dark-outline artifact.
|
|
190
|
-
// .z = temporal variance, .w = spatial (3×3) variance. Blend toward
|
|
191
|
-
// max(temporal, spatial) so disoccluded/low-history pixels widen sigma_l.
|
|
192
|
-
const vSample = textureLoad( varTexNode, coord );
|
|
193
|
-
const variance = mix( vSample.z, max( vSample.z, vSample.w ), spatialVarianceWeight );
|
|
194
222
|
const sigmaL = phiLuminance
|
|
195
|
-
.mul( sqrt( max(
|
|
223
|
+
.mul( sqrt( max( centerVariance, float( 0.0 ) ) ) )
|
|
196
224
|
.div( centerAlbedoLum )
|
|
197
225
|
.add( float( 0.0001 ) );
|
|
198
226
|
|
|
199
227
|
const colorSum = vec3( 0.0 ).toVar();
|
|
200
228
|
const weightSum = float( 0.0 ).toVar();
|
|
229
|
+
// Squared weights, normalised by weightSum² — the variance of a weighted mean.
|
|
230
|
+
const varianceSum = float( 0.0 ).toVar();
|
|
201
231
|
|
|
202
232
|
// 5×5 à-trous kernel (Gaussian-approx, Σ=1)
|
|
203
233
|
for ( let iy = 0; iy < 5; iy ++ ) {
|
|
@@ -213,7 +243,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
213
243
|
const sy = gy.add( stepSize.mul( dy ) )
|
|
214
244
|
.clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
215
245
|
|
|
216
|
-
const
|
|
246
|
+
const sSample = textureLoad( readTexNode, ivec2( sx, sy ) );
|
|
247
|
+
const sColor = sSample.xyz;
|
|
217
248
|
const sND = textureLoad( ndTexNode, ivec2( sx, sy ) );
|
|
218
249
|
const sNormal = textureLoad( snTexNode, ivec2( sx, sy ) ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
219
250
|
const sDepth = sND.w;
|
|
@@ -228,24 +259,39 @@ export class BilateralFilter extends RenderStage {
|
|
|
228
259
|
sigmaL, phiNormal, phiDepth, phiColor
|
|
229
260
|
);
|
|
230
261
|
|
|
262
|
+
const sVariance = isFirst.select(
|
|
263
|
+
seedVariance( ivec2( sx, sy ) ),
|
|
264
|
+
sSample.w.mul( sSample.w )
|
|
265
|
+
);
|
|
266
|
+
|
|
231
267
|
colorSum.addAssign( sColor.mul( w ) );
|
|
232
268
|
weightSum.addAssign( w );
|
|
269
|
+
varianceSum.addAssign( w.mul( w ).mul( max( sVariance, float( 0.0 ) ) ) );
|
|
233
270
|
|
|
234
271
|
}
|
|
235
272
|
|
|
236
273
|
}
|
|
237
274
|
|
|
238
275
|
const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
|
|
276
|
+
const filteredVariance = varianceSum
|
|
277
|
+
.div( max( weightSum.mul( weightSum ), float( 1e-8 ) ) );
|
|
239
278
|
|
|
240
279
|
// Remodulate by albedo only on the final iteration so the
|
|
241
280
|
// inner ping-pong stays in demodulated space.
|
|
242
281
|
const isLast = isLastIterationU.equal( int( 1 ) );
|
|
243
282
|
const output = isLast.select( filtered.mul( centerSafeAlbedo ), filtered );
|
|
244
283
|
|
|
284
|
+
// The last pass restores the path tracer's coverage — Compositor reads .w as
|
|
285
|
+
// image alpha when transparentBackground is on.
|
|
286
|
+
const carriedAlpha = isLast.select(
|
|
287
|
+
textureLoad( srcAlphaTexNode, coord ).w,
|
|
288
|
+
sqrt( max( filteredVariance, float( 0.0 ) ) )
|
|
289
|
+
);
|
|
290
|
+
|
|
245
291
|
textureStore(
|
|
246
292
|
writeStorageTex,
|
|
247
293
|
uvec2( uint( gx ), uint( gy ) ),
|
|
248
|
-
vec4( output,
|
|
294
|
+
vec4( output, carriedAlpha )
|
|
249
295
|
).toWriteOnly();
|
|
250
296
|
|
|
251
297
|
} );
|
|
@@ -271,6 +317,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
271
317
|
const snTex = context.getTexture( this.shadingNormalTextureName ) || ndTex;
|
|
272
318
|
const albedoTex = context.getTexture( this.albedoTextureName );
|
|
273
319
|
const varTex = context.getTexture( this.varianceTextureName );
|
|
320
|
+
const srcAlphaTex = context.getTexture( 'pathtracer:color' );
|
|
274
321
|
|
|
275
322
|
if ( ! inputTex ) return;
|
|
276
323
|
|
|
@@ -291,6 +338,18 @@ export class BilateralFilter extends RenderStage {
|
|
|
291
338
|
if ( ndTex ) this._normalDepthTexNode.value = ndTex;
|
|
292
339
|
if ( snTex ) this._shadingNormalTexNode.value = snTex;
|
|
293
340
|
if ( albedoTex ) this._albedoTexNode.value = albedoTex;
|
|
341
|
+
// Bind these BEFORE compile too: two TextureNodes still holding the default empty
|
|
342
|
+
// texture when the node builder runs share one GPU binding, which then resolves to
|
|
343
|
+
// whichever is assigned last. That aliased varTexNode onto the colour input.
|
|
344
|
+
//
|
|
345
|
+
// Safe only because inputTex is a RenderTarget texture, which produces the same
|
|
346
|
+
// `textureLoad(t, coord, level)` codegen as EmptyTexture — the form the StorageTexture
|
|
347
|
+
// ping-pong needs on passes 1+. Binding an actual StorageTexture here would emit the
|
|
348
|
+
// no-level form and later reads would return zero. EdgeFilter must NOT do this: its
|
|
349
|
+
// pass-0 input IS a StorageTexture.
|
|
350
|
+
this._readTexNode.value = inputTex;
|
|
351
|
+
if ( varTex ) this._varianceTexNode.value = varTex;
|
|
352
|
+
if ( srcAlphaTex ) this._srcAlphaTexNode.value = srcAlphaTex;
|
|
294
353
|
|
|
295
354
|
// First-frame compile while StorageTexture-typed nodes still hold
|
|
296
355
|
// EmptyTexture — codegen then emits textureLoad with the level
|
|
@@ -303,8 +362,6 @@ export class BilateralFilter extends RenderStage {
|
|
|
303
362
|
|
|
304
363
|
}
|
|
305
364
|
|
|
306
|
-
if ( varTex ) this._varianceTexNode.value = varTex;
|
|
307
|
-
|
|
308
365
|
// À-trous iterations: step size 2^i, ping-pong write direction.
|
|
309
366
|
// Last iteration multiplies by albedo to remodulate.
|
|
310
367
|
let readTex = inputTex;
|
|
@@ -316,6 +373,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
316
373
|
this.stepSizeU.value = 1 << i;
|
|
317
374
|
this._readTexNode.value = readTex;
|
|
318
375
|
this.isLastIterationU.value = ( i === this.iterations - 1 ) ? 1 : 0;
|
|
376
|
+
this.isFirstIterationU.value = i === 0 ? 1 : 0;
|
|
319
377
|
|
|
320
378
|
this.renderer.compute( writeNode );
|
|
321
379
|
|
|
@@ -420,6 +478,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
420
478
|
this._shadingNormalTexNode?.dispose();
|
|
421
479
|
this._albedoTexNode?.dispose();
|
|
422
480
|
this._varianceTexNode?.dispose();
|
|
481
|
+
this._srcAlphaTexNode?.dispose();
|
|
423
482
|
|
|
424
483
|
}
|
|
425
484
|
|
package/src/Stages/EdgeFilter.js
CHANGED
|
@@ -73,6 +73,8 @@ export class EdgeFilter extends RenderStage {
|
|
|
73
73
|
|
|
74
74
|
this.stepSizeU = uniform( 1, 'int' );
|
|
75
75
|
this.isLastIterationU = uniform( 0, 'int' );
|
|
76
|
+
// 1 on pass 0 → seed variance from variance:output; later passes read the carried value.
|
|
77
|
+
this.isFirstIterationU = uniform( 1, 'int' );
|
|
76
78
|
this.resW = uniform( options.width || 1 );
|
|
77
79
|
this.resH = uniform( options.height || 1 );
|
|
78
80
|
|
|
@@ -202,6 +204,7 @@ export class EdgeFilter extends RenderStage {
|
|
|
202
204
|
const phiDepth = this.phiDepth;
|
|
203
205
|
const stepSize = this.stepSizeU;
|
|
204
206
|
const isLastIterationU = this.isLastIterationU;
|
|
207
|
+
const isFirstIterationU = this.isFirstIterationU;
|
|
205
208
|
const resW = this.resW;
|
|
206
209
|
const resH = this.resH;
|
|
207
210
|
|
|
@@ -216,7 +219,8 @@ export class EdgeFilter extends RenderStage {
|
|
|
216
219
|
|
|
217
220
|
const coord = ivec2( gx, gy );
|
|
218
221
|
|
|
219
|
-
const
|
|
222
|
+
const centerSample = textureLoad( readTex, coord );
|
|
223
|
+
const centerLighting = centerSample.xyz;
|
|
220
224
|
const centerLum = luminance( centerLighting );
|
|
221
225
|
const centerND = textureLoad( ndTex, coord );
|
|
222
226
|
const centerDepth = centerND.w;
|
|
@@ -233,13 +237,30 @@ export class EdgeFilter extends RenderStage {
|
|
|
233
237
|
// max() falls back to the spatial estimate as temporal collapses on a
|
|
234
238
|
// converging accumulation buffer. Dividing by albedoLum rescales the
|
|
235
239
|
// modulated-space variance into demodulated space (std ∝ 1/albedo).
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
240
|
+
//
|
|
241
|
+
// Filtered alongside colour and carried between passes as √variance in the
|
|
242
|
+
// ping-pong's spare alpha, so sigma_l tightens as the signal smooths. Stored as
|
|
243
|
+
// the root because converged variance (~1e-6) is a half-float subnormal.
|
|
244
|
+
const isFirst = isFirstIterationU.equal( int( 1 ) );
|
|
245
|
+
const seedVariance = ( at ) => {
|
|
246
|
+
|
|
247
|
+
const v = textureLoad( varTex, at );
|
|
248
|
+
return max( v.z, v.w );
|
|
249
|
+
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const centerVariance = isFirst.select(
|
|
253
|
+
seedVariance( coord ),
|
|
254
|
+
centerSample.w.mul( centerSample.w )
|
|
255
|
+
).toVar();
|
|
256
|
+
|
|
257
|
+
const sigmaL = phiLuminance.mul( sqrt( max( centerVariance, float( 0.0 ) ) ) )
|
|
239
258
|
.div( centerAlbedoLum ).add( float( 0.0001 ) );
|
|
240
259
|
|
|
241
260
|
const colorSum = vec3( 0.0 ).toVar();
|
|
242
261
|
const weightSum = float( 0.0 ).toVar();
|
|
262
|
+
// Squared weights, normalised by weightSum² — the variance of a weighted mean.
|
|
263
|
+
const varianceSum = float( 0.0 ).toVar();
|
|
243
264
|
|
|
244
265
|
for ( let iy = 0; iy < 5; iy ++ ) {
|
|
245
266
|
|
|
@@ -253,7 +274,8 @@ export class EdgeFilter extends RenderStage {
|
|
|
253
274
|
const sy = gy.add( stepSize.mul( dy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
254
275
|
const sCoord = ivec2( sx, sy );
|
|
255
276
|
|
|
256
|
-
const
|
|
277
|
+
const sSample = textureLoad( readTex, sCoord );
|
|
278
|
+
const sLighting = sSample.xyz;
|
|
257
279
|
const sLum = luminance( sLighting );
|
|
258
280
|
const sND = textureLoad( ndTex, sCoord );
|
|
259
281
|
const sDepth = sND.w;
|
|
@@ -276,14 +298,22 @@ export class EdgeFilter extends RenderStage {
|
|
|
276
298
|
const geomW = mix( float( 1.0 ), normW.mul( depW ), bothHit );
|
|
277
299
|
const w = float( kw ).mul( lumW ).mul( geomW ).mul( sameKind );
|
|
278
300
|
|
|
301
|
+
const sVariance = isFirst.select(
|
|
302
|
+
seedVariance( sCoord ),
|
|
303
|
+
sSample.w.mul( sSample.w )
|
|
304
|
+
);
|
|
305
|
+
|
|
279
306
|
colorSum.addAssign( sLighting.mul( w ) );
|
|
280
307
|
weightSum.addAssign( w );
|
|
308
|
+
varianceSum.addAssign( w.mul( w ).mul( max( sVariance, float( 0.0 ) ) ) );
|
|
281
309
|
|
|
282
310
|
}
|
|
283
311
|
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
|
|
315
|
+
const filteredVariance = varianceSum
|
|
316
|
+
.div( max( weightSum.mul( weightSum ), float( 1e-8 ) ) );
|
|
287
317
|
|
|
288
318
|
// Final pass: remodulate by albedo and blend against the raw color by
|
|
289
319
|
// filterStrength. Inner passes stay in demodulated space.
|
|
@@ -293,7 +323,14 @@ export class EdgeFilter extends RenderStage {
|
|
|
293
323
|
const finalModulated = mix( rawColor, remodded, filterStrength );
|
|
294
324
|
const output = isLast.select( finalModulated, filtered );
|
|
295
325
|
|
|
296
|
-
|
|
326
|
+
// The last pass restores the path tracer's coverage — Compositor reads .w as
|
|
327
|
+
// image alpha when transparentBackground is on.
|
|
328
|
+
const carriedAlpha = isLast.select(
|
|
329
|
+
textureLoad( colorTex, coord ).w,
|
|
330
|
+
sqrt( max( filteredVariance, float( 0.0 ) ) )
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
textureStore( writeStorageTex, uvec2( uint( gx ), uint( gy ) ), vec4( output, carriedAlpha ) ).toWriteOnly();
|
|
297
334
|
|
|
298
335
|
} );
|
|
299
336
|
|
|
@@ -378,6 +415,7 @@ export class EdgeFilter extends RenderStage {
|
|
|
378
415
|
|
|
379
416
|
this.stepSizeU.value = 1 << i;
|
|
380
417
|
this.isLastIterationU.value = ( i === this.iterations - 1 ) ? 1 : 0;
|
|
418
|
+
this.isFirstIterationU.value = i === 0 ? 1 : 0;
|
|
381
419
|
this._readTexNode.value = readTex;
|
|
382
420
|
|
|
383
421
|
this.renderer.compute( writeNode );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, vec3, vec4, float, int, uint, uvec2, uniform,
|
|
1
|
+
import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uniform, min, storage, If,
|
|
2
2
|
textureStore, workgroupId, localId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, StorageTexture } from 'three/webgpu';
|
|
4
4
|
import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
|
|
@@ -6,6 +6,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
6
6
|
import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
7
7
|
import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
|
|
8
8
|
import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
9
|
+
import { cameraRayDirection } from '../TSL/CameraRay.js';
|
|
9
10
|
import { getMaterial } from '../TSL/Common.js';
|
|
10
11
|
import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
11
12
|
|
|
@@ -48,6 +49,12 @@ export class NormalDepth extends RenderStage {
|
|
|
48
49
|
|
|
49
50
|
this.cameraWorldMatrix = uniform( new Matrix4(), 'mat4' );
|
|
50
51
|
this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
|
|
52
|
+
// Mirrored from the path tracer each frame — these rays must use the same camera model
|
|
53
|
+
// as the colour buffer or the denoiser's normal/depth edge-stops fight the image.
|
|
54
|
+
this.cameraProjection = uniform( 0, 'int' );
|
|
55
|
+
this.panoLonRange = uniform( new Vector2(), 'vec2' );
|
|
56
|
+
this.panoLatRange = uniform( new Vector2(), 'vec2' );
|
|
57
|
+
this.panoLevelHorizon = uniform( 1, 'int' );
|
|
51
58
|
this.resolutionWidth = uniform( options.width || 1 );
|
|
52
59
|
this.resolutionHeight = uniform( options.height || 1 );
|
|
53
60
|
|
|
@@ -122,8 +129,11 @@ export class NormalDepth extends RenderStage {
|
|
|
122
129
|
|
|
123
130
|
this.on( 'pipeline:reset', () => {
|
|
124
131
|
|
|
132
|
+
// _hasHistory is deliberately NOT cleared, mirroring MotionVector.reset(): the
|
|
133
|
+
// prev-frame G-buffer tracks camera motion, not accumulation, and a camera move
|
|
134
|
+
// resets accumulation every frame of a drag. Clearing it made render() overwrite
|
|
135
|
+
// prevRT with the current frame, so ASVGF's temporal gate could never reject.
|
|
125
136
|
this._dirty = true;
|
|
126
|
-
this._hasHistory = false;
|
|
127
137
|
|
|
128
138
|
} );
|
|
129
139
|
|
|
@@ -149,6 +159,8 @@ export class NormalDepth extends RenderStage {
|
|
|
149
159
|
this._bvhStorageNode = null;
|
|
150
160
|
this._matStorageNode = null;
|
|
151
161
|
this._dirty = true;
|
|
162
|
+
// New geometry — the previous frame describes a scene that is no longer there.
|
|
163
|
+
this._hasHistory = false;
|
|
152
164
|
|
|
153
165
|
}
|
|
154
166
|
|
|
@@ -213,20 +225,12 @@ export class NormalDepth extends RenderStage {
|
|
|
213
225
|
|
|
214
226
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
215
227
|
|
|
216
|
-
// Pixel center → NDC, Y negated for Three.js WebGPU.
|
|
217
|
-
const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
|
|
218
|
-
const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
|
|
219
|
-
const ndcPos = vec3( ndcX, ndcY, 1.0 );
|
|
220
|
-
|
|
221
228
|
// No jitter — deterministic per-pixel ray so the temporal gate
|
|
222
229
|
// sees stable per-pixel normals across frames.
|
|
223
|
-
const
|
|
224
|
-
const rayDirWorld =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
camWorldMat[ 1 ].xyz,
|
|
228
|
-
camWorldMat[ 2 ].xyz
|
|
229
|
-
).mul( rayDirCS.xyz.div( rayDirCS.w ) )
|
|
230
|
+
const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
|
|
231
|
+
const rayDirWorld = cameraRayDirection(
|
|
232
|
+
uv, camWorldMat, camProjInvMat,
|
|
233
|
+
this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
|
|
230
234
|
);
|
|
231
235
|
const rayOrigin = vec3( camWorldMat[ 3 ] );
|
|
232
236
|
|
|
@@ -302,6 +306,10 @@ export class NormalDepth extends RenderStage {
|
|
|
302
306
|
|
|
303
307
|
this.cameraWorldMatrix.value.copy( pt.uniforms.get( 'cameraWorldMatrix' ).value );
|
|
304
308
|
this.cameraProjectionMatrixInverse.value.copy( pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value );
|
|
309
|
+
this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
|
|
310
|
+
this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
|
|
311
|
+
this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
|
|
312
|
+
this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
|
|
305
313
|
|
|
306
314
|
}
|
|
307
315
|
|
|
@@ -367,13 +375,16 @@ export class NormalDepth extends RenderStage {
|
|
|
367
375
|
this._outputStorageTex?.dispose();
|
|
368
376
|
this._shadingStorageTex?.dispose();
|
|
369
377
|
this.reset();
|
|
378
|
+
// The textures backing the ping-pong are gone, so the prev-frame G-buffer is too.
|
|
379
|
+
this._hasHistory = false;
|
|
370
380
|
|
|
371
381
|
}
|
|
372
382
|
|
|
383
|
+
// Accumulation reset only — _hasHistory survives on purpose (see the pipeline:reset
|
|
384
|
+
// listener). It is invalidated by setSize(), a geometry swap, and releaseGPUMemory().
|
|
373
385
|
reset() {
|
|
374
386
|
|
|
375
387
|
this._dirty = true;
|
|
376
|
-
this._hasHistory = false;
|
|
377
388
|
|
|
378
389
|
}
|
|
379
390
|
|