rayzee 7.10.2 → 7.11.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 +2367 -2048
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +69 -54
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +23 -8
- package/src/Processor/AssetLoader.js +11 -0
- package/src/Processor/GeometryExtractor.js +45 -13
- package/src/Processor/SceneProcessor.js +51 -0
- package/src/Processor/TextureCreator.js +21 -10
- package/src/Stages/EdgeFilter.js +284 -123
- package/src/TSL/Clearcoat.js +34 -10
- package/src/TSL/Common.js +80 -0
- package/src/TSL/EmissiveSampling.js +2 -2
- package/src/TSL/LightsDirect.js +3 -2
- package/src/TSL/LightsIndirect.js +4 -7
- package/src/TSL/LightsSampling.js +17 -6
- package/src/TSL/MaterialEvaluation.js +37 -9
- package/src/TSL/MaterialProperties.js +78 -2
- package/src/TSL/MaterialSampling.js +19 -0
- package/src/TSL/MaterialTransmission.js +3 -1
- package/src/TSL/PathTracerCore.js +35 -10
- package/src/TSL/ShadeKernel.js +31 -1
- package/src/TSL/Struct.js +39 -0
- package/src/TSL/TextureSampling.js +103 -1
- package/src/managers/DenoisingManager.js +3 -0
- package/src/managers/MaterialDataManager.js +36 -3
package/src/Stages/EdgeFilter.js
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
|
-
import { Fn, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
-
If, dot, max,
|
|
1
|
+
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
+
If, dot, max, mix, pow, sqrt, select,
|
|
3
3
|
textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
|
-
import {
|
|
5
|
+
import { FloatType, RGBAFormat, LinearFilter, Box2, Vector2 } from 'three';
|
|
6
6
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
|
-
import {
|
|
8
|
-
import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
7
|
+
import { luminance } from '../TSL/Common.js';
|
|
8
|
+
import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
9
|
+
|
|
10
|
+
// NormalDepth writes depth ≈ 65504 for miss (env/background) rays. This sits below
|
|
11
|
+
// the sentinel and above any real ray distance, so it's a clean hit/miss flag.
|
|
12
|
+
const MISS_THRESHOLD = 6e4;
|
|
13
|
+
|
|
14
|
+
// NaN/±Inf guard on demodulated lighting: dividing color by a near-zero albedo can
|
|
15
|
+
// blow up, and one poisoned tap would smear through the à-trous passes. Per-channel:
|
|
16
|
+
// NaN (x≠x) → 0, ±Inf clamped to a bounded HDR range. Ceiling matches ASVGF/Variance
|
|
17
|
+
// (1e7); the FloatType ping-pong textures hold it without HalfFloat's 65k clip.
|
|
18
|
+
const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
19
|
+
const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
|
|
20
|
+
|
|
21
|
+
// 5×5 à-trous kernel weights (B3-spline / Gaussian approx, Σ = 1.0).
|
|
22
|
+
const ATROUS_KERNEL = [
|
|
23
|
+
1.0 / 256.0, 4.0 / 256.0, 6.0 / 256.0, 4.0 / 256.0, 1.0 / 256.0,
|
|
24
|
+
4.0 / 256.0, 16.0 / 256.0, 24.0 / 256.0, 16.0 / 256.0, 4.0 / 256.0,
|
|
25
|
+
6.0 / 256.0, 24.0 / 256.0, 36.0 / 256.0, 24.0 / 256.0, 6.0 / 256.0,
|
|
26
|
+
4.0 / 256.0, 16.0 / 256.0, 24.0 / 256.0, 16.0 / 256.0, 4.0 / 256.0,
|
|
27
|
+
1.0 / 256.0, 4.0 / 256.0, 6.0 / 256.0, 4.0 / 256.0, 1.0 / 256.0,
|
|
28
|
+
];
|
|
9
29
|
|
|
10
30
|
/**
|
|
11
31
|
* WebGPU Edge-Aware Filtering Stage (Compute Shader).
|
|
12
32
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
33
|
+
* A spatial-only SVGF à-trous denoiser (Dammertz 2010 + Schied 2017 edge-stops)
|
|
34
|
+
* that runs directly on the progressively-accumulated frame — no temporal
|
|
35
|
+
* reprojection, motion vectors, or history buffers, so it is free of the
|
|
36
|
+
* ghosting/lag artefacts of the ASVGF chain. Intended for static-camera
|
|
37
|
+
* progressive rendering where a light, artefact-free spatial filter is wanted.
|
|
18
38
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
39
|
+
* Pipeline per frame:
|
|
40
|
+
* 1. Demodulate: lighting = color / albedo (miss rays use a neutral albedo so
|
|
41
|
+
* bright HDR sky neither overflows the divide nor gets zeroed on remodulate).
|
|
42
|
+
* 2. À-trous: `iterations` ping-pong passes with step size 2^i over a dense
|
|
43
|
+
* 5×5 Gaussian kernel, growing the effective footprint to tens of pixels.
|
|
44
|
+
* Edge-stops: variance-guided luminance σ (from variance:output), a
|
|
45
|
+
* shading-normal cone, and a scale-invariant relative depth gate.
|
|
46
|
+
* 3. Remodulate + strength blend on the final pass.
|
|
47
|
+
*
|
|
48
|
+
* Reads: pathtracer:color, pathtracer:normalDepth, pathtracer:shadingNormal,
|
|
49
|
+
* pathtracer:albedo, variance:output
|
|
21
50
|
* Publishes: edgeFiltering:output
|
|
22
51
|
* Mode: PER_CYCLE
|
|
23
52
|
*/
|
|
@@ -32,45 +61,58 @@ export class EdgeFilter extends RenderStage {
|
|
|
32
61
|
|
|
33
62
|
this.renderer = renderer;
|
|
34
63
|
|
|
35
|
-
// filterStrength: 0 =
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this.phiNormal = uniform( options.phiNormal ??
|
|
42
|
-
this.phiDepth = uniform( options.phiDepth ?? 1
|
|
43
|
-
this.
|
|
64
|
+
// filterStrength: final blend, 0 = raw color, 1 = fully filtered.
|
|
65
|
+
// phiLuminance: variance-scaled luminance edge-stop (bigger = more blending).
|
|
66
|
+
// phiNormal: normal cone exponent (bigger = tighter, preserves more edges).
|
|
67
|
+
// phiDepth: RELATIVE depth tolerance (fraction of ray distance, scale-invariant).
|
|
68
|
+
this.filterStrength = uniform( options.filterStrength ?? 1.0 );
|
|
69
|
+
this.phiLuminance = uniform( options.phiLuminance ?? 4.0 );
|
|
70
|
+
this.phiNormal = uniform( options.phiNormal ?? 64.0 );
|
|
71
|
+
this.phiDepth = uniform( options.phiDepth ?? 0.1 );
|
|
72
|
+
this.iterations = options.atrousIterations ?? options.edgeAtrousIterations ?? 5;
|
|
73
|
+
|
|
74
|
+
this.stepSizeU = uniform( 1, 'int' );
|
|
75
|
+
this.isLastIterationU = uniform( 0, 'int' );
|
|
44
76
|
this.resW = uniform( options.width || 1 );
|
|
45
77
|
this.resH = uniform( options.height || 1 );
|
|
46
78
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this._inputTexNode = new TextureNode();
|
|
79
|
+
// Input texture nodes (RenderTarget-backed — safe to bind pre-compile).
|
|
80
|
+
this._colorTexNode = new TextureNode();
|
|
50
81
|
this._ndTexNode = new TextureNode();
|
|
82
|
+
this._snTexNode = new TextureNode();
|
|
83
|
+
this._albedoTexNode = new TextureNode();
|
|
84
|
+
this._varTexNode = new TextureNode();
|
|
85
|
+
|
|
86
|
+
// À-trous read node: defaults to EmptyTexture at compile time so the WGSL
|
|
87
|
+
// codegen emits the `level` parameter required for later StorageTexture reads.
|
|
88
|
+
this._readTexNode = new TextureNode();
|
|
51
89
|
|
|
52
|
-
// Pre-allocate StorageTexture at max — defensive against three.js #33061
|
|
53
|
-
// (TSL compute pipeline re-compile returns zeros after resize).
|
|
54
90
|
const w = options.width || 1;
|
|
55
91
|
const h = options.height || 1;
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
93
|
+
// Pre-allocate StorageTextures at max — defensive against three.js #33061
|
|
94
|
+
// (TSL compute pipeline keeps a stale GPUTextureView after setSize()).
|
|
95
|
+
// LinearFilter required for textureLoad codegen on StorageTextures.
|
|
96
|
+
this._demodTex = this._makeStorageTex(); // demodulated lighting (pre-pass output)
|
|
97
|
+
this._storageTexA = this._makeStorageTex(); // à-trous ping
|
|
98
|
+
this._storageTexB = this._makeStorageTex(); // à-trous pong
|
|
62
99
|
|
|
63
100
|
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
64
101
|
|
|
102
|
+
// Active-size RT published downstream (UV-sampled); the over-allocated
|
|
103
|
+
// StorageTexture must not be published — UV sampling reads the wrong region.
|
|
104
|
+
// FloatType to match the storage textures so copyTextureToTexture is format-compatible.
|
|
65
105
|
this.outputTarget = new RenderTarget( w, h, {
|
|
66
|
-
type:
|
|
106
|
+
type: FloatType,
|
|
67
107
|
format: RGBAFormat,
|
|
68
|
-
minFilter:
|
|
69
|
-
magFilter:
|
|
108
|
+
minFilter: LinearFilter,
|
|
109
|
+
magFilter: LinearFilter,
|
|
70
110
|
depthBuffer: false,
|
|
71
111
|
stencilBuffer: false
|
|
72
112
|
} );
|
|
73
113
|
|
|
114
|
+
this._compiled = false;
|
|
115
|
+
|
|
74
116
|
this._dispatchX = Math.ceil( w / 16 );
|
|
75
117
|
this._dispatchY = Math.ceil( h / 16 );
|
|
76
118
|
|
|
@@ -78,17 +120,88 @@ export class EdgeFilter extends RenderStage {
|
|
|
78
120
|
|
|
79
121
|
}
|
|
80
122
|
|
|
123
|
+
_makeStorageTex() {
|
|
124
|
+
|
|
125
|
+
// FloatType: demodulated lighting on dark materials (color / 0.01) exceeds
|
|
126
|
+
// HalfFloat's 65k cap on HDR — same reason ASVGF's ping-pong is FloatType.
|
|
127
|
+
const tex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
|
|
128
|
+
tex.type = FloatType;
|
|
129
|
+
tex.format = RGBAFormat;
|
|
130
|
+
tex.minFilter = LinearFilter;
|
|
131
|
+
tex.magFilter = LinearFilter;
|
|
132
|
+
return tex;
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
81
136
|
_buildCompute() {
|
|
82
137
|
|
|
83
|
-
|
|
138
|
+
this._computeDemod = this._buildDemod();
|
|
139
|
+
// One à-trous node per ping-pong write direction.
|
|
140
|
+
this._computeAtrousA = this._buildAtrous( this._storageTexA );
|
|
141
|
+
this._computeAtrousB = this._buildAtrous( this._storageTexB );
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Demodulate pass: lighting = color / albedo, written to _demodTex.
|
|
146
|
+
_buildDemod() {
|
|
147
|
+
|
|
148
|
+
const colorTex = this._colorTexNode;
|
|
149
|
+
const ndTex = this._ndTexNode;
|
|
150
|
+
const albedoTex = this._albedoTexNode;
|
|
151
|
+
const demodTex = this._demodTex;
|
|
152
|
+
const resW = this.resW;
|
|
153
|
+
const resH = this.resH;
|
|
154
|
+
|
|
155
|
+
const WG_SIZE = 16;
|
|
156
|
+
|
|
157
|
+
const computeFn = Fn( () => {
|
|
158
|
+
|
|
159
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
160
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
161
|
+
|
|
162
|
+
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
163
|
+
|
|
164
|
+
const coord = ivec2( gx, gy );
|
|
165
|
+
const color = textureLoad( colorTex, coord ).xyz;
|
|
166
|
+
const depth = textureLoad( ndTex, coord ).w;
|
|
167
|
+
const isHit = depth.lessThan( float( MISS_THRESHOLD ) );
|
|
168
|
+
|
|
169
|
+
// Miss rays (env/background) have black aux albedo — dividing by it would
|
|
170
|
+
// overflow HalfFloat on bright HDR sky and remodulate back to zero. Use a
|
|
171
|
+
// neutral albedo of 1 for misses so the demod/remod round-trip is identity.
|
|
172
|
+
const albedo = textureLoad( albedoTex, coord ).xyz;
|
|
173
|
+
const demodAlbedo = select( isHit, max( albedo, vec3( ALBEDO_EPS ) ), vec3( 1.0 ) );
|
|
174
|
+
|
|
175
|
+
const lighting = sanitizeRGB( color.div( demodAlbedo ) );
|
|
176
|
+
|
|
177
|
+
textureStore( demodTex, uvec2( uint( gx ), uint( gy ) ), vec4( lighting, 1.0 ) ).toWriteOnly();
|
|
178
|
+
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
return computeFn().compute(
|
|
184
|
+
[ this._dispatchX, this._dispatchY, 1 ],
|
|
185
|
+
[ WG_SIZE, WG_SIZE, 1 ]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// À-trous pass over demodulated lighting, writing to `writeStorageTex`.
|
|
191
|
+
_buildAtrous( writeStorageTex ) {
|
|
192
|
+
|
|
193
|
+
const readTex = this._readTexNode;
|
|
194
|
+
const colorTex = this._colorTexNode;
|
|
84
195
|
const ndTex = this._ndTexNode;
|
|
85
|
-
const
|
|
196
|
+
const snTex = this._snTexNode;
|
|
197
|
+
const albedoTex = this._albedoTexNode;
|
|
198
|
+
const varTex = this._varTexNode;
|
|
86
199
|
const filterStrength = this.filterStrength;
|
|
87
|
-
const
|
|
88
|
-
const threshold = this.edgeThreshold;
|
|
200
|
+
const phiLuminance = this.phiLuminance;
|
|
89
201
|
const phiNormal = this.phiNormal;
|
|
90
202
|
const phiDepth = this.phiDepth;
|
|
91
|
-
const
|
|
203
|
+
const stepSize = this.stepSizeU;
|
|
204
|
+
const isLastIterationU = this.isLastIterationU;
|
|
92
205
|
const resW = this.resW;
|
|
93
206
|
const resH = this.resH;
|
|
94
207
|
|
|
@@ -102,65 +215,68 @@ export class EdgeFilter extends RenderStage {
|
|
|
102
215
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
103
216
|
|
|
104
217
|
const coord = ivec2( gx, gy );
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
// NormalDepth writes (0,0,0, 65504) 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. Threshold
|
|
111
|
-
// sits below the 65504 sentinel (and above any real depth).
|
|
112
|
-
const MISS_THRESHOLD = 6e4;
|
|
218
|
+
|
|
219
|
+
const centerLighting = textureLoad( readTex, coord ).xyz;
|
|
220
|
+
const centerLum = luminance( centerLighting );
|
|
113
221
|
const centerND = textureLoad( ndTex, coord );
|
|
114
|
-
const centerNormal = centerND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
115
222
|
const centerDepth = centerND.w;
|
|
116
|
-
|
|
223
|
+
// Shading (bump-perturbed) normal so normal/bump detail isn't collapsed.
|
|
224
|
+
const centerNormal = textureLoad( snTex, coord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
225
|
+
const centerIsHitBool = centerDepth.lessThan( float( MISS_THRESHOLD ) );
|
|
226
|
+
const centerIsHit = select( centerIsHitBool, float( 1.0 ), float( 0.0 ) );
|
|
117
227
|
|
|
118
|
-
const
|
|
228
|
+
const centerAlbedo = textureLoad( albedoTex, coord ).xyz;
|
|
229
|
+
const demodAlbedo = select( centerIsHitBool, max( centerAlbedo, vec3( ALBEDO_EPS ) ), vec3( 1.0 ) );
|
|
230
|
+
const centerAlbedoLum = max( luminance( demodAlbedo ), float( ALBEDO_EPS ) );
|
|
119
231
|
|
|
120
|
-
|
|
121
|
-
|
|
232
|
+
// Variance-guided luminance σ (SVGF). .z = temporal, .w = spatial variance;
|
|
233
|
+
// max() falls back to the spatial estimate as temporal collapses on a
|
|
234
|
+
// converging accumulation buffer. Dividing by albedoLum rescales the
|
|
235
|
+
// modulated-space variance into demodulated space (std ∝ 1/albedo).
|
|
236
|
+
const vSample = textureLoad( varTex, coord );
|
|
237
|
+
const variance = max( vSample.z, vSample.w );
|
|
238
|
+
const sigmaL = phiLuminance.mul( sqrt( max( variance, float( 0.0 ) ) ) )
|
|
239
|
+
.div( centerAlbedoLum ).add( float( 0.0001 ) );
|
|
122
240
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
[ 1, 1 ], [ - 1, 1 ], [ - 1, - 1 ], [ 1, - 1 ],
|
|
126
|
-
];
|
|
241
|
+
const colorSum = vec3( 0.0 ).toVar();
|
|
242
|
+
const weightSum = float( 0.0 ).toVar();
|
|
127
243
|
|
|
128
|
-
for (
|
|
244
|
+
for ( let iy = 0; iy < 5; iy ++ ) {
|
|
129
245
|
|
|
130
|
-
for (
|
|
246
|
+
for ( let ix = 0; ix < 5; ix ++ ) {
|
|
131
247
|
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
248
|
+
const dx = ix - 2;
|
|
249
|
+
const dy = iy - 2;
|
|
250
|
+
const kw = ATROUS_KERNEL[ iy * 5 + ix ];
|
|
135
251
|
|
|
136
|
-
const
|
|
137
|
-
const
|
|
252
|
+
const sx = gx.add( stepSize.mul( dx ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
253
|
+
const sy = gy.add( stepSize.mul( dy ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
254
|
+
const sCoord = ivec2( sx, sy );
|
|
138
255
|
|
|
256
|
+
const sLighting = textureLoad( readTex, sCoord ).xyz;
|
|
257
|
+
const sLum = luminance( sLighting );
|
|
139
258
|
const sND = textureLoad( ndTex, sCoord );
|
|
140
|
-
const sNormal = sND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
141
259
|
const sDepth = sND.w;
|
|
142
|
-
const
|
|
260
|
+
const sNormal = textureLoad( snTex, sCoord ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
261
|
+
const sampleIsHit = select( sDepth.lessThan( float( MISS_THRESHOLD ) ), float( 1.0 ), float( 0.0 ) );
|
|
143
262
|
|
|
144
|
-
const lumW =
|
|
263
|
+
const lumW = centerLum.sub( sLum ).abs().div( sigmaL ).negate().exp();
|
|
264
|
+
// Clamp dot to [0,1] before pow — miss-ray normals decode to non-unit
|
|
265
|
+
// (-1,-1,-1) with dot 3, which would saturate pow to +inf → inf*0 = NaN.
|
|
266
|
+
const normW = pow( dot( centerNormal, sNormal ).clamp( 0.0, 1.0 ), phiNormal );
|
|
267
|
+
// Relative depth tolerance → scale-invariant across scene sizes.
|
|
268
|
+
const depW = centerDepth.sub( sDepth ).abs()
|
|
269
|
+
.div( max( centerDepth.mul( phiDepth ), float( 0.001 ) ) ).negate().exp();
|
|
145
270
|
|
|
146
271
|
const bothHit = centerIsHit.mul( sampleIsHit );
|
|
147
272
|
const bothMiss = centerIsHit.oneMinus().mul( sampleIsHit.oneMinus() );
|
|
148
273
|
const sameKind = bothHit.add( bothMiss );
|
|
149
274
|
|
|
150
|
-
//
|
|
151
|
-
// non-unit (-1,-1,-1) with dot=3, which would saturate pow(.,phi)
|
|
152
|
-
// to +inf and poison downstream via inf*0 = NaN.
|
|
153
|
-
const cosTheta = dot( centerNormal, sNormal ).clamp( 0.0, 1.0 );
|
|
154
|
-
const normW = pow( cosTheta, phiNormal );
|
|
155
|
-
const depW = abs( centerDepth.sub( sDepth ) ).div( max( phiDepth, float( 0.001 ) ) ).negate().exp();
|
|
156
|
-
|
|
157
|
-
// Geometric weights only meaningful for hit-vs-hit pairs.
|
|
275
|
+
// Geometric weights are only meaningful for hit-vs-hit pairs.
|
|
158
276
|
const geomW = mix( float( 1.0 ), normW.mul( depW ), bothHit );
|
|
277
|
+
const w = float( kw ).mul( lumW ).mul( geomW ).mul( sameKind );
|
|
159
278
|
|
|
160
|
-
|
|
161
|
-
const w = lumW.mul( geomW ).mul( sameKind ).mul( distWeight );
|
|
162
|
-
|
|
163
|
-
colorSum.addAssign( sColor.mul( w ) );
|
|
279
|
+
colorSum.addAssign( sLighting.mul( w ) );
|
|
164
280
|
weightSum.addAssign( w );
|
|
165
281
|
|
|
166
282
|
}
|
|
@@ -168,28 +284,22 @@ export class EdgeFilter extends RenderStage {
|
|
|
168
284
|
}
|
|
169
285
|
|
|
170
286
|
const filtered = colorSum.div( max( weightSum, float( 0.0001 ) ) );
|
|
171
|
-
const finalColor = mix( center, filtered, effectiveStrength );
|
|
172
|
-
|
|
173
|
-
// Firefly clamp on output luminance.
|
|
174
|
-
const finalLum = dot( finalColor, REC709_LUMINANCE_COEFFICIENTS );
|
|
175
|
-
const clampedColor = finalColor.toVar();
|
|
176
|
-
If( finalLum.greaterThan( 10.0 ), () => {
|
|
177
287
|
|
|
178
|
-
|
|
288
|
+
// Final pass: remodulate by albedo and blend against the raw color by
|
|
289
|
+
// filterStrength. Inner passes stay in demodulated space.
|
|
290
|
+
const isLast = isLastIterationU.equal( int( 1 ) );
|
|
291
|
+
const rawColor = textureLoad( colorTex, coord ).xyz;
|
|
292
|
+
const remodded = filtered.mul( demodAlbedo );
|
|
293
|
+
const finalModulated = mix( rawColor, remodded, filterStrength );
|
|
294
|
+
const output = isLast.select( finalModulated, filtered );
|
|
179
295
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
textureStore(
|
|
183
|
-
outputStorageTex,
|
|
184
|
-
uvec2( uint( gx ), uint( gy ) ),
|
|
185
|
-
vec4( clampedColor, 1.0 )
|
|
186
|
-
).toWriteOnly();
|
|
296
|
+
textureStore( writeStorageTex, uvec2( uint( gx ), uint( gy ) ), vec4( output, 1.0 ) ).toWriteOnly();
|
|
187
297
|
|
|
188
298
|
} );
|
|
189
299
|
|
|
190
300
|
} );
|
|
191
301
|
|
|
192
|
-
|
|
302
|
+
return computeFn().compute(
|
|
193
303
|
[ this._dispatchX, this._dispatchY, 1 ],
|
|
194
304
|
[ WG_SIZE, WG_SIZE, 1 ]
|
|
195
305
|
);
|
|
@@ -200,29 +310,31 @@ export class EdgeFilter extends RenderStage {
|
|
|
200
310
|
|
|
201
311
|
if ( ! this.enabled ) return;
|
|
202
312
|
|
|
203
|
-
const
|
|
204
|
-
|| context.getTexture( 'bilateralFiltering:output' )
|
|
205
|
-
|| context.getTexture( 'pathtracer:color' );
|
|
206
|
-
|
|
313
|
+
const colorTex = context.getTexture( 'pathtracer:color' );
|
|
207
314
|
const ndTex = context.getTexture( 'pathtracer:normalDepth' );
|
|
315
|
+
// Fall back to geometric normalDepth if the mapped normal isn't published.
|
|
316
|
+
const snTex = context.getTexture( 'pathtracer:shadingNormal' ) || ndTex;
|
|
317
|
+
const albedoTex = context.getTexture( 'pathtracer:albedo' );
|
|
318
|
+
const varTex = context.getTexture( 'variance:output' );
|
|
208
319
|
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
320
|
+
// The SVGF filter needs color + geometry + albedo + variance. Without the full
|
|
321
|
+
// set there's no edge/variance guidance — pass the input through rather than
|
|
322
|
+
// producing an unguided blur.
|
|
323
|
+
if ( ! colorTex || ! ndTex || ! albedoTex || ! varTex ) {
|
|
212
324
|
|
|
213
|
-
if (
|
|
325
|
+
if ( colorTex ) context.setTexture( 'edgeFiltering:output', colorTex );
|
|
214
326
|
return;
|
|
215
327
|
|
|
216
328
|
}
|
|
217
329
|
|
|
218
330
|
if ( context.getState( 'interactionMode' ) ) {
|
|
219
331
|
|
|
220
|
-
context.setTexture( 'edgeFiltering:output',
|
|
332
|
+
context.setTexture( 'edgeFiltering:output', colorTex );
|
|
221
333
|
return;
|
|
222
334
|
|
|
223
335
|
}
|
|
224
336
|
|
|
225
|
-
const img =
|
|
337
|
+
const img = colorTex.image;
|
|
226
338
|
if ( img && img.width > 0 && img.height > 0 ) {
|
|
227
339
|
|
|
228
340
|
if ( img.width !== this.outputTarget.width ||
|
|
@@ -234,18 +346,56 @@ export class EdgeFilter extends RenderStage {
|
|
|
234
346
|
|
|
235
347
|
}
|
|
236
348
|
|
|
237
|
-
this.
|
|
349
|
+
this._colorTexNode.value = colorTex;
|
|
238
350
|
this._ndTexNode.value = ndTex;
|
|
351
|
+
this._snTexNode.value = snTex;
|
|
352
|
+
this._albedoTexNode.value = albedoTex;
|
|
353
|
+
this._varTexNode.value = varTex;
|
|
354
|
+
|
|
355
|
+
// First-frame compile while _readTexNode still holds EmptyTexture — codegen
|
|
356
|
+
// then emits textureLoad with the level parameter the runtime requires for
|
|
357
|
+
// non-zero StorageTexture reads. The throwaway dispatches also initialise the
|
|
358
|
+
// ping-pong textures. _readTexNode must NOT be assigned before this.
|
|
359
|
+
if ( ! this._compiled ) {
|
|
360
|
+
|
|
361
|
+
this.renderer.compute( this._computeDemod );
|
|
362
|
+
this.renderer.compute( this._computeAtrousA );
|
|
363
|
+
this.renderer.compute( this._computeAtrousB );
|
|
364
|
+
this._compiled = true;
|
|
365
|
+
|
|
366
|
+
}
|
|
239
367
|
|
|
240
|
-
|
|
241
|
-
this.
|
|
368
|
+
// 1. Demodulate → _demodTex.
|
|
369
|
+
this.renderer.compute( this._computeDemod );
|
|
242
370
|
|
|
243
|
-
|
|
371
|
+
// 2. À-trous iterations: step 2^i, ping-pong write direction. First pass reads
|
|
372
|
+
// _demodTex; the last pass remodulates + strength-blends.
|
|
373
|
+
let readTex = this._demodTex;
|
|
374
|
+
let writeNode = this._computeAtrousA;
|
|
375
|
+
let nextWriteNode = this._computeAtrousB;
|
|
376
|
+
|
|
377
|
+
for ( let i = 0; i < this.iterations; i ++ ) {
|
|
378
|
+
|
|
379
|
+
this.stepSizeU.value = 1 << i;
|
|
380
|
+
this.isLastIterationU.value = ( i === this.iterations - 1 ) ? 1 : 0;
|
|
381
|
+
this._readTexNode.value = readTex;
|
|
382
|
+
|
|
383
|
+
this.renderer.compute( writeNode );
|
|
384
|
+
|
|
385
|
+
readTex = ( writeNode === this._computeAtrousA )
|
|
386
|
+
? this._storageTexA
|
|
387
|
+
: this._storageTexB;
|
|
388
|
+
|
|
389
|
+
const tmp = writeNode;
|
|
390
|
+
writeNode = nextWriteNode;
|
|
391
|
+
nextWriteNode = tmp;
|
|
392
|
+
|
|
393
|
+
}
|
|
244
394
|
|
|
245
|
-
// Copy out of the over-allocated StorageTexture into the
|
|
246
|
-
// RenderTarget; downstream stages
|
|
395
|
+
// Copy the final result out of the over-allocated StorageTexture into the
|
|
396
|
+
// active-size RenderTarget; downstream stages UV-sample the latter.
|
|
247
397
|
this._srcRegion.max.set( this.outputTarget.width, this.outputTarget.height );
|
|
248
|
-
this.renderer.copyTextureToTexture(
|
|
398
|
+
this.renderer.copyTextureToTexture( readTex, this.outputTarget.texture, this._srcRegion );
|
|
249
399
|
|
|
250
400
|
context.setTexture( 'edgeFiltering:output', this.outputTarget.texture );
|
|
251
401
|
|
|
@@ -261,35 +411,35 @@ export class EdgeFilter extends RenderStage {
|
|
|
261
411
|
|
|
262
412
|
if ( ! params ) return;
|
|
263
413
|
if ( params.filterStrength !== undefined ) this.filterStrength.value = params.filterStrength;
|
|
264
|
-
if ( params.
|
|
265
|
-
if ( params.edgeThreshold !== undefined ) this.edgeThreshold.value = params.edgeThreshold;
|
|
414
|
+
if ( params.phiLuminance !== undefined ) this.phiLuminance.value = params.phiLuminance;
|
|
266
415
|
if ( params.phiNormal !== undefined ) this.phiNormal.value = params.phiNormal;
|
|
267
416
|
if ( params.phiDepth !== undefined ) this.phiDepth.value = params.phiDepth;
|
|
417
|
+
if ( params.atrousIterations !== undefined ) this.iterations = params.atrousIterations;
|
|
268
418
|
|
|
269
419
|
}
|
|
270
420
|
|
|
271
|
-
// Free the 2048²
|
|
272
|
-
// after re-enable
|
|
421
|
+
// Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
|
|
422
|
+
// after re-enable (no temporal state to re-anchor). See ASVGF.releaseGPUMemory.
|
|
273
423
|
releaseGPUMemory() {
|
|
274
424
|
|
|
275
|
-
this.
|
|
425
|
+
this._demodTex?.dispose();
|
|
426
|
+
this._storageTexA?.dispose();
|
|
427
|
+
this._storageTexB?.dispose();
|
|
276
428
|
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
277
429
|
this.context?.removeTexture( 'edgeFiltering:output' );
|
|
278
430
|
this.outputTarget?.texture?.dispose();
|
|
279
|
-
this.reset();
|
|
280
431
|
|
|
281
432
|
}
|
|
282
433
|
|
|
283
434
|
reset() {
|
|
284
435
|
|
|
285
|
-
|
|
286
|
-
this.iterationCount.value = 0;
|
|
436
|
+
// No per-frame or temporal state — variance guidance is owned by the Variance stage.
|
|
287
437
|
|
|
288
438
|
}
|
|
289
439
|
|
|
290
440
|
setSize( width, height ) {
|
|
291
441
|
|
|
292
|
-
//
|
|
442
|
+
// StorageTextures stay at their max allocation (see constructor).
|
|
293
443
|
this.outputTarget.setSize( width, height );
|
|
294
444
|
this.outputTarget.texture.needsUpdate = true;
|
|
295
445
|
this.resW.value = width;
|
|
@@ -297,17 +447,28 @@ export class EdgeFilter extends RenderStage {
|
|
|
297
447
|
|
|
298
448
|
this._dispatchX = Math.ceil( width / 16 );
|
|
299
449
|
this._dispatchY = Math.ceil( height / 16 );
|
|
300
|
-
|
|
450
|
+
const size = [ this._dispatchX, this._dispatchY, 1 ];
|
|
451
|
+
this._computeDemod.dispatchSize = size;
|
|
452
|
+
this._computeAtrousA.dispatchSize = size;
|
|
453
|
+
this._computeAtrousB.dispatchSize = size;
|
|
301
454
|
|
|
302
455
|
}
|
|
303
456
|
|
|
304
457
|
dispose() {
|
|
305
458
|
|
|
306
|
-
this.
|
|
307
|
-
this.
|
|
459
|
+
this._computeDemod?.dispose();
|
|
460
|
+
this._computeAtrousA?.dispose();
|
|
461
|
+
this._computeAtrousB?.dispose();
|
|
462
|
+
this._demodTex?.dispose();
|
|
463
|
+
this._storageTexA?.dispose();
|
|
464
|
+
this._storageTexB?.dispose();
|
|
308
465
|
this.outputTarget?.dispose();
|
|
309
|
-
this.
|
|
466
|
+
this._colorTexNode?.dispose();
|
|
310
467
|
this._ndTexNode?.dispose();
|
|
468
|
+
this._snTexNode?.dispose();
|
|
469
|
+
this._albedoTexNode?.dispose();
|
|
470
|
+
this._varTexNode?.dispose();
|
|
471
|
+
this._readTexNode?.dispose();
|
|
311
472
|
|
|
312
473
|
}
|
|
313
474
|
|
package/src/TSL/Clearcoat.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Fn,
|
|
3
3
|
vec3,
|
|
4
4
|
float,
|
|
5
|
+
dot,
|
|
5
6
|
normalize,
|
|
6
7
|
reflect,
|
|
7
8
|
max,
|
|
@@ -10,10 +11,10 @@ import {
|
|
|
10
11
|
|
|
11
12
|
import { struct } from './patches.js';
|
|
12
13
|
|
|
13
|
-
import { DotProducts } from './Struct.js';
|
|
14
|
-
import { PI, MIN_CLEARCOAT_ROUGHNESS,
|
|
15
|
-
import { DistributionGGX } from './MaterialProperties.js';
|
|
16
|
-
import { ImportanceSampleGGX, ImportanceSampleCosine } from './MaterialSampling.js';
|
|
14
|
+
import { AnisoFrame, DotProducts } from './Struct.js';
|
|
15
|
+
import { PI, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame } from './Common.js';
|
|
16
|
+
import { DistributionGGX, computeAnisoAlphas, calculateVNDFPDFAniso } from './MaterialProperties.js';
|
|
17
|
+
import { ImportanceSampleGGX, ImportanceSampleCosine, sampleGGXVNDFAniso } from './MaterialSampling.js';
|
|
17
18
|
import { evaluateLayeredBRDF } from './MaterialEvaluation.js';
|
|
18
19
|
import { RandomValue } from './Random.js';
|
|
19
20
|
|
|
@@ -62,8 +63,20 @@ export const sampleClearcoat = Fn( ( [
|
|
|
62
63
|
|
|
63
64
|
} ).ElseIf( rand.lessThan( clearcoatWeight.add( specularWeight ) ), () => {
|
|
64
65
|
|
|
65
|
-
// Sample base specular
|
|
66
|
-
|
|
66
|
+
// Sample base specular (anisotropic VNDF when anisotropy > 0)
|
|
67
|
+
If( material.anisotropy.greaterThan( 0.0 ), () => {
|
|
68
|
+
|
|
69
|
+
const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
|
|
70
|
+
const localV = vec3( dot( V, f.Ta ), dot( V, f.Ba ), dot( V, N ) );
|
|
71
|
+
const a = computeAnisoAlphas( material.roughness, material.anisotropy );
|
|
72
|
+
const localH = sampleGGXVNDFAniso( { V: localV, alphaX: a.x, alphaY: a.y, Xi: randomSample } );
|
|
73
|
+
H.assign( f.Ta.mul( localH.x ).add( f.Ba.mul( localH.y ) ).add( N.mul( localH.z ) ) );
|
|
74
|
+
|
|
75
|
+
} ).Else( () => {
|
|
76
|
+
|
|
77
|
+
H.assign( ImportanceSampleGGX( { N, roughness: baseRoughness, Xi: randomSample } ) );
|
|
78
|
+
|
|
79
|
+
} );
|
|
67
80
|
L.assign( reflect( V.negate(), H ) );
|
|
68
81
|
|
|
69
82
|
} ).Else( () => {
|
|
@@ -74,12 +87,23 @@ export const sampleClearcoat = Fn( ( [
|
|
|
74
87
|
|
|
75
88
|
} );
|
|
76
89
|
|
|
77
|
-
// Calculate dot products
|
|
78
|
-
const dots = DotProducts.wrap(
|
|
90
|
+
// Calculate dot products (aniso-aware: also projects onto the anisotropy frame)
|
|
91
|
+
const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
|
|
79
92
|
|
|
80
|
-
// Calculate individual PDFs
|
|
93
|
+
// Calculate individual PDFs. Clearcoat layer is always isotropic; the base specular
|
|
94
|
+
// matches its sampler — VNDF-aniso when anisotropy>0, else the half-vector GGX pdf.
|
|
81
95
|
const clearcoatPDF = DistributionGGX( dots.NoH, clearcoatRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( clearcoatWeight );
|
|
82
|
-
const specularPDF =
|
|
96
|
+
const specularPDF = float( 0.0 ).toVar();
|
|
97
|
+
If( material.anisotropy.greaterThan( 0.0 ), () => {
|
|
98
|
+
|
|
99
|
+
const a = computeAnisoAlphas( material.roughness, material.anisotropy );
|
|
100
|
+
specularPDF.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ).mul( specularWeight ) );
|
|
101
|
+
|
|
102
|
+
} ).Else( () => {
|
|
103
|
+
|
|
104
|
+
specularPDF.assign( DistributionGGX( dots.NoH, baseRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( specularWeight ) );
|
|
105
|
+
|
|
106
|
+
} );
|
|
83
107
|
const diffusePDF = dots.NoL.div( PI ).mul( diffuseWeight );
|
|
84
108
|
|
|
85
109
|
// Combined PDF using MIS
|