rayzee 7.1.0 → 7.2.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 +1 -1
- package/dist/rayzee.es.js +1156 -1137
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +45 -45
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +13 -16
- package/src/PathTracerApp.js +10 -3
- package/src/Stages/ASVGF.js +40 -14
- package/src/Stages/BilateralFilter.js +23 -4
- package/src/Stages/NormalDepth.js +101 -7
- package/src/Stages/Variance.js +7 -3
- package/src/managers/CameraManager.js +19 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -161,11 +161,15 @@ export const MAX_STORAGE_TEXTURE_SIZE = 2048;
|
|
|
161
161
|
|
|
162
162
|
export const ASVGF_QUALITY_PRESETS = {
|
|
163
163
|
// phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
|
|
164
|
-
// permissive.
|
|
165
|
-
//
|
|
164
|
+
// permissive. The adaptive temporal gradient (gradientStrength > 0) is always
|
|
165
|
+
// on: it measures real change in units of noise σ (gradientSigmaScale), so a
|
|
166
|
+
// static scene reads ~0 (no convergence penalty) and only moving lights / anim
|
|
167
|
+
// / disocclusion drop history. See ASVGF._buildGradientCompute.
|
|
166
168
|
low: {
|
|
167
169
|
temporalAlpha: 0.1,
|
|
168
|
-
gradientStrength: 0.
|
|
170
|
+
gradientStrength: 0.8,
|
|
171
|
+
gradientSigmaScale: 2.5,
|
|
172
|
+
gradientNoiseFloor: 0.05,
|
|
169
173
|
atrousIterations: 3,
|
|
170
174
|
phiColor: 1.0,
|
|
171
175
|
phiNormal: 64.0,
|
|
@@ -176,7 +180,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
176
180
|
},
|
|
177
181
|
medium: {
|
|
178
182
|
temporalAlpha: 0.03,
|
|
179
|
-
gradientStrength:
|
|
183
|
+
gradientStrength: 1.0,
|
|
184
|
+
gradientSigmaScale: 2.5,
|
|
185
|
+
gradientNoiseFloor: 0.05,
|
|
180
186
|
atrousIterations: 4,
|
|
181
187
|
phiColor: 0.5,
|
|
182
188
|
phiNormal: 128.0,
|
|
@@ -187,7 +193,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
187
193
|
},
|
|
188
194
|
high: {
|
|
189
195
|
temporalAlpha: 0.0,
|
|
190
|
-
gradientStrength:
|
|
196
|
+
gradientStrength: 1.0,
|
|
197
|
+
gradientSigmaScale: 2.5,
|
|
198
|
+
gradientNoiseFloor: 0.05,
|
|
191
199
|
atrousIterations: 6,
|
|
192
200
|
phiColor: 0.3,
|
|
193
201
|
phiNormal: 256.0,
|
|
@@ -198,17 +206,6 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
198
206
|
}
|
|
199
207
|
};
|
|
200
208
|
|
|
201
|
-
// Adaptive variants — same SVGF quality as the base preset plus the temporal-
|
|
202
|
-
// gradient anti-lag enabled (gradientStrength > 0). The gradient measures real
|
|
203
|
-
// change in units of noise σ (gradientSigmaScale), so a static scene reads ~0
|
|
204
|
-
// and convergence is unaffected; only moving lights/anim/disocclusion drop
|
|
205
|
-
// history. Mutating the exported object is fine (const binding, mutable object);
|
|
206
|
-
// spreading lets each inherit its base. See ASVGF._buildGradientCompute.
|
|
207
|
-
const ADAPTIVE_GRADIENT = { gradientSigmaScale: 2.5, gradientNoiseFloor: 0.05 };
|
|
208
|
-
ASVGF_QUALITY_PRESETS.low_adaptive = { ...ASVGF_QUALITY_PRESETS.low, gradientStrength: 0.8, ...ADAPTIVE_GRADIENT };
|
|
209
|
-
ASVGF_QUALITY_PRESETS.medium_adaptive = { ...ASVGF_QUALITY_PRESETS.medium, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
|
|
210
|
-
ASVGF_QUALITY_PRESETS.high_adaptive = { ...ASVGF_QUALITY_PRESETS.high, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
|
|
211
|
-
|
|
212
209
|
export const CAMERA_RANGES = {
|
|
213
210
|
fov: {
|
|
214
211
|
min: 10,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -310,12 +310,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
this._ensureVRAMWiring();
|
|
313
|
-
|
|
313
|
+
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
314
|
+
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
315
|
+
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
316
|
+
// textures each frame is wasted. Measure at burst start (catches any
|
|
317
|
+
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
318
|
+
const tracker = this.stages.pathTracer?.vramTracker;
|
|
319
|
+
const frame = this.stages.pathTracer?.frameCount ?? 0;
|
|
320
|
+
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
314
321
|
updateStats( {
|
|
315
322
|
timeElapsed: this.completion.timeElapsed,
|
|
316
323
|
samples: getDisplaySamples( this.stages.pathTracer ),
|
|
317
|
-
memoryUsed:
|
|
318
|
-
memoryPeak:
|
|
324
|
+
memoryUsed: tracker?.current ?? 0,
|
|
325
|
+
memoryPeak: tracker?.peak ?? 0,
|
|
319
326
|
} );
|
|
320
327
|
|
|
321
328
|
// Check time limit
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
-
If, dot, max, min, abs, mix, pow, exp, sqrt,
|
|
2
|
+
If, dot, max, min, abs, mix, pow, exp, sqrt, select,
|
|
3
3
|
textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
5
|
import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -7,6 +7,11 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
7
7
|
import { luminance } from '../TSL/Common.js';
|
|
8
8
|
import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
9
9
|
|
|
10
|
+
// Replace NaN/±Inf with a bounded value so one firefly can't permanently poison the
|
|
11
|
+
// temporal EMA (mix() propagates NaN forever). Per-channel: NaN (x!=x) → 0, ±Inf → [0,1e7].
|
|
12
|
+
const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
13
|
+
const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* ASVGF — SVGF temporal denoising with albedo demodulation + adaptive-α.
|
|
12
17
|
*
|
|
@@ -15,8 +20,8 @@ import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
|
15
20
|
* floored by a per-pixel σ (max(Δ − k·σ, 0)), max-normalised and squared
|
|
16
21
|
* (Q2RTX get_gradient). gradientStrength scales it into effectiveAlpha =
|
|
17
22
|
* mix(baseAlpha, 1, gradient·gradientStrength) — high change ⇒ drop history
|
|
18
|
-
* (anti-lag).
|
|
19
|
-
*
|
|
23
|
+
* (anti-lag). All quality presets enable it (gradientStrength > 0); a static
|
|
24
|
+
* scene reads gradient ≈ 0, so convergence is unaffected.
|
|
20
25
|
*
|
|
21
26
|
* Reads: pathtracer:color, pathtracer:albedo, pathtracer:normalDepth,
|
|
22
27
|
* pathtracer:prevNormalDepth, motionVector:screenSpace
|
|
@@ -46,6 +51,9 @@ export class ASVGF extends RenderStage {
|
|
|
46
51
|
this.resH = uniform( options.height || 1 );
|
|
47
52
|
|
|
48
53
|
this.temporalEnabledU = uniform( 1.0 );
|
|
54
|
+
// 1.0 for one frame after asvgf:reset → forces a fresh sample so stale pre-reset
|
|
55
|
+
// (wrong-scene) history isn't blended in (mirrors Variance's _needsWarmReset).
|
|
56
|
+
this.forceResetU = uniform( 0.0 );
|
|
49
57
|
|
|
50
58
|
this._colorTexNode = new TextureNode();
|
|
51
59
|
this._albedoTexNode = new TextureNode();
|
|
@@ -118,6 +126,7 @@ export class ASVGF extends RenderStage {
|
|
|
118
126
|
|
|
119
127
|
this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
|
|
120
128
|
this._compiled = false;
|
|
129
|
+
this._needsWarmReset = false;
|
|
121
130
|
|
|
122
131
|
this._dispatchX = Math.ceil( w / 8 );
|
|
123
132
|
this._dispatchY = Math.ceil( h / 8 );
|
|
@@ -207,7 +216,7 @@ export class ASVGF extends RenderStage {
|
|
|
207
216
|
// safeAlbedo = max(albedo, ALBEDO_EPS) keeps sky/dark-material round-trip).
|
|
208
217
|
const curColor = textureLoad( colorTex, ivec2( gxL, gyL ) ).xyz;
|
|
209
218
|
const curAlbedo = textureLoad( albedoTex, ivec2( gxL, gyL ) ).xyz;
|
|
210
|
-
const curLighting = curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) );
|
|
219
|
+
const curLighting = sanitizeRGB( curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) ) );
|
|
211
220
|
const curLum = luminance( curLighting ).toVar();
|
|
212
221
|
sharedCurLum.element( k ).assign( curLum );
|
|
213
222
|
|
|
@@ -217,8 +226,10 @@ export class ASVGF extends RenderStage {
|
|
|
217
226
|
const motion = textureLoad( motionTex, ivec2( gxL, gyL ) );
|
|
218
227
|
const prevXf = float( gxL ).sub( motion.x.mul( resW ) );
|
|
219
228
|
const prevYf = float( gyL ).sub( motion.y.mul( resH ) );
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
// Round-to-nearest (not truncate) — the gradient does a nearest-neighbour
|
|
230
|
+
// history lookup; +0.5 removes the half-pixel floor bias vs the temporal tap.
|
|
231
|
+
const prevX = int( prevXf.add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
232
|
+
const prevY = int( prevYf.add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
222
233
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
223
234
|
const histLighting = textureLoad( histTex, ivec2( prevX, prevY ) ).xyz;
|
|
224
235
|
const histLum = motionValid.select( luminance( histLighting ), curLum );
|
|
@@ -303,8 +314,8 @@ export class ASVGF extends RenderStage {
|
|
|
303
314
|
// Trust the gradient only once enough history has accumulated at the
|
|
304
315
|
// reprojected centre — early frames have noisy history → false fires.
|
|
305
316
|
const cMotion = textureLoad( motionTex, ivec2( gx, gy ) );
|
|
306
|
-
const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
307
|
-
const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
317
|
+
const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ).add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
318
|
+
const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ).add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
308
319
|
const histLen = cMotion.w.greaterThan( 0.5 )
|
|
309
320
|
.select( textureLoad( histTex, ivec2( cPrevX, cPrevY ) ).w, float( 0.0 ) );
|
|
310
321
|
const confidence = histLen.div( 4.0 ).clamp( 0.0, 1.0 );
|
|
@@ -355,6 +366,7 @@ export class ASVGF extends RenderStage {
|
|
|
355
366
|
const temporalAlphaMin = this.temporalAlpha;
|
|
356
367
|
const gradientStrength = this.gradientStrength;
|
|
357
368
|
const temporalEnabledU = this.temporalEnabledU;
|
|
369
|
+
const forceResetU = this.forceResetU;
|
|
358
370
|
const resW = this.resW;
|
|
359
371
|
const resH = this.resH;
|
|
360
372
|
|
|
@@ -374,23 +386,28 @@ export class ASVGF extends RenderStage {
|
|
|
374
386
|
// Same safeAlbedo on both demod and re-mod sides → exact
|
|
375
387
|
// round-trip for sky/miss rays where albedo=0.
|
|
376
388
|
const safeAlbedo = max( currentAlbedo, vec3( ALBEDO_EPS ) );
|
|
377
|
-
const currentLighting = currentColor.div( safeAlbedo );
|
|
389
|
+
const currentLighting = sanitizeRGB( currentColor.div( safeAlbedo ) );
|
|
378
390
|
|
|
379
391
|
// Defaults = fresh sample (no temporal blend).
|
|
380
392
|
const demodResult = vec4( currentLighting, 1.0 ).toVar();
|
|
381
393
|
const modulatedResult = vec4( currentColor, 1.0 ).toVar();
|
|
382
394
|
|
|
383
|
-
|
|
395
|
+
// forceResetU skips the blend for one frame after asvgf:reset → re-anchors
|
|
396
|
+
// history to the current (post-reset) scene instead of the stale ping-pong.
|
|
397
|
+
If( temporalEnabledU.greaterThan( 0.5 ).and( forceResetU.lessThan( 0.5 ) ), () => {
|
|
384
398
|
|
|
385
399
|
const motion = textureLoad( motionTex, coord );
|
|
386
400
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
387
401
|
|
|
388
402
|
const prevXf = float( gx ).sub( motion.x.mul( resW ) );
|
|
389
403
|
const prevYf = float( gy ).sub( motion.y.mul( resH ) );
|
|
404
|
+
// Upper bound is the inclusive last pixel (< res, not < res-1): the 2×2 taps
|
|
405
|
+
// already clamp to [0,res-1] and wSum gates bad taps, so res-1 wrongly
|
|
406
|
+
// rejected the trailing column/row. Matches MotionVector's inclusive UV.
|
|
390
407
|
const prevOnScreen = prevXf.greaterThanEqual( 0.0 )
|
|
391
|
-
.and( prevXf.lessThan( float( resW )
|
|
408
|
+
.and( prevXf.lessThan( float( resW ) ) )
|
|
392
409
|
.and( prevYf.greaterThanEqual( 0.0 ) )
|
|
393
|
-
.and( prevYf.lessThan( float( resH )
|
|
410
|
+
.and( prevYf.lessThan( float( resH ) ) );
|
|
394
411
|
|
|
395
412
|
If( motionValid.and( prevOnScreen ), () => {
|
|
396
413
|
|
|
@@ -458,7 +475,7 @@ export class ASVGF extends RenderStage {
|
|
|
458
475
|
.add( p11.w.mul( v11 ) )
|
|
459
476
|
.mul( invWSum );
|
|
460
477
|
|
|
461
|
-
// adaptive α —
|
|
478
|
+
// adaptive α — gradient·gradientStrength boosts toward 1 on change.
|
|
462
479
|
const gradient = textureLoad( gradientTex, coord ).x;
|
|
463
480
|
const adaptiveBoost = gradient.mul( gradientStrength ).clamp( 0.0, 1.0 );
|
|
464
481
|
|
|
@@ -468,7 +485,9 @@ export class ASVGF extends RenderStage {
|
|
|
468
485
|
);
|
|
469
486
|
const effectiveAlpha = mix( baseAlpha, float( 1.0 ), adaptiveBoost );
|
|
470
487
|
|
|
471
|
-
|
|
488
|
+
// Sanitize the blend too: a NaN already baked into prevLighting
|
|
489
|
+
// would otherwise survive mix() and re-poison the write target.
|
|
490
|
+
const blendedLighting = sanitizeRGB( mix( prevLighting, currentLighting, effectiveAlpha ) );
|
|
472
491
|
const newHistory = min( prevHistory.add( 1.0 ), maxAccumFrames );
|
|
473
492
|
|
|
474
493
|
demodResult.assign( vec4( blendedLighting, newHistory ) );
|
|
@@ -701,7 +720,11 @@ export class ASVGF extends RenderStage {
|
|
|
701
720
|
|
|
702
721
|
}
|
|
703
722
|
|
|
723
|
+
// One-shot fresh re-anchor after asvgf:reset, threaded through the SINGLE
|
|
724
|
+
// writeNode dispatch (a dual-node warmup would alias the read target — see setSize).
|
|
725
|
+
this.forceResetU.value = this._needsWarmReset ? 1.0 : 0.0;
|
|
704
726
|
this.renderer.compute( writeNode );
|
|
727
|
+
this._needsWarmReset = false;
|
|
705
728
|
|
|
706
729
|
// Copy active region out of the over-allocated StorageTextures into
|
|
707
730
|
// right-sized RTs; downstream stages UV-sample these.
|
|
@@ -767,6 +790,9 @@ export class ASVGF extends RenderStage {
|
|
|
767
790
|
resetTemporalData() {
|
|
768
791
|
|
|
769
792
|
this.currentMoments = 0;
|
|
793
|
+
// Re-anchor history on the next frame — the ping-pong textures still hold
|
|
794
|
+
// pre-reset (wrong-scene) lighting + history count, which would otherwise blend in.
|
|
795
|
+
this._needsWarmReset = true;
|
|
770
796
|
|
|
771
797
|
}
|
|
772
798
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, sqrt,
|
|
1
|
+
import { Fn, wgslFn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform, If, max, mix, sqrt,
|
|
2
2
|
textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
4
4
|
import { HalfFloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -59,6 +59,9 @@ export class BilateralFilter extends RenderStage {
|
|
|
59
59
|
this.renderer = renderer;
|
|
60
60
|
this.inputTextureName = options.inputTextureName || 'asvgf:demodulated';
|
|
61
61
|
this.normalDepthTextureName = options.normalDepthTextureName || 'pathtracer:normalDepth';
|
|
62
|
+
// Mapped (normal/bump-perturbed) normal for the normal edge-stop — geometric
|
|
63
|
+
// normals are flat across a normal-mapped surface so normW can't preserve bump detail.
|
|
64
|
+
this.shadingNormalTextureName = options.shadingNormalTextureName || 'pathtracer:shadingNormal';
|
|
62
65
|
this.albedoTextureName = options.albedoTextureName || 'pathtracer:albedo';
|
|
63
66
|
this.varianceTextureName = options.varianceTextureName || 'variance:output';
|
|
64
67
|
this.iterations = options.iterations ?? 4;
|
|
@@ -69,6 +72,10 @@ export class BilateralFilter extends RenderStage {
|
|
|
69
72
|
this.phiNormal = uniform( options.phiNormal ?? 128.0 );
|
|
70
73
|
this.phiDepth = uniform( options.phiDepth ?? 0.05 );
|
|
71
74
|
this.phiLuminance = uniform( options.phiLuminance ?? 4.0 );
|
|
75
|
+
// Blend Variance's spatial-variance channel into sigma_l (1 = max(temporal, spatial),
|
|
76
|
+
// 0 = temporal-only). Widens the luminance gate where history is thin but the
|
|
77
|
+
// neighbourhood is noisy (disocclusion). Default on — validated −1.7% RMSE @4spp.
|
|
78
|
+
this.spatialVarianceWeight = uniform( options.spatialVarianceWeight ?? 1.0 );
|
|
72
79
|
this.stepSizeU = uniform( 1, 'int' );
|
|
73
80
|
// 1 on the final iteration → multiply by albedo to remodulate.
|
|
74
81
|
this.isLastIterationU = uniform( 0, 'int' );
|
|
@@ -77,6 +84,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
77
84
|
|
|
78
85
|
this._readTexNode = new TextureNode();
|
|
79
86
|
this._normalDepthTexNode = new TextureNode();
|
|
87
|
+
this._shadingNormalTexNode = new TextureNode();
|
|
80
88
|
this._albedoTexNode = new TextureNode();
|
|
81
89
|
this._varianceTexNode = new TextureNode();
|
|
82
90
|
|
|
@@ -133,12 +141,14 @@ export class BilateralFilter extends RenderStage {
|
|
|
133
141
|
|
|
134
142
|
const readTexNode = this._readTexNode;
|
|
135
143
|
const ndTexNode = this._normalDepthTexNode;
|
|
144
|
+
const snTexNode = this._shadingNormalTexNode;
|
|
136
145
|
const albedoTexNode = this._albedoTexNode;
|
|
137
146
|
const varTexNode = this._varianceTexNode;
|
|
138
147
|
const phiColor = this.phiColor;
|
|
139
148
|
const phiNormal = this.phiNormal;
|
|
140
149
|
const phiDepth = this.phiDepth;
|
|
141
150
|
const phiLuminance = this.phiLuminance;
|
|
151
|
+
const spatialVarianceWeight = this.spatialVarianceWeight;
|
|
142
152
|
const stepSize = this.stepSizeU;
|
|
143
153
|
const isLastIterationU = this.isLastIterationU;
|
|
144
154
|
const resW = this.resW;
|
|
@@ -165,7 +175,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
165
175
|
const coord = ivec2( gx, gy );
|
|
166
176
|
const centerColor = textureLoad( readTexNode, coord ).xyz;
|
|
167
177
|
const centerND = textureLoad( ndTexNode, coord );
|
|
168
|
-
|
|
178
|
+
// Normal edge-stop reads the mapped (shading) normal; depth gate stays geometric.
|
|
179
|
+
const centerNormal = textureLoad( snTexNode, coord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
169
180
|
const centerDepth = centerND.w;
|
|
170
181
|
const centerLum = luminance( centerColor );
|
|
171
182
|
const centerSafeAlbedo = max( textureLoad( albedoTexNode, coord ).xyz, vec3( ALBEDO_EPS ) );
|
|
@@ -176,7 +187,10 @@ export class BilateralFilter extends RenderStage {
|
|
|
176
187
|
// from demodulation — otherwise dark materials get an
|
|
177
188
|
// under-estimated sigma → over-strict luminance gate → no
|
|
178
189
|
// blending → silhouette dark-outline artifact.
|
|
179
|
-
|
|
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 );
|
|
180
194
|
const sigmaL = phiLuminance
|
|
181
195
|
.mul( sqrt( max( variance, float( 0.0 ) ) ) )
|
|
182
196
|
.div( centerAlbedoLum )
|
|
@@ -201,7 +215,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
201
215
|
|
|
202
216
|
const sColor = textureLoad( readTexNode, ivec2( sx, sy ) ).xyz;
|
|
203
217
|
const sND = textureLoad( ndTexNode, ivec2( sx, sy ) );
|
|
204
|
-
const sNormal =
|
|
218
|
+
const sNormal = textureLoad( snTexNode, ivec2( sx, sy ) ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
205
219
|
const sDepth = sND.w;
|
|
206
220
|
const sLum = luminance( sColor );
|
|
207
221
|
|
|
@@ -253,6 +267,8 @@ export class BilateralFilter extends RenderStage {
|
|
|
253
267
|
|| context.getTexture( 'asvgf:output' )
|
|
254
268
|
|| context.getTexture( 'pathtracer:color' );
|
|
255
269
|
const ndTex = context.getTexture( this.normalDepthTextureName );
|
|
270
|
+
// Fall back to geometric normalDepth if the mapped normal isn't published.
|
|
271
|
+
const snTex = context.getTexture( this.shadingNormalTextureName ) || ndTex;
|
|
256
272
|
const albedoTex = context.getTexture( this.albedoTextureName );
|
|
257
273
|
const varTex = context.getTexture( this.varianceTextureName );
|
|
258
274
|
|
|
@@ -273,6 +289,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
273
289
|
|
|
274
290
|
// RenderTarget textures — safe to bind before first-compile.
|
|
275
291
|
if ( ndTex ) this._normalDepthTexNode.value = ndTex;
|
|
292
|
+
if ( snTex ) this._shadingNormalTexNode.value = snTex;
|
|
276
293
|
if ( albedoTex ) this._albedoTexNode.value = albedoTex;
|
|
277
294
|
|
|
278
295
|
// First-frame compile while StorageTexture-typed nodes still hold
|
|
@@ -329,6 +346,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
329
346
|
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
330
347
|
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
331
348
|
if ( params.phiLuminance !== undefined ) this.phiLuminance.value = params.phiLuminance;
|
|
349
|
+
if ( params.spatialVarianceWeight !== undefined ) this.spatialVarianceWeight.value = params.spatialVarianceWeight;
|
|
332
350
|
if ( params.atrousIterations !== undefined ) this.iterations = params.atrousIterations;
|
|
333
351
|
|
|
334
352
|
}
|
|
@@ -364,6 +382,7 @@ export class BilateralFilter extends RenderStage {
|
|
|
364
382
|
this._outputTarget?.dispose();
|
|
365
383
|
this._readTexNode?.dispose();
|
|
366
384
|
this._normalDepthTexNode?.dispose();
|
|
385
|
+
this._shadingNormalTexNode?.dispose();
|
|
367
386
|
this._albedoTexNode?.dispose();
|
|
368
387
|
this._varianceTexNode?.dispose();
|
|
369
388
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Fn, vec3, vec4, float, int, uint, uvec2, uniform, normalize, mat3, storage, If,
|
|
2
|
-
textureStore, workgroupId, localId } from 'three/tsl';
|
|
2
|
+
texture, textureStore, workgroupId, localId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, StorageTexture } from 'three/webgpu';
|
|
4
|
-
import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
|
|
4
|
+
import { HalfFloatType, RGBAFormat, NearestFilter, LinearFilter, DataArrayTexture, Matrix4, Box2, Vector2 } from 'three';
|
|
5
5
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
6
6
|
import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
7
|
-
import { Ray, HitInfo } from '../TSL/Struct.js';
|
|
7
|
+
import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
|
|
8
8
|
import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
9
|
+
import { getMaterial } from '../TSL/Common.js';
|
|
10
|
+
import { computeUVCache, processNormal, processBump } from '../TSL/TextureSampling.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* NormalDepth — primary-ray G-buffer for SVGF gates.
|
|
@@ -21,7 +23,13 @@ import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
|
21
23
|
* aliases current — without that aliasing prev would point at older data
|
|
22
24
|
* while this frame's motion vector reflects zero motion → false rejection.
|
|
23
25
|
*
|
|
24
|
-
*
|
|
26
|
+
* Also emits a SHADING normal (geometric normal perturbed by the normal/bump
|
|
27
|
+
* map, recomputed from the SAME deterministic hit — no extra ray) so the
|
|
28
|
+
* spatial denoiser's edge-stop can see normal-map detail the flat geometric
|
|
29
|
+
* normal hides. Deterministic ⇒ jitter-free, so it's safe for the gates.
|
|
30
|
+
*
|
|
31
|
+
* Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth,
|
|
32
|
+
* pathtracer:shadingNormal
|
|
25
33
|
*/
|
|
26
34
|
export class NormalDepth extends RenderStage {
|
|
27
35
|
|
|
@@ -52,6 +60,22 @@ export class NormalDepth extends RenderStage {
|
|
|
52
60
|
this._outputStorageTex.minFilter = NearestFilter;
|
|
53
61
|
this._outputStorageTex.magFilter = NearestFilter;
|
|
54
62
|
|
|
63
|
+
// Shading-normal output (geometric normal perturbed by normal/bump map).
|
|
64
|
+
// Single buffer — only the spatial filter (current frame) consumes it.
|
|
65
|
+
this._shadingStorageTex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
|
|
66
|
+
this._shadingStorageTex.type = HalfFloatType;
|
|
67
|
+
this._shadingStorageTex.format = RGBAFormat;
|
|
68
|
+
this._shadingStorageTex.minFilter = NearestFilter;
|
|
69
|
+
this._shadingStorageTex.magFilter = NearestFilter;
|
|
70
|
+
this._shadingRT = new RenderTarget( w, h, {
|
|
71
|
+
type: HalfFloatType,
|
|
72
|
+
format: RGBAFormat,
|
|
73
|
+
minFilter: NearestFilter,
|
|
74
|
+
magFilter: NearestFilter,
|
|
75
|
+
depthBuffer: false,
|
|
76
|
+
stencilBuffer: false
|
|
77
|
+
} );
|
|
78
|
+
|
|
55
79
|
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
56
80
|
|
|
57
81
|
// Ping-pong RTs share format with the StorageTexture so copyTextureToTexture works.
|
|
@@ -73,11 +97,30 @@ export class NormalDepth extends RenderStage {
|
|
|
73
97
|
|
|
74
98
|
this._triStorageNode = null;
|
|
75
99
|
this._bvhStorageNode = null;
|
|
100
|
+
this._matStorageNode = null;
|
|
76
101
|
this._lastTriAttr = null;
|
|
77
102
|
this._lastBvhAttr = null;
|
|
103
|
+
this._lastMatAttr = null;
|
|
78
104
|
this._computeNode = null;
|
|
79
105
|
this._computeBuilt = false;
|
|
80
106
|
|
|
107
|
+
// Normal/bump map array nodes — persistent placeholders, value swapped to
|
|
108
|
+
// the real DataArrayTextures on model load. processNormal/processBump
|
|
109
|
+
// runtime-guard on map indices, so the placeholder is never sampled.
|
|
110
|
+
this._normalMapsTex = texture( this._makePlaceholderArray() );
|
|
111
|
+
this._bumpMapsTex = texture( this._makePlaceholderArray() );
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_makePlaceholderArray() {
|
|
116
|
+
|
|
117
|
+
const t = new DataArrayTexture( new Uint8Array( [ 128, 128, 255, 255 ] ), 1, 1, 1 );
|
|
118
|
+
t.minFilter = LinearFilter;
|
|
119
|
+
t.magFilter = LinearFilter;
|
|
120
|
+
t.generateMipmaps = false;
|
|
121
|
+
t.needsUpdate = true;
|
|
122
|
+
return t;
|
|
123
|
+
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
setupEventListeners() {
|
|
@@ -102,10 +145,12 @@ export class NormalDepth extends RenderStage {
|
|
|
102
145
|
const pt = this.pathTracer;
|
|
103
146
|
if ( ! pt ) return false;
|
|
104
147
|
|
|
148
|
+
const matAttr = pt.materialData?.materialStorageAttr;
|
|
105
149
|
const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
|
|
106
150
|
const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
|
|
151
|
+
const matSwapped = matAttr && matAttr !== this._lastMatAttr;
|
|
107
152
|
|
|
108
|
-
if ( triSwapped || bvhSwapped ) {
|
|
153
|
+
if ( triSwapped || bvhSwapped || matSwapped ) {
|
|
109
154
|
|
|
110
155
|
// Buffer identity changed → compute's bind group is stale; rebuild.
|
|
111
156
|
this._computeNode?.dispose?.();
|
|
@@ -113,6 +158,7 @@ export class NormalDepth extends RenderStage {
|
|
|
113
158
|
this._computeBuilt = false;
|
|
114
159
|
this._triStorageNode = null;
|
|
115
160
|
this._bvhStorageNode = null;
|
|
161
|
+
this._matStorageNode = null;
|
|
116
162
|
this._dirty = true;
|
|
117
163
|
|
|
118
164
|
}
|
|
@@ -133,10 +179,22 @@ export class NormalDepth extends RenderStage {
|
|
|
133
179
|
|
|
134
180
|
}
|
|
135
181
|
|
|
182
|
+
if ( matAttr && ! this._matStorageNode ) {
|
|
183
|
+
|
|
184
|
+
this._matStorageNode = storage( matAttr, 'vec4', matAttr.count ).toReadOnly();
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// In-place map swaps (model change) — graph closes over the node, only .value changes.
|
|
189
|
+
const md = pt.materialData;
|
|
190
|
+
if ( md?.normalMaps ) this._normalMapsTex.value = md.normalMaps;
|
|
191
|
+
if ( md?.bumpMaps ) this._bumpMapsTex.value = md.bumpMaps;
|
|
192
|
+
|
|
136
193
|
this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
|
|
137
194
|
this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
|
|
195
|
+
this._lastMatAttr = matAttr || this._lastMatAttr;
|
|
138
196
|
|
|
139
|
-
return !! ( this._triStorageNode && this._bvhStorageNode );
|
|
197
|
+
return !! ( this._triStorageNode && this._bvhStorageNode && this._matStorageNode );
|
|
140
198
|
|
|
141
199
|
}
|
|
142
200
|
|
|
@@ -144,11 +202,15 @@ export class NormalDepth extends RenderStage {
|
|
|
144
202
|
|
|
145
203
|
const triStorage = this._triStorageNode;
|
|
146
204
|
const bvhStorage = this._bvhStorageNode;
|
|
205
|
+
const matStorage = this._matStorageNode;
|
|
206
|
+
const normalMaps = this._normalMapsTex;
|
|
207
|
+
const bumpMaps = this._bumpMapsTex;
|
|
147
208
|
const camWorld = this.cameraWorldMatrix;
|
|
148
209
|
const camProjInv = this.cameraProjectionMatrixInverse;
|
|
149
210
|
const resW = this.resolutionWidth;
|
|
150
211
|
const resH = this.resolutionHeight;
|
|
151
212
|
const outputTex = this._outputStorageTex;
|
|
213
|
+
const shadingTex = this._shadingStorageTex;
|
|
152
214
|
|
|
153
215
|
const WG_SIZE = 8;
|
|
154
216
|
|
|
@@ -195,6 +257,31 @@ export class NormalDepth extends RenderStage {
|
|
|
195
257
|
result
|
|
196
258
|
).toWriteOnly();
|
|
197
259
|
|
|
260
|
+
// Shading normal: perturb the geometric normal by the normal/bump map
|
|
261
|
+
// from the SAME hit (deterministic UV → jitter-free). Miss → geo default.
|
|
262
|
+
const shadingNormal = hit.normal.toVar();
|
|
263
|
+
If( hit.didHit, () => {
|
|
264
|
+
|
|
265
|
+
const material = RayTracingMaterial.wrap(
|
|
266
|
+
getMaterial( hit.materialIndex, matStorage )
|
|
267
|
+
).toVar();
|
|
268
|
+
const uvCache = UVCache.wrap( computeUVCache( hit.uv, material ) ).toVar();
|
|
269
|
+
const mapped = processNormal( normalMaps, hit.normal, material, uvCache ).toVar();
|
|
270
|
+
shadingNormal.assign( processBump( bumpMaps, mapped, material, uvCache ) );
|
|
271
|
+
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
const shadingResult = hit.didHit.select(
|
|
275
|
+
vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), depth ),
|
|
276
|
+
vec4( 0.0, 0.0, 0.0, float( 1e6 ) )
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
textureStore(
|
|
280
|
+
shadingTex,
|
|
281
|
+
uvec2( uint( gx ), uint( gy ) ),
|
|
282
|
+
shadingResult
|
|
283
|
+
).toWriteOnly();
|
|
284
|
+
|
|
198
285
|
} );
|
|
199
286
|
|
|
200
287
|
} );
|
|
@@ -233,6 +320,7 @@ export class NormalDepth extends RenderStage {
|
|
|
233
320
|
const currentRT = this._currentIdx === 0 ? this._rtA : this._rtB;
|
|
234
321
|
context.setTexture( 'pathtracer:normalDepth', currentRT.texture );
|
|
235
322
|
context.setTexture( 'pathtracer:prevNormalDepth', currentRT.texture );
|
|
323
|
+
context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
|
|
236
324
|
return;
|
|
237
325
|
|
|
238
326
|
}
|
|
@@ -257,9 +345,10 @@ export class NormalDepth extends RenderStage {
|
|
|
257
345
|
|
|
258
346
|
this.renderer.compute( this._computeNode );
|
|
259
347
|
|
|
260
|
-
// Copy only the active region out of the over-allocated
|
|
348
|
+
// Copy only the active region out of the over-allocated StorageTextures.
|
|
261
349
|
this._srcRegion.max.set( writeRT.width, writeRT.height );
|
|
262
350
|
this.renderer.copyTextureToTexture( this._outputStorageTex, writeRT.texture, this._srcRegion );
|
|
351
|
+
this.renderer.copyTextureToTexture( this._shadingStorageTex, this._shadingRT.texture, this._srcRegion );
|
|
263
352
|
|
|
264
353
|
// First dispatch: seed prev from current so ASVGF doesn't see false
|
|
265
354
|
// disocclusion on frame 1.
|
|
@@ -272,6 +361,7 @@ export class NormalDepth extends RenderStage {
|
|
|
272
361
|
|
|
273
362
|
context.setTexture( 'pathtracer:normalDepth', writeRT.texture );
|
|
274
363
|
context.setTexture( 'pathtracer:prevNormalDepth', prevRT.texture );
|
|
364
|
+
context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
|
|
275
365
|
|
|
276
366
|
this._dirty = false;
|
|
277
367
|
|
|
@@ -294,6 +384,8 @@ export class NormalDepth extends RenderStage {
|
|
|
294
384
|
this._rtA.texture.needsUpdate = true;
|
|
295
385
|
this._rtB.setSize( width, height );
|
|
296
386
|
this._rtB.texture.needsUpdate = true;
|
|
387
|
+
this._shadingRT.setSize( width, height );
|
|
388
|
+
this._shadingRT.texture.needsUpdate = true;
|
|
297
389
|
this._hasHistory = false;
|
|
298
390
|
this.resolutionWidth.value = width;
|
|
299
391
|
this.resolutionHeight.value = height;
|
|
@@ -314,6 +406,8 @@ export class NormalDepth extends RenderStage {
|
|
|
314
406
|
|
|
315
407
|
this._computeNode?.dispose();
|
|
316
408
|
this._outputStorageTex?.dispose();
|
|
409
|
+
this._shadingStorageTex?.dispose();
|
|
410
|
+
this._shadingRT?.dispose();
|
|
317
411
|
this._rtA?.dispose();
|
|
318
412
|
this._rtB?.dispose();
|
|
319
413
|
|
package/src/Stages/Variance.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, int, uint, ivec2, uvec2, uniform, If, max,
|
|
1
|
+
import { Fn, wgslFn, float, int, uint, ivec2, uvec2, uniform, If, max, select,
|
|
2
2
|
textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
4
4
|
import { FloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -6,6 +6,10 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
6
6
|
import { luminance } from '../TSL/Common.js';
|
|
7
7
|
import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
8
8
|
|
|
9
|
+
// NaN/±Inf guard: a poisoned luminance would otherwise corrupt the moment EMA forever
|
|
10
|
+
// (mean/meanSq → variance → BilateralFilter sigmaL). NaN (x!=x) → 0, ±Inf → [0,1e7].
|
|
11
|
+
const sanitizeLum = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
12
|
+
|
|
9
13
|
// ── wgslFn helpers ──────────────────────────────────────────
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -203,7 +207,7 @@ export class Variance extends RenderStage {
|
|
|
203
207
|
const gy1 = tileOriginY.add( int( sy1 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
204
208
|
|
|
205
209
|
const sColor1 = textureLoad( colorTex, ivec2( gx1, gy1 ) ).xyz;
|
|
206
|
-
sharedLum.element( linearIdx ).assign( luminance( sColor1 ) );
|
|
210
|
+
sharedLum.element( linearIdx ).assign( sanitizeLum( luminance( sColor1 ) ) );
|
|
207
211
|
|
|
208
212
|
// Load #2: threads 0-35 load positions 64-99
|
|
209
213
|
If( linearIdx.lessThan( uint( EXTRA_LOAD ) ), () => {
|
|
@@ -215,7 +219,7 @@ export class Variance extends RenderStage {
|
|
|
215
219
|
const gy2 = tileOriginY.add( int( sy2 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
216
220
|
|
|
217
221
|
const sColor2 = textureLoad( colorTex, ivec2( gx2, gy2 ) ).xyz;
|
|
218
|
-
sharedLum.element( idx2 ).assign( luminance( sColor2 ) );
|
|
222
|
+
sharedLum.element( idx2 ).assign( sanitizeLum( luminance( sColor2 ) ) );
|
|
219
223
|
|
|
220
224
|
} );
|
|
221
225
|
|
|
@@ -42,6 +42,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
42
42
|
this._lastValidFocusDistance = null;
|
|
43
43
|
this._smoothedFocusDistance = null;
|
|
44
44
|
this._afPointDirty = false;
|
|
45
|
+
this._afSuspended = false;
|
|
45
46
|
|
|
46
47
|
// Saved state for default camera when switching to model cameras
|
|
47
48
|
this._defaultCameraState = null;
|
|
@@ -273,6 +274,24 @@ export class CameraManager extends EventDispatcher {
|
|
|
273
274
|
|
|
274
275
|
if ( this.autoFocusMode === 'manual' ) return;
|
|
275
276
|
|
|
277
|
+
// Depth-of-field is the only consumer of the auto-focus distance. With DOF
|
|
278
|
+
// off (the default) the per-frame scene raycast is pure waste, so skip it.
|
|
279
|
+
// Re-snap on the frame DOF turns back on so focus is correct immediately
|
|
280
|
+
// rather than racking from a stale smoothed value.
|
|
281
|
+
if ( ! pathTracer?.enableDOF?.value ) {
|
|
282
|
+
|
|
283
|
+
this._afSuspended = true;
|
|
284
|
+
return;
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if ( this._afSuspended ) {
|
|
289
|
+
|
|
290
|
+
this._afSuspended = false;
|
|
291
|
+
this._smoothedFocusDistance = null;
|
|
292
|
+
|
|
293
|
+
}
|
|
294
|
+
|
|
276
295
|
// Lock focus during active tiled final rendering
|
|
277
296
|
const stage = pathTracer;
|
|
278
297
|
if ( stage?.isReady
|