rayzee 7.20.2 → 7.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rayzee.es.js +3855 -3065
- package/dist/rayzee.umd.js +83 -109
- package/package.json +1 -1
- package/src/EngineDefaults.js +4 -3
- package/src/PathTracerApp.js +7 -0
- package/src/Pipeline/BindingAudit.js +178 -0
- package/src/Pipeline/RenderPipeline.js +5 -2
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/PackedRayBuffer.js +33 -8
- 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 +10 -2
- package/src/Stages/PathTracer.js +7 -3
- package/src/Stages/PathTracerStage.js +15 -46
- package/src/TSL/Clearcoat.js +29 -40
- package/src/TSL/Common.js +1 -47
- package/src/TSL/EmissiveSampling.js +47 -50
- package/src/TSL/LightBVHSampling.js +16 -12
- package/src/TSL/LightsCore.js +3 -2
- package/src/TSL/LightsDirect.js +4 -4
- package/src/TSL/LightsIndirect.js +36 -355
- package/src/TSL/LightsSampling.js +153 -386
- package/src/TSL/MaterialEvaluation.js +44 -71
- package/src/TSL/MaterialProperties.js +209 -176
- package/src/TSL/MaterialTransmission.js +17 -32
- package/src/TSL/PathTracerCore.js +51 -66
- package/src/TSL/Random.js +202 -132
- package/src/TSL/ShadeKernel.js +89 -56
- package/src/TSL/Struct.js +6 -38
- package/src/TSL/Subsurface.js +13 -8
- package/src/index.js +5 -0
- package/src/managers/EnvironmentManager.js +0 -5
- package/src/managers/UniformManager.js +4 -1
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 );
|
|
@@ -122,8 +122,11 @@ export class NormalDepth extends RenderStage {
|
|
|
122
122
|
|
|
123
123
|
this.on( 'pipeline:reset', () => {
|
|
124
124
|
|
|
125
|
+
// _hasHistory is deliberately NOT cleared, mirroring MotionVector.reset(): the
|
|
126
|
+
// prev-frame G-buffer tracks camera motion, not accumulation, and a camera move
|
|
127
|
+
// resets accumulation every frame of a drag. Clearing it made render() overwrite
|
|
128
|
+
// prevRT with the current frame, so ASVGF's temporal gate could never reject.
|
|
125
129
|
this._dirty = true;
|
|
126
|
-
this._hasHistory = false;
|
|
127
130
|
|
|
128
131
|
} );
|
|
129
132
|
|
|
@@ -149,6 +152,8 @@ export class NormalDepth extends RenderStage {
|
|
|
149
152
|
this._bvhStorageNode = null;
|
|
150
153
|
this._matStorageNode = null;
|
|
151
154
|
this._dirty = true;
|
|
155
|
+
// New geometry — the previous frame describes a scene that is no longer there.
|
|
156
|
+
this._hasHistory = false;
|
|
152
157
|
|
|
153
158
|
}
|
|
154
159
|
|
|
@@ -367,13 +372,16 @@ export class NormalDepth extends RenderStage {
|
|
|
367
372
|
this._outputStorageTex?.dispose();
|
|
368
373
|
this._shadingStorageTex?.dispose();
|
|
369
374
|
this.reset();
|
|
375
|
+
// The textures backing the ping-pong are gone, so the prev-frame G-buffer is too.
|
|
376
|
+
this._hasHistory = false;
|
|
370
377
|
|
|
371
378
|
}
|
|
372
379
|
|
|
380
|
+
// Accumulation reset only — _hasHistory survives on purpose (see the pipeline:reset
|
|
381
|
+
// listener). It is invalidated by setSize(), a geometry swap, and releaseGPUMemory().
|
|
373
382
|
reset() {
|
|
374
383
|
|
|
375
384
|
this._dirty = true;
|
|
376
|
-
this._hasHistory = false;
|
|
377
385
|
|
|
378
386
|
}
|
|
379
387
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -272,6 +272,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
272
272
|
|
|
273
273
|
this._updateAccumulationUniforms( frameValue, renderMode );
|
|
274
274
|
this.frame.value = frameValue;
|
|
275
|
+
this.seedFrame.value = this._seedTick ++;
|
|
275
276
|
|
|
276
277
|
this._setWfDispatch();
|
|
277
278
|
|
|
@@ -942,7 +943,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
942
943
|
rngBufferRW: pb.rngBuffer.rw,
|
|
943
944
|
gBufferRW,
|
|
944
945
|
resolution: this.resolution,
|
|
945
|
-
|
|
946
|
+
// RNG axis only (baseSeed + stratified jitter) — takes the seed counter, not the
|
|
947
|
+
// accumulation index. FinalWrite keeps `frame`.
|
|
948
|
+
frame: this.seedFrame,
|
|
946
949
|
cameraWorldMatrix: this.cameraWorldMatrix,
|
|
947
950
|
cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
|
|
948
951
|
enableDOF: this.enableDOF,
|
|
@@ -1180,7 +1183,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
1180
1183
|
environmentIntensity: this.environmentIntensity,
|
|
1181
1184
|
envMatrix: this.environmentMatrix,
|
|
1182
1185
|
enableEnvironmentLight: this.enableEnvironment,
|
|
1183
|
-
useEnvMapIS: this.useEnvMapIS,
|
|
1184
1186
|
groundProjectionEnabled: this.groundProjectionEnabled,
|
|
1185
1187
|
groundProjectionRadius: this.groundProjectionRadius,
|
|
1186
1188
|
groundProjectionHeight: this.groundProjectionHeight,
|
|
@@ -1210,7 +1212,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
1210
1212
|
cameraProjectionMatrix: this.cameraProjectionMatrix,
|
|
1211
1213
|
cameraViewMatrix: this.cameraViewMatrix,
|
|
1212
1214
|
fireflyThreshold: this.fireflyThreshold,
|
|
1213
|
-
frame
|
|
1215
|
+
// RNG axis only (keys STBN via frame & 63).
|
|
1216
|
+
frame: this.seedFrame,
|
|
1217
|
+
accumFrame: this.frame,
|
|
1214
1218
|
resolution: this.resolution,
|
|
1215
1219
|
emissiveTriangleCount: this.emissiveTriangleCount,
|
|
1216
1220
|
emissiveVec4Offset: this.emissiveVec4Offset,
|
|
@@ -51,7 +51,6 @@ const BVH_VEC4_PER_NODE = 4;
|
|
|
51
51
|
* - camera:moved - When camera position/orientation changes
|
|
52
52
|
* - asvgf:reset - Request ASVGF to reset temporal data
|
|
53
53
|
* - asvgf:updateParameters - Update ASVGF parameters
|
|
54
|
-
* - asvgf:setTemporal - Enable/disable ASVGF temporal accumulation
|
|
55
54
|
*
|
|
56
55
|
* Textures published to context:
|
|
57
56
|
* - pathtracer:color - Main color output
|
|
@@ -143,6 +142,11 @@ export class PathTracerStage extends RenderStage {
|
|
|
143
142
|
this.lastCameraMatrix = new Matrix4();
|
|
144
143
|
this.lastProjectionMatrix = new Matrix4();
|
|
145
144
|
|
|
145
|
+
// Sampler seed axis — advances per rendered frame, survives accumulation resets.
|
|
146
|
+
// Pinned back to the accumulation index in deterministic mode.
|
|
147
|
+
this._seedTick = 0;
|
|
148
|
+
this._pinSeedToFrame = false;
|
|
149
|
+
|
|
146
150
|
// Denoising management state
|
|
147
151
|
this.lastRenderMode = - 1;
|
|
148
152
|
this.renderModeChangeTimeout = null;
|
|
@@ -293,18 +297,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
293
297
|
|
|
294
298
|
}
|
|
295
299
|
},
|
|
296
|
-
useEnvMapIS: {
|
|
297
|
-
get value() {
|
|
298
|
-
|
|
299
|
-
return self.useEnvMapIS.value;
|
|
300
|
-
|
|
301
|
-
},
|
|
302
|
-
set value( v ) {
|
|
303
|
-
|
|
304
|
-
self.useEnvMapIS.value = v;
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
300
|
enableAccumulation: {
|
|
309
301
|
get value() {
|
|
310
302
|
|
|
@@ -348,7 +340,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
348
340
|
enabled: DEFAULT_STATE.interactionModeEnabled,
|
|
349
341
|
qualitySettings: {
|
|
350
342
|
maxBounceCount: 1,
|
|
351
|
-
useEnvMapIS: false,
|
|
352
343
|
enableAccumulation: false,
|
|
353
344
|
enableEmissiveTriangleSampling: false,
|
|
354
345
|
},
|
|
@@ -651,10 +642,14 @@ export class PathTracerStage extends RenderStage {
|
|
|
651
642
|
this.isComplete = false;
|
|
652
643
|
this.performanceMonitor?.reset();
|
|
653
644
|
|
|
654
|
-
|
|
655
|
-
|
|
645
|
+
// lastRenderMode is deliberately NOT invalidated here — clearing it made
|
|
646
|
+
// manageASVGFForRenderMode see a phantom mode change 50 ms after every reset, wiping
|
|
647
|
+
// ASVGF's history. A real mode change is still caught by the renderMode comparison.
|
|
656
648
|
this.lastInteractionModeState = false;
|
|
657
649
|
|
|
650
|
+
// Only deterministic mode rewinds the seed axis; see _pinSeedToFrame.
|
|
651
|
+
if ( this._pinSeedToFrame ) this._seedTick = 0;
|
|
652
|
+
|
|
658
653
|
}
|
|
659
654
|
|
|
660
655
|
/**
|
|
@@ -1238,7 +1233,10 @@ export class PathTracerStage extends RenderStage {
|
|
|
1238
1233
|
if ( this.pendingRenderMode !== null && this.pendingRenderMode !== this.lastRenderMode ) {
|
|
1239
1234
|
|
|
1240
1235
|
this.lastRenderMode = this.pendingRenderMode;
|
|
1241
|
-
|
|
1236
|
+
// History from the previous mode is not comparable. temporalAlpha is NOT
|
|
1237
|
+
// touched here — it is owned by ASVGF_QUALITY_PRESETS, and overwriting it
|
|
1238
|
+
// with a hardcoded value meant `medium` and `high` never took effect.
|
|
1239
|
+
this.emit( 'asvgf:reset' );
|
|
1242
1240
|
|
|
1243
1241
|
}
|
|
1244
1242
|
|
|
@@ -1249,35 +1247,6 @@ export class PathTracerStage extends RenderStage {
|
|
|
1249
1247
|
|
|
1250
1248
|
}
|
|
1251
1249
|
|
|
1252
|
-
this._handleFullQuadASVGF();
|
|
1253
|
-
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
_onRenderModeChanged( newMode ) {
|
|
1257
|
-
|
|
1258
|
-
if ( newMode === 1 ) {
|
|
1259
|
-
|
|
1260
|
-
this.emit( 'asvgf:updateParameters', {
|
|
1261
|
-
enableDebug: false,
|
|
1262
|
-
temporalAlpha: 0.15
|
|
1263
|
-
} );
|
|
1264
|
-
|
|
1265
|
-
} else {
|
|
1266
|
-
|
|
1267
|
-
this.emit( 'asvgf:updateParameters', {
|
|
1268
|
-
temporalAlpha: 0.1,
|
|
1269
|
-
} );
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
this.emit( 'asvgf:reset' );
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
_handleFullQuadASVGF() {
|
|
1278
|
-
|
|
1279
|
-
this.emit( 'asvgf:setTemporal', { enabled: true } );
|
|
1280
|
-
|
|
1281
1250
|
}
|
|
1282
1251
|
|
|
1283
1252
|
// ===== UNIFORM & DATA SETTERS =====
|
package/src/TSL/Clearcoat.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Fn,
|
|
3
3
|
vec3,
|
|
4
|
-
|
|
4
|
+
int,
|
|
5
5
|
dot,
|
|
6
6
|
normalize,
|
|
7
7
|
reflect,
|
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import { struct } from './patches.js';
|
|
13
13
|
|
|
14
|
-
import { AnisoFrame, DotProducts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
14
|
+
import { AnisoFrame, DotProducts,
|
|
15
|
+
BRDFWeights,
|
|
16
|
+
} from './Struct.js';
|
|
17
|
+
import { MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame, constructTBN } from './Common.js';
|
|
18
|
+
import { computeAnisoAlphas, calculateBRDFWeightsFromMaterial, calculateBSDFSamplingPDF } from './MaterialProperties.js';
|
|
19
|
+
import { ImportanceSampleCosine, sampleGGXVNDF, sampleGGXVNDFAniso } from './MaterialSampling.js';
|
|
20
|
+
import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
|
|
21
|
+
import { getRandomSample1D } from './Random.js';
|
|
20
22
|
|
|
21
23
|
export const ClearcoatResult = struct( {
|
|
22
24
|
brdf: 'vec3',
|
|
@@ -29,6 +31,7 @@ export const ClearcoatResult = struct( {
|
|
|
29
31
|
// L (light direction) is returned via the out pattern as a separate return
|
|
30
32
|
export const sampleClearcoat = Fn( ( [
|
|
31
33
|
ray, hitInfo, material, randomSample, rngState,
|
|
34
|
+
pixelCoord, resolution, frame, dimBase,
|
|
32
35
|
] ) => {
|
|
33
36
|
|
|
34
37
|
const N = hitInfo.normal;
|
|
@@ -38,27 +41,26 @@ export const sampleClearcoat = Fn( ( [
|
|
|
38
41
|
const clearcoatRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
|
|
39
42
|
const baseRoughness = max( material.roughness, MIN_CLEARCOAT_ROUGHNESS );
|
|
40
43
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
specularWeight.
|
|
49
|
-
clearcoatWeight.divAssign( total );
|
|
50
|
-
diffuseWeight.divAssign( total );
|
|
44
|
+
// The shared selection probabilities, NOT a fourth private scheme. This function used to
|
|
45
|
+
// derive its own specular/clearcoat/diffuse split, so a clearcoat material was sampled with
|
|
46
|
+
// one set of probabilities while every MIS site evaluated the density of another — the white
|
|
47
|
+
// furnace read the resulting mismatch as an energy error that flipped sign once the other
|
|
48
|
+
// sites were unified.
|
|
49
|
+
const weights = BRDFWeights.wrap( calculateBRDFWeightsFromMaterial( material ) ).toVar();
|
|
50
|
+
const clearcoatWeight = weights.clearcoat.toVar();
|
|
51
|
+
const specularWeight = weights.specular.toVar();
|
|
51
52
|
|
|
52
53
|
// Choose which layer to sample
|
|
53
|
-
const rand =
|
|
54
|
+
const rand = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 13 ) ), rngState, resolution, frame );
|
|
54
55
|
|
|
55
56
|
const L = vec3( 0.0 ).toVar();
|
|
56
57
|
const H = vec3( 0.0 ).toVar();
|
|
57
58
|
|
|
58
59
|
If( rand.lessThan( clearcoatWeight ), () => {
|
|
59
60
|
|
|
60
|
-
// Sample clearcoat layer
|
|
61
|
-
|
|
61
|
+
// Sample clearcoat layer (VNDF — see the base-specular note below)
|
|
62
|
+
const ccTBN = constructTBN( { N } );
|
|
63
|
+
H.assign( ccTBN.mul( sampleGGXVNDF( { V: ccTBN.transpose().mul( V ), roughness: clearcoatRoughness, Xi: randomSample } ) ) );
|
|
62
64
|
L.assign( reflect( V.negate(), H ) );
|
|
63
65
|
|
|
64
66
|
} ).ElseIf( rand.lessThan( clearcoatWeight.add( specularWeight ) ), () => {
|
|
@@ -74,7 +76,9 @@ export const sampleClearcoat = Fn( ( [
|
|
|
74
76
|
|
|
75
77
|
} ).Else( () => {
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
// VNDF to match the density the MIS sites evaluate for this lobe.
|
|
80
|
+
const tbn = constructTBN( { N } );
|
|
81
|
+
H.assign( tbn.mul( sampleGGXVNDF( { V: tbn.transpose().mul( V ), roughness: baseRoughness, Xi: randomSample } ) ) );
|
|
78
82
|
|
|
79
83
|
} );
|
|
80
84
|
L.assign( reflect( V.negate(), H ) );
|
|
@@ -90,27 +94,12 @@ export const sampleClearcoat = Fn( ( [
|
|
|
90
94
|
// Calculate dot products (aniso-aware: also projects onto the anisotropy frame)
|
|
91
95
|
const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
|
|
92
96
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
const clearcoatPDF = DistributionGGX( dots.NoH, clearcoatRoughness ).mul( dots.NoH ).div( float( 4.0 ).mul( dots.VoH ) ).mul( clearcoatWeight );
|
|
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
|
-
} );
|
|
107
|
-
const diffusePDF = dots.NoL.div( PI ).mul( diffuseWeight );
|
|
108
|
-
|
|
109
|
-
// Combined PDF using MIS
|
|
110
|
-
const pdf = max( clearcoatPDF.add( specularPDF ).add( diffusePDF ), 0.001 );
|
|
97
|
+
// One density for every MIS site — see calculateBSDFSamplingPDF.
|
|
98
|
+
const pdf = max( calculateBSDFSamplingPDF( material, weights, dots ), 0.001 );
|
|
111
99
|
|
|
112
100
|
// Evaluate complete BRDF
|
|
113
|
-
|
|
101
|
+
// The same BRDF every other site evaluates — see the clearcoat note in MaterialEvaluation.
|
|
102
|
+
const brdf = evaluateMaterialResponseFromDots( material, dots );
|
|
114
103
|
|
|
115
104
|
// Return brdf, L direction, and pdf packed together
|
|
116
105
|
// Caller needs L and pdf - return as struct-like output
|
package/src/TSL/Common.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp
|
|
1
|
+
import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AnisoFrame,
|
|
5
5
|
DotProducts,
|
|
6
6
|
MaterialClassification,
|
|
7
|
-
MISStrategy,
|
|
8
7
|
RayTracingMaterial,
|
|
9
8
|
ShadowMaterial,
|
|
10
9
|
} from './Struct.js';
|
|
@@ -299,51 +298,6 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
|
|
|
299
298
|
|
|
300
299
|
} );
|
|
301
300
|
|
|
302
|
-
// Dynamic MIS strategy based on material properties
|
|
303
|
-
export const selectOptimalMISStrategy = Fn( ( [ roughness, metalness, bounceIndex, throughput ] ) => {
|
|
304
|
-
|
|
305
|
-
const throughputStrength = maxComponent( { v: throughput } ).toVar();
|
|
306
|
-
|
|
307
|
-
// Environment is now handled via deterministic NEE — not part of stochastic selection.
|
|
308
|
-
// Only lights and BRDF are stochastically selected.
|
|
309
|
-
const brdfWeight = float( 0.5 ).toVar();
|
|
310
|
-
const lightWeight = float( 0.5 ).toVar();
|
|
311
|
-
const useBRDFSampling = tslBool( true );
|
|
312
|
-
const useLightSampling = throughputStrength.greaterThan( 0.01 );
|
|
313
|
-
|
|
314
|
-
If( roughness.lessThan( 0.1 ).and( metalness.greaterThan( 0.8 ) ), () => {
|
|
315
|
-
|
|
316
|
-
// Highly specular materials — favor BRDF
|
|
317
|
-
brdfWeight.assign( 0.7 );
|
|
318
|
-
lightWeight.assign( 0.3 );
|
|
319
|
-
|
|
320
|
-
} ).ElseIf( roughness.greaterThan( 0.7 ), () => {
|
|
321
|
-
|
|
322
|
-
// Diffuse materials — favor light sampling
|
|
323
|
-
brdfWeight.assign( 0.4 );
|
|
324
|
-
lightWeight.assign( 0.6 );
|
|
325
|
-
|
|
326
|
-
} );
|
|
327
|
-
|
|
328
|
-
// Gentle adjustment for very deep bounces — shift toward BRDF
|
|
329
|
-
If( bounceIndex.greaterThan( int( 5 ) ), () => {
|
|
330
|
-
|
|
331
|
-
brdfWeight.assign( 0.6 );
|
|
332
|
-
lightWeight.assign( 0.4 );
|
|
333
|
-
|
|
334
|
-
} );
|
|
335
|
-
|
|
336
|
-
return MISStrategy( {
|
|
337
|
-
brdfWeight,
|
|
338
|
-
lightWeight,
|
|
339
|
-
envWeight: float( 0.0 ),
|
|
340
|
-
useBRDFSampling,
|
|
341
|
-
useLightSampling,
|
|
342
|
-
useEnvSampling: tslBool( false ),
|
|
343
|
-
} );
|
|
344
|
-
|
|
345
|
-
} );
|
|
346
|
-
|
|
347
301
|
// Storage buffer access — flat 1D indexing (WebGPU native)
|
|
348
302
|
// No 2D coordinate math needed: directly indexes into the buffer
|
|
349
303
|
export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dataOffset ] ) => {
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Fn,
|
|
6
|
-
vec2,
|
|
7
6
|
vec3,
|
|
8
7
|
float,
|
|
9
8
|
int,
|
|
@@ -27,7 +26,7 @@ import {
|
|
|
27
26
|
|
|
28
27
|
import { struct } from './patches.js';
|
|
29
28
|
import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProductsAniso } from './Common.js';
|
|
30
|
-
import {
|
|
29
|
+
import { getRandomSample1D, getRandomSample2D } from './Random.js';
|
|
31
30
|
import { calculateMaterialPDFFromDots } from './LightsSampling.js';
|
|
32
31
|
import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
|
|
33
32
|
import { DotProducts } from './Struct.js';
|
|
@@ -390,6 +389,7 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissive
|
|
|
390
389
|
export const sampleEmissiveTriangle = Fn( ( [
|
|
391
390
|
hitPoint, surfaceNormal,
|
|
392
391
|
rngState,
|
|
392
|
+
pixelCoord, resolution, frame, dimBase,
|
|
393
393
|
emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
|
|
394
394
|
triangleBuffer,
|
|
395
395
|
] ) => {
|
|
@@ -410,7 +410,7 @@ export const sampleEmissiveTriangle = Fn( ( [
|
|
|
410
410
|
If( emissiveTriangleCount.greaterThan( int( 0 ) ), () => {
|
|
411
411
|
|
|
412
412
|
// CDF importance-weighted triangle selection (brighter triangles sampled more)
|
|
413
|
-
const randEmissive =
|
|
413
|
+
const randEmissive = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame );
|
|
414
414
|
const emissiveIndex = binarySearchCDF( emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, randEmissive ).toVar();
|
|
415
415
|
|
|
416
416
|
// Fetch emissive triangle data from packed light buffer (2 vec4s per entry)
|
|
@@ -427,10 +427,7 @@ export const sampleEmissiveTriangle = Fn( ( [
|
|
|
427
427
|
// Fetch triangle geometry
|
|
428
428
|
const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
|
|
429
429
|
|
|
430
|
-
|
|
431
|
-
const xi_r1 = RandomValue( rngState ).toVar();
|
|
432
|
-
const xi_r2 = RandomValue( rngState ).toVar();
|
|
433
|
-
const xi = vec2( xi_r1, xi_r2 );
|
|
430
|
+
const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
|
|
434
431
|
|
|
435
432
|
const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
|
|
436
433
|
|
|
@@ -532,6 +529,7 @@ export const sampleEmissiveTriangle = Fn( ( [
|
|
|
532
529
|
export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
533
530
|
hitPoint, normal, viewDir, material,
|
|
534
531
|
bounceIndex, rngState,
|
|
532
|
+
pixelCoord, resolution, frame, dimBase,
|
|
535
533
|
emissiveBoost,
|
|
536
534
|
emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
|
|
537
535
|
triangleBuffer,
|
|
@@ -547,60 +545,57 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
|
547
545
|
distance: float( 0.0 ),
|
|
548
546
|
} ).toVar();
|
|
549
547
|
|
|
550
|
-
//
|
|
551
|
-
|
|
548
|
+
// No rough-diffuse secondary-bounce skip: dropping NEE while the emissive-hit MIS still
|
|
549
|
+
// down-weights BSDF hits deletes emitter energy from deep GI.
|
|
552
550
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
) );
|
|
561
|
-
|
|
562
|
-
If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
|
|
551
|
+
// Sample emissive triangle (CDF importance-weighted)
|
|
552
|
+
const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
|
|
553
|
+
hitPoint, normal, rngState,
|
|
554
|
+
pixelCoord, resolution, frame, dimBase,
|
|
555
|
+
emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
|
|
556
|
+
triangleBuffer,
|
|
557
|
+
) );
|
|
563
558
|
|
|
564
|
-
|
|
565
|
-
result.emissionOnly.assign( emissiveSample.emission );
|
|
566
|
-
result.distance.assign( emissiveSample.distance );
|
|
559
|
+
If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
|
|
567
560
|
|
|
568
|
-
|
|
569
|
-
|
|
561
|
+
result.hasEmissive.assign( true );
|
|
562
|
+
result.emissionOnly.assign( emissiveSample.emission );
|
|
563
|
+
result.distance.assign( emissiveSample.distance );
|
|
570
564
|
|
|
571
|
-
|
|
565
|
+
// Check geometric validity
|
|
566
|
+
const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
|
|
572
567
|
|
|
573
|
-
|
|
574
|
-
const rayOffset = calculateRayOffsetFn( hitPoint, normal, material );
|
|
575
|
-
const rayOrigin = hitPoint.add( rayOffset );
|
|
568
|
+
If( NoL.greaterThan( 0.0 ), () => {
|
|
576
569
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
570
|
+
// Calculate ray offset for shadow ray
|
|
571
|
+
const rayOffset = calculateRayOffsetFn( hitPoint, normal, material );
|
|
572
|
+
const rayOrigin = hitPoint.add( rayOffset );
|
|
580
573
|
|
|
581
|
-
|
|
574
|
+
// Trace shadow ray
|
|
575
|
+
const shadowDist = emissiveSample.distance.sub( 0.001 );
|
|
576
|
+
const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist );
|
|
582
577
|
|
|
583
|
-
|
|
584
|
-
// would otherwise run twice with identical inputs).
|
|
585
|
-
const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, emissiveSample.direction, material ) );
|
|
586
|
-
const brdfValue = evaluateMaterialResponseFromDots( material, dots );
|
|
587
|
-
const brdfPdf = calculateMaterialPDFFromDots( material, dots );
|
|
578
|
+
If( visibility.greaterThan( 0.0 ), () => {
|
|
588
579
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
);
|
|
580
|
+
// Share H + dot products between BRDF eval and PDF (computeDotProducts
|
|
581
|
+
// would otherwise run twice with identical inputs).
|
|
582
|
+
const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, emissiveSample.direction, material ) );
|
|
583
|
+
const brdfValue = evaluateMaterialResponseFromDots( material, dots );
|
|
584
|
+
const brdfPdf = calculateMaterialPDFFromDots( material, dots );
|
|
595
585
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
586
|
+
// MIS weight: balance light sampling vs BRDF sampling
|
|
587
|
+
const misWeight = select(
|
|
588
|
+
brdfPdf.greaterThan( 0.0 ),
|
|
589
|
+
powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: brdfPdf } ),
|
|
590
|
+
float( 1.0 )
|
|
591
|
+
);
|
|
602
592
|
|
|
603
|
-
|
|
593
|
+
// MC estimator: Le * brdf * cos_surface / pdf_solidAngle
|
|
594
|
+
// emissiveBoost is an additional user-controlled intensity multiplier
|
|
595
|
+
result.contribution.assign(
|
|
596
|
+
emissiveSample.emission.mul( brdfValue ).mul( NoL )
|
|
597
|
+
.div( emissiveSample.pdf ).mul( visibility ).mul( emissiveBoost ).mul( misWeight )
|
|
598
|
+
);
|
|
604
599
|
|
|
605
600
|
} );
|
|
606
601
|
|
|
@@ -616,6 +611,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
|
616
611
|
export const calculateEmissiveTriangleContribution = Fn( ( [
|
|
617
612
|
hitPoint, normal, viewDir, material,
|
|
618
613
|
bounceIndex, rngState,
|
|
614
|
+
pixelCoord, resolution, frame, dimBase,
|
|
619
615
|
emissiveBoost,
|
|
620
616
|
emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
|
|
621
617
|
triangleBuffer,
|
|
@@ -626,6 +622,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
|
|
|
626
622
|
const result = EmissiveContributionResult.wrap( calculateEmissiveTriangleContributionDebug(
|
|
627
623
|
hitPoint, normal, viewDir, material,
|
|
628
624
|
bounceIndex, rngState,
|
|
625
|
+
pixelCoord, resolution, frame, dimBase,
|
|
629
626
|
emissiveBoost,
|
|
630
627
|
emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
|
|
631
628
|
triangleBuffer,
|