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.
@@ -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 centerLighting = textureLoad( readTex, coord ).xyz;
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
- const vSample = textureLoad( varTex, coord );
237
- const variance = max( vSample.z, vSample.w );
238
- const sigmaL = phiLuminance.mul( sqrt( max( variance, float( 0.0 ) ) ) )
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 sLighting = textureLoad( readTex, sCoord ).xyz;
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
- textureStore( writeStorageTex, uvec2( uint( gx ), uint( gy ) ), vec4( output, 1.0 ) ).toWriteOnly();
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
 
@@ -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
- frame: this.frame,
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: this.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
- this.lastRenderMode = - 1;
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
- this._onRenderModeChanged( this.pendingRenderMode );
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 =====
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fn,
3
3
  vec3,
4
- float,
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 } 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';
18
- import { evaluateLayeredBRDF } from './MaterialEvaluation.js';
19
- import { RandomValue } from './Random.js';
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
- // Calculate sampling weights based on material properties
42
- const specularWeight = float( 1.0 ).sub( baseRoughness ).mul( float( 0.5 ).add( float( 0.5 ).mul( material.metalness ) ) ).toVar();
43
- const clearcoatWeight = material.clearcoat.mul( float( 1.0 ).sub( clearcoatRoughness ) ).toVar();
44
- const diffuseWeight = float( 1.0 ).sub( specularWeight ).mul( float( 1.0 ).sub( material.metalness ) ).toVar();
45
-
46
- // Normalize weights
47
- const total = specularWeight.add( clearcoatWeight ).add( diffuseWeight );
48
- specularWeight.divAssign( total );
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 = RandomValue( rngState );
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
- H.assign( ImportanceSampleGGX( { N, roughness: clearcoatRoughness, Xi: randomSample } ) );
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
- H.assign( ImportanceSampleGGX( { N, roughness: baseRoughness, Xi: randomSample } ) );
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
- // 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.
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
- const brdf = evaluateLayeredBRDF( dots, material );
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, bool as tslBool } from 'three/tsl';
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 { RandomValue } from './Random.js';
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 = RandomValue( rngState );
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
- // Generate random numbers for sampling
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
- // Skip for very rough diffuse surfaces on secondary bounces
551
- const skip = bounceIndex.greaterThan( int( 1 ) ).and( material.roughness.greaterThan( 0.9 ) ).and( material.metalness.lessThan( 0.1 ) );
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
- If( skip.not(), () => {
554
-
555
- // Sample emissive triangle (CDF importance-weighted)
556
- const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
557
- hitPoint, normal, rngState,
558
- emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
559
- triangleBuffer,
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
- result.hasEmissive.assign( true );
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
- // Check geometric validity
569
- const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
561
+ result.hasEmissive.assign( true );
562
+ result.emissionOnly.assign( emissiveSample.emission );
563
+ result.distance.assign( emissiveSample.distance );
570
564
 
571
- If( NoL.greaterThan( 0.0 ), () => {
565
+ // Check geometric validity
566
+ const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
572
567
 
573
- // Calculate ray offset for shadow ray
574
- const rayOffset = calculateRayOffsetFn( hitPoint, normal, material );
575
- const rayOrigin = hitPoint.add( rayOffset );
568
+ If( NoL.greaterThan( 0.0 ), () => {
576
569
 
577
- // Trace shadow ray
578
- const shadowDist = emissiveSample.distance.sub( 0.001 );
579
- const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist );
570
+ // Calculate ray offset for shadow ray
571
+ const rayOffset = calculateRayOffsetFn( hitPoint, normal, material );
572
+ const rayOrigin = hitPoint.add( rayOffset );
580
573
 
581
- If( visibility.greaterThan( 0.0 ), () => {
574
+ // Trace shadow ray
575
+ const shadowDist = emissiveSample.distance.sub( 0.001 );
576
+ const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist );
582
577
 
583
- // Share H + dot products between BRDF eval and PDF (computeDotProducts
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
- // MIS weight: balance light sampling vs BRDF sampling
590
- const misWeight = select(
591
- brdfPdf.greaterThan( 0.0 ),
592
- powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: brdfPdf } ),
593
- float( 1.0 )
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
- // MC estimator: Le * brdf * cos_surface / pdf_solidAngle
597
- // emissiveBoost is an additional user-controlled intensity multiplier
598
- result.contribution.assign(
599
- emissiveSample.emission.mul( brdfValue ).mul( NoL )
600
- .div( emissiveSample.pdf ).mul( visibility ).mul( emissiveBoost ).mul( misWeight )
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,