rayzee 7.20.2 → 7.20.3

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.
@@ -7,398 +7,79 @@
7
7
  * strategy — it is handled via deterministic NEE in LightsSampling.js.
8
8
  *
9
9
  * Contains:
10
- * - calculateTransmissionPDF — transmission PDF for MIS
11
- * - calculateClearcoatPDF — clearcoat PDF for MIS
12
- * - computeSamplingInfo — compute importance weights for material strategies
13
- * - selectSamplingStrategy — CDF-based strategy selection (specular, diffuse, transmission, clearcoat)
14
10
  * - calculateIndirectLighting — material multi-strategy MIS for bounce direction
15
11
  */
16
12
 
17
- import {
18
- Fn,
19
- float,
20
- vec2,
21
- vec3,
22
- int,
23
- bool as tslBool,
24
- max,
25
- abs,
26
- sqrt,
27
- dot,
28
- If,
29
- select,
30
- } from 'three/tsl';
13
+ import { Fn, float, vec3, int, max, dot, If, select } from 'three/tsl';
31
14
 
32
- import {
33
- IndirectLightingResult,
34
- } from './LightsCore.js';
35
- import {
36
- SamplingStrategyWeights,
37
- } from './Struct.js';
38
- import {
39
- PI_INV,
40
- EPSILON,
41
- MIN_PDF,
42
- } from './Common.js';
43
- import { DistributionGGX, calculateVNDFPDF, specularVNDFPDFForDir } from './MaterialProperties.js';
44
- import { evaluateMaterialResponse } from './MaterialEvaluation.js';
45
- import { RandomValue } from './Random.js';
46
- import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
15
+ import { IndirectLightingResult } from './LightsCore.js';
16
+ import { MIN_PDF } from './Common.js';
17
+ import { getRandomSample2D } from './Random.js';
47
18
  import { cosineWeightedSample } from './MaterialSampling.js';
48
19
 
49
- // =============================================================================
50
- // PDF Calculation Helpers
51
- // =============================================================================
52
-
53
- // Transmission PDF (Walter et al. 2007)
54
- export const calculateTransmissionPDF = Fn( ( [ V, L, N, ior, roughness, entering ] ) => {
55
-
56
- // eta is the relative IOR: eta_transmitted / eta_incident
57
- // When entering: air(1.0) -> material(ior), so eta = ior
58
- // When exiting: material(ior) -> air(1.0), so eta = 1.0/ior
59
- const eta = select( entering, ior, float( 1.0 ).div( ior ) ).toVar();
60
-
61
- // Transmission half-vector formula
62
- const H_raw = V.add( L.mul( eta ) ).toVar();
63
- const lenSq = dot( H_raw, H_raw ).toVar();
64
- const H = select( lenSq.greaterThan( EPSILON ), H_raw.div( sqrt( lenSq ) ), N ).toVar();
65
-
66
- // Ensure H points into the correct hemisphere
67
- If( dot( H, N ).lessThan( 0.0 ), () => {
68
-
69
- H.assign( H.negate() );
70
-
71
- } );
72
-
73
- const VoH = abs( dot( V, H ) );
74
- const LoH = abs( dot( L, H ) ).toVar();
75
- const NoH = abs( dot( N, H ) ).toVar();
76
-
77
- // GGX distribution
78
- const D = DistributionGGX( NoH, roughness );
79
-
80
- // Jacobian for transmission
81
- const denom_inner = VoH.add( LoH.mul( eta ) ).toVar();
82
- const denom = denom_inner.mul( denom_inner );
83
- const jacobian = LoH.mul( eta ).mul( eta ).div( max( denom, EPSILON ) );
84
-
85
- return D.mul( NoH ).mul( jacobian );
86
-
87
- } );
88
-
89
- // Clearcoat PDF
90
- export const calculateClearcoatPDF = Fn( ( [ V, L, N, clearcoatRoughness ] ) => {
91
-
92
- const H_raw = V.add( L ).toVar();
93
- const lenSq = dot( H_raw, H_raw ).toVar();
94
- const H = select( lenSq.greaterThan( EPSILON ), H_raw.div( sqrt( lenSq ) ), N );
95
-
96
- const NoH = max( dot( N, H ), 0.0 );
97
- const NoV = max( dot( N, V ), 0.0 );
98
-
99
- return calculateVNDFPDF( NoH, NoV, clearcoatRoughness );
100
-
101
- } );
102
-
103
- // =============================================================================
104
- // Sampling Strategy Computation
105
- // =============================================================================
106
-
107
- // Compute normalized strategy weights based on importance info
108
- export const computeSamplingInfo = Fn( ( [
109
- samplingInfo,
110
- ] ) => {
111
-
112
- // Material-only strategies (env handled via deterministic NEE in direct lighting)
113
- const specularW = samplingInfo.specularImportance.toVar();
114
- const useSpecular = specularW.greaterThan( 0.001 ).toVar();
115
-
116
- const diffuseW = samplingInfo.diffuseImportance.toVar();
117
- const useDiffuse = diffuseW.greaterThan( 0.001 ).toVar();
118
-
119
- const transmissionW = samplingInfo.transmissionImportance.toVar();
120
- const useTransmission = transmissionW.greaterThan( 0.001 ).toVar();
121
-
122
- const clearcoatW = samplingInfo.clearcoatImportance.toVar();
123
- const useClearcoat = clearcoatW.greaterThan( 0.001 ).toVar();
124
-
125
- const totalW = specularW.add( diffuseW ).add( transmissionW ).add( clearcoatW ).toVar();
126
-
127
- If( totalW.lessThan( 0.001 ), () => {
128
-
129
- specularW.assign( 0.0 );
130
- diffuseW.assign( 1.0 );
131
- transmissionW.assign( 0.0 );
132
- clearcoatW.assign( 0.0 );
133
- totalW.assign( 1.0 );
134
- useSpecular.assign( tslBool( false ) );
135
- useDiffuse.assign( tslBool( true ) );
136
- useTransmission.assign( tslBool( false ) );
137
- useClearcoat.assign( tslBool( false ) );
138
-
139
- } ).Else( () => {
140
-
141
- const invTotal = float( 1.0 ).div( totalW ).toVar();
142
- specularW.mulAssign( invTotal );
143
- diffuseW.mulAssign( invTotal );
144
- transmissionW.mulAssign( invTotal );
145
- clearcoatW.mulAssign( invTotal );
146
- totalW.assign( 1.0 );
147
-
148
- } );
149
-
150
- return SamplingStrategyWeights( {
151
- envWeight: float( 0.0 ),
152
- specularWeight: specularW,
153
- diffuseWeight: diffuseW,
154
- transmissionWeight: transmissionW,
155
- clearcoatWeight: clearcoatW,
156
- totalWeight: totalW,
157
- useEnv: tslBool( false ),
158
- useSpecular,
159
- useDiffuse,
160
- useTransmission,
161
- useClearcoat,
162
- } );
163
-
164
- } );
165
-
166
- // =============================================================================
167
- // Strategy Selection via Cumulative Distribution
168
- // =============================================================================
169
-
170
- // Strategy IDs: 1=specular, 2=diffuse, 3=transmission, 4=clearcoat
171
- // (env removed — handled via deterministic NEE in direct lighting)
172
- export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
173
-
174
- const selectedStrategy = int( 2 ).toVar(); // Default: diffuse
175
-
176
- const cumulative = float( 0.0 ).toVar();
177
- const found = tslBool( false ).toVar();
178
-
179
- If( weights.useSpecular.and( found.not() ), () => {
180
-
181
- cumulative.addAssign( weights.specularWeight );
182
-
183
- If( randomValue.lessThan( cumulative ), () => {
184
-
185
- selectedStrategy.assign( 1 );
186
- found.assign( tslBool( true ) );
187
-
188
- } );
189
-
190
- } );
191
-
192
- If( weights.useDiffuse.and( found.not() ), () => {
193
20
 
194
- cumulative.addAssign( weights.diffuseWeight );
195
21
 
196
- If( randomValue.lessThan( cumulative ), () => {
197
22
 
198
- selectedStrategy.assign( 2 );
199
- found.assign( tslBool( true ) );
200
23
 
201
- } );
202
-
203
- } );
204
-
205
- If( weights.useTransmission.and( found.not() ), () => {
206
-
207
- cumulative.addAssign( weights.transmissionWeight );
208
-
209
- If( randomValue.lessThan( cumulative ), () => {
210
-
211
- selectedStrategy.assign( 3 );
212
- found.assign( tslBool( true ) );
213
-
214
- } );
215
-
216
- } );
217
-
218
- If( weights.useClearcoat.and( found.not() ), () => {
219
-
220
- selectedStrategy.assign( 4 );
221
-
222
- } );
223
-
224
- return selectedStrategy;
225
-
226
- } );
227
-
228
- // =============================================================================
229
- // Cosine Weighted PDF helper
230
- // =============================================================================
231
-
232
- const cosineWeightedPDF = Fn( ( [ NoL ] ) => {
233
-
234
- return max( NoL, 0.0 ).mul( PI_INV );
235
-
236
- } );
237
24
 
238
25
  // =============================================================================
239
26
  // Indirect Lighting Calculation
240
27
  // =============================================================================
241
28
 
242
29
  export const calculateIndirectLighting = Fn( ( [
243
- V, N, material,
30
+ N, material,
244
31
  // brdfSample fields (DirectionSample)
245
32
  brdfSampleDirection, brdfSamplePdf, brdfSampleValue,
33
+ // DirectionSample carries these so refraction throughput does not need a second sampler run.
34
+ isTransmission, brdfSampleColorWeight,
246
35
  rngState,
247
- samplingInfo,
36
+ pixelCoord, resolution, frame, dimBase,
248
37
  ] ) => {
249
38
 
250
39
  // Initialize result
251
40
  const r_direction = vec3( 0.0 ).toVar();
252
41
  const r_throughput = vec3( 0.0 ).toVar();
253
- const r_misWeight = float( 0.0 ).toVar();
254
- const r_pdf = float( 0.0 ).toVar();
255
42
  const r_combinedPdf = float( 0.0 ).toVar();
256
43
 
257
- // Validate input sampling info
258
- const validInput = samplingInfo.diffuseImportance.greaterThanEqual( 0.0 )
259
- .and( samplingInfo.specularImportance.greaterThanEqual( 0.0 ) )
260
- .and( samplingInfo.transmissionImportance.greaterThanEqual( 0.0 ) )
261
- .and( samplingInfo.clearcoatImportance.greaterThanEqual( 0.0 ) );
44
+ // A non-finite pdf would poison the accumulation buffer permanently, so fall back to a
45
+ // cosine bounce rather than dividing by it.
46
+ If( brdfSamplePdf.greaterThan( 0.0 ).not(), () => {
262
47
 
263
- If( validInput.not(), () => {
264
-
265
- // Fallback to diffuse sampling
266
- const r1_fb = RandomValue( rngState ).toVar();
267
- const r2_fb = RandomValue( rngState ).toVar();
268
- const sampleRand = vec2( r1_fb, r2_fb );
48
+ const sampleRand = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 8 ) ), rngState, resolution, frame );
269
49
  r_direction.assign( cosineWeightedSample( N, sampleRand ) );
270
50
  r_throughput.assign( material.color.xyz );
271
- r_misWeight.assign( 1.0 );
272
- r_pdf.assign( 1.0 );
51
+ r_combinedPdf.assign( 1.0 );
273
52
 
274
53
  } ).Else( () => {
275
54
 
276
- // Use corrected sampling info
277
- const weights = SamplingStrategyWeights.wrap( computeSamplingInfo(
278
- samplingInfo,
279
- ).toVar() );
280
-
281
- const selectionRand = RandomValue( rngState ).toVar();
282
- const r1 = RandomValue( rngState ).toVar();
283
- const r2 = RandomValue( rngState ).toVar();
284
- const sampleRand = vec2( r1, r2 ).toVar();
285
-
286
- // Strategy selection
287
- const selectedStrategy = selectSamplingStrategy( weights, selectionRand ).toVar();
288
-
289
- const sampleDir = vec3( 0.0 ).toVar();
290
- const samplePdf = float( 0.0 ).toVar();
291
- const sampleBrdfValue = vec3( 0.0 ).toVar();
292
- // Transmission lobe uses this weight instead of a reflection-BRDF eval, which is
293
- // invalid for the below-surface refraction direction.
294
- const transColorWeight = vec3( 1.0 ).toVar();
295
-
296
- // Execute selected strategy (chained If/ElseIf/Else for exclusive branches)
297
- // Environment removed — handled via deterministic NEE in direct lighting
55
+ // generateSampledDirection IS the sampler — this used to re-select a strategy on top of
56
+ // it, and its strategies 1 and 4 both just returned brdfSampleDirection. So a sample that
57
+ // the inner selection had drawn from the diffuse or sheen lobe was reported to MIS as a
58
+ // specular-VNDF draw, making combinedPdf something no strategy actually sampled from.
59
+ // Since prevBouncePdf is that value, and the NEE sites evaluate a mixture, the two MIS
60
+ // strategies' weights stopped summing to 1.
61
+ //
62
+ // brdfSamplePdf is now the true mixture density (calculateBSDFSamplingPDF), so the
63
+ // estimator is just f·NoL/pdf and the MIS pairs share one function by construction.
64
+ const NoL = max( dot( N, brdfSampleDirection ), 0.0 ).toVar();
65
+ const pdf = max( brdfSamplePdf, MIN_PDF ).toVar();
66
+
67
+ r_direction.assign( brdfSampleDirection );
68
+ r_combinedPdf.assign( pdf );
69
+
70
+ // The reflection BRDF eval is invalid below the surface, so refraction carries the
71
+ // sampler's own tint (material.color × colorWeight), mirroring handleTransmission.
72
+ r_throughput.assign( select(
73
+ isTransmission,
74
+ material.color.xyz.mul( brdfSampleColorWeight ),
75
+ brdfSampleValue.mul( NoL ).div( pdf ),
76
+ ) );
298
77
 
299
- // Strategy 1: Specular
300
- If( selectedStrategy.equal( int( 1 ) ), () => {
301
-
302
- sampleDir.assign( brdfSampleDirection );
303
- samplePdf.assign( brdfSamplePdf );
304
- sampleBrdfValue.assign( brdfSampleValue );
305
-
306
- } ).ElseIf( selectedStrategy.equal( int( 2 ) ), () => {
307
-
308
- // Strategy 2: Diffuse
309
- sampleDir.assign( cosineWeightedSample( N, sampleRand ) );
310
- samplePdf.assign( cosineWeightedPDF( max( dot( N, sampleDir ), 0.0 ) ) );
311
- sampleBrdfValue.assign( evaluateMaterialResponse( V, sampleDir, N, material ) );
312
-
313
- } ).ElseIf( selectedStrategy.equal( int( 3 ) ), () => {
314
-
315
- // Strategy 3: Transmission
316
- const entering = dot( V, N ).greaterThan( 0.0 );
317
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
318
- V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState, float( 0.0 )
319
- ).toVar() );
320
- sampleDir.assign( mtResult.direction );
321
- samplePdf.assign( mtResult.pdf );
322
- // evaluateMaterialResponse is reflection-only and returns a non-physical value for
323
- // the below-surface refraction direction (its dots are floored to 0.001). Carry the
324
- // sampler's spectral tint and apply an energy-consistent transmission throughput
325
- // below (mirrors handleTransmission's material.color × colorWeight convention).
326
- transColorWeight.assign( mtResult.colorWeight );
327
-
328
- } ).Else( () => {
329
-
330
- // Strategy 4: Clearcoat (fallback)
331
- sampleDir.assign( brdfSampleDirection );
332
- samplePdf.assign( brdfSamplePdf );
333
- sampleBrdfValue.assign( brdfSampleValue );
334
-
335
- } );
336
-
337
- const NoL = max( dot( N, sampleDir ), 0.0 ).toVar();
338
-
339
- // Calculate combined PDF for MIS (material strategies only)
340
- const combinedPdf = float( 0.0 ).toVar();
341
-
342
- If( weights.useSpecular, () => {
343
-
344
- // Evaluate specular PDF at sampleDir (not brdfSampleDirection which may differ);
345
- // auto-selects iso/aniso to stay consistent with the BRDF sampler.
346
- const specPdfAtSampleDir = specularVNDFPDFForDir( material, N, V, sampleDir );
347
- combinedPdf.addAssign( weights.specularWeight.mul( specPdfAtSampleDir ) );
348
-
349
- } );
350
-
351
- If( weights.useDiffuse, () => {
352
-
353
- combinedPdf.addAssign( weights.diffuseWeight.mul( cosineWeightedPDF( NoL ) ) );
354
-
355
- } );
356
-
357
- If( weights.useTransmission.and( material.transmission.greaterThan( 0.0 ) ), () => {
358
-
359
- // Calculate transmission PDF for this direction
360
- const entering = dot( V, N ).greaterThan( 0.0 );
361
- combinedPdf.addAssign( weights.transmissionWeight.mul(
362
- calculateTransmissionPDF( V, sampleDir, N, material.ior, material.roughness, entering )
363
- ) );
364
-
365
- } );
366
-
367
- If( weights.useClearcoat.and( material.clearcoat.greaterThan( 0.0 ) ), () => {
368
-
369
- // Calculate clearcoat PDF for this direction
370
- combinedPdf.addAssign( weights.clearcoatWeight.mul(
371
- calculateClearcoatPDF( V, sampleDir, N, material.clearcoatRoughness )
372
- ) );
373
-
374
- } );
375
-
376
- // Ensure valid PDFs
377
- samplePdf.assign( max( samplePdf, MIN_PDF ) );
378
- combinedPdf.assign( max( combinedPdf, MIN_PDF ) );
379
-
380
- // MIS weight calculation
381
- const misWeight = samplePdf.div( combinedPdf ).toVar();
382
-
383
- // Reflection lobes: f·NoL·mis/pdf. Transmission lobe: the sampler's energy-consistent
384
- // weight (material tint × dispersion colorWeight × mis) — the reflection BRDF value is
385
- // invalid below the surface, so it is not used for transmission.
386
- const reflThroughput = sampleBrdfValue.mul( NoL ).mul( misWeight ).div( samplePdf );
387
- const transThroughput = material.color.xyz.mul( transColorWeight ).mul( misWeight );
388
-
389
- r_direction.assign( sampleDir );
390
- r_throughput.assign( select( selectedStrategy.equal( int( 3 ) ), transThroughput, reflThroughput ) );
391
- r_misWeight.assign( misWeight );
392
- r_pdf.assign( samplePdf );
393
- r_combinedPdf.assign( combinedPdf );
394
-
395
- } ); // End validInput check
78
+ } );
396
79
 
397
80
  return IndirectLightingResult( {
398
81
  direction: r_direction,
399
82
  throughput: r_throughput,
400
- misWeight: r_misWeight,
401
- pdf: r_pdf,
402
83
  combinedPdf: r_combinedPdf,
403
84
  } );
404
85
 
@@ -72,8 +72,8 @@ import {
72
72
 
73
73
  import { traverseBVHShadow } from './BVHTraversal.js';
74
74
  import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
75
- import { calculateVNDFPDF, calculateVNDFPDFAniso, computeAnisoAlphas } from './MaterialProperties.js';
76
- import { RandomValue } from './Random.js';
75
+ import { calculateBSDFSamplingPDFFromMaterial } from './MaterialProperties.js';
76
+ import { getRandomSample1D, getRandomSample2D, SAMPLER_DIM_AUX_BASE } from './Random.js';
77
77
  import {
78
78
  selectOptimalMISStrategy,
79
79
  PI,
@@ -340,6 +340,7 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
340
340
  // unbiased; PDF = winnerImportance / totalWeight, identical to the CDF form.
341
341
  export const sampleLightWithImportance = Fn( ( [
342
342
  rayOrigin, normal, material, randomSeed, bounceIndex, rngState,
343
+ pixelCoord, resolution, frame, dimBase,
343
344
  // Light buffers + counts
344
345
  directionalLightsBuffer, numDirectionalLights,
345
346
  areaLightsBuffer, numAreaLights,
@@ -380,7 +381,9 @@ export const sampleLightWithImportance = Fn( ( [
380
381
  const importance = calculateDirectionalLightImportance( light, normal, material, bounceIndex ).toVar();
381
382
  totalWeight.addAssign( importance );
382
383
  If( importance.greaterThan( 0.0 ).and(
383
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
384
+ getRandomSample1D( pixelCoord, int( 0 ),
385
+ int( SAMPLER_DIM_AUX_BASE + 64 + 0 * 16 ).add( i ), rngState, resolution, frame
386
+ ).mul( totalWeight ).lessThan( importance )
384
387
  ), () => {
385
388
 
386
389
  selectedType.assign( 0 );
@@ -405,7 +408,9 @@ export const sampleLightWithImportance = Fn( ( [
405
408
  const importance = select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ).toVar();
406
409
  totalWeight.addAssign( importance );
407
410
  If( importance.greaterThan( 0.0 ).and(
408
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
411
+ getRandomSample1D( pixelCoord, int( 0 ),
412
+ int( SAMPLER_DIM_AUX_BASE + 64 + 1 * 16 ).add( i ), rngState, resolution, frame
413
+ ).mul( totalWeight ).lessThan( importance )
409
414
  ), () => {
410
415
 
411
416
  selectedType.assign( 1 );
@@ -430,7 +435,9 @@ export const sampleLightWithImportance = Fn( ( [
430
435
  const importance = calculatePointLightImportance( light, rayOrigin, normal, material ).toVar();
431
436
  totalWeight.addAssign( importance );
432
437
  If( importance.greaterThan( 0.0 ).and(
433
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
438
+ getRandomSample1D( pixelCoord, int( 0 ),
439
+ int( SAMPLER_DIM_AUX_BASE + 64 + 2 * 16 ).add( i ), rngState, resolution, frame
440
+ ).mul( totalWeight ).lessThan( importance )
434
441
  ), () => {
435
442
 
436
443
  selectedType.assign( 2 );
@@ -455,7 +462,9 @@ export const sampleLightWithImportance = Fn( ( [
455
462
  const importance = calculateSpotLightImportance( light, rayOrigin, normal, material ).toVar();
456
463
  totalWeight.addAssign( importance );
457
464
  If( importance.greaterThan( 0.0 ).and(
458
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
465
+ getRandomSample1D( pixelCoord, int( 0 ),
466
+ int( SAMPLER_DIM_AUX_BASE + 64 + 3 * 16 ).add( i ), rngState, resolution, frame
467
+ ).mul( totalWeight ).lessThan( importance )
459
468
  ), () => {
460
469
 
461
470
  selectedType.assign( 3 );
@@ -518,7 +527,7 @@ export const sampleLightWithImportance = Fn( ( [
518
527
 
519
528
  If( light.intensity.greaterThan( 0.0 ), () => {
520
529
 
521
- const uv = vec2( randomSeed.y, RandomValue( rngState ) ).toVar();
530
+ const uv = vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ).toVar();
522
531
  const areaSample = LightSample.wrap( sampleRectAreaLight( light, rayOrigin, uv, lightSelectionPdf ) );
523
532
  r_valid.assign( areaSample.valid );
524
533
  r_direction.assign( areaSample.direction );
@@ -607,7 +616,7 @@ export const sampleLightWithImportance = Fn( ( [
607
616
  const cosHalfAngle = cos( light.angle.mul( 0.5 ) ).toVar();
608
617
  const cosTheta = mix( cosHalfAngle, float( 1.0 ), randomSeed.y ).toVar();
609
618
  const sinTheta = sqrt( max( float( 0.0 ), float( 1.0 ).sub( cosTheta.mul( cosTheta ) ) ) ).toVar();
610
- const phi = float( TWO_PI ).mul( RandomValue( rngState ) ).toVar();
619
+ const phi = float( TWO_PI ).mul( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 14 ) ), rngState, resolution, frame ) ).toVar();
611
620
 
612
621
  const w = normalize( light.direction ).toVar();
613
622
  const u = normalize( cross(
@@ -639,7 +648,7 @@ export const sampleLightWithImportance = Fn( ( [
639
648
  If( selectedType.equal( int( 1 ) ).and( selectedIdx.greaterThanEqual( int( 0 ) ) ), () => {
640
649
 
641
650
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, selectedIdx ) );
642
- const uv = vec2( randomSeed.y, RandomValue( rngState ) ).toVar();
651
+ const uv = vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ).toVar();
643
652
  const areaSample = LightSample.wrap( sampleRectAreaLight( light, rayOrigin, uv, pdf ) );
644
653
  r_valid.assign( areaSample.valid );
645
654
  r_direction.assign( areaSample.direction );
@@ -700,66 +709,14 @@ export const sampleLightWithImportance = Fn( ( [
700
709
  // PDF computation given precomputed dot products. Use this when the caller
701
710
  // already has dots from a paired evaluateMaterialResponseFromDots invocation
702
711
  // to avoid recomputing H + dots.
712
+ // BSDF-sampling density for MIS — must be the exact function the sampler draws from, or the two
713
+ // strategies' weights stop summing to 1.
703
714
  export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
704
715
 
705
- const NoV = dots.NoV;
706
- const NoL = dots.NoL.toVar();
707
- const NoH = dots.NoH;
708
-
709
- // Calculate lobe weights
710
- const diffuseWeight = float( 1.0 ).sub( material.metalness ).mul(
711
- float( 1.0 ).sub( material.transmission )
712
- ).toVar();
713
-
714
- const specularWeight = float( 1.0 ).sub(
715
- diffuseWeight.mul( float( 1.0 ).sub( material.metalness ) )
716
- ).toVar();
717
-
718
- const totalWeight = diffuseWeight.add( specularWeight ).toVar();
719
-
720
- const pdf = float( 0.0 ).toVar();
721
-
722
- If( totalWeight.greaterThan( 0.0 ), () => {
723
-
724
- // Guard division
725
- const invTotalWeight = float( 1.0 ).div( max( totalWeight, 1e-10 ) ).toVar();
726
- diffuseWeight.mulAssign( invTotalWeight );
727
- specularWeight.mulAssign( invTotalWeight );
728
-
729
- // Diffuse PDF (cosine-weighted hemisphere)
730
- If( diffuseWeight.greaterThan( 0.0 ).and( NoL.greaterThan( 0.0 ) ), () => {
731
-
732
- pdf.addAssign( diffuseWeight.mul( NoL ).mul( PI_INV ) );
733
-
734
- } );
735
-
736
- // Specular PDF (VNDF sampling used in path tracer)
737
- If( specularWeight.greaterThan( 0.0 ).and( NoL.greaterThan( 0.0 ) ), () => {
738
-
739
- const roughness = max( material.roughness, 0.02 );
740
- const specPdf = float( 0.0 ).toVar();
741
- If( material.anisotropy.greaterThan( 0.0 ), () => {
742
-
743
- const a = computeAnisoAlphas( material.roughness, material.anisotropy );
744
- specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
745
-
746
- } ).Else( () => {
747
-
748
- specPdf.assign( calculateVNDFPDF( NoH, NoV, roughness ) );
749
-
750
- } );
751
- pdf.addAssign( specularWeight.mul( specPdf ) );
752
-
753
- } );
754
-
755
- } );
756
-
757
- return max( pdf, 1e-8 );
716
+ return max( calculateBSDFSamplingPDFFromMaterial( material, dots ), 1e-8 );
758
717
 
759
718
  } );
760
719
 
761
- // Wrapper that computes dots internally. Use this when the caller doesn't
762
- // already have dots; otherwise prefer calculateMaterialPDFFromDots.
763
720
  export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material ] ) => {
764
721
 
765
722
  const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
@@ -861,6 +818,9 @@ export const calculateDirectLightingUnified = Fn( ( [
861
818
  brdfSampleDirection, brdfSamplePdf, brdfSampleValue,
862
819
  // Tracing context
863
820
  bounceIndex, rngState,
821
+ // Sampler context — NEE's continuous variates come off the sampler keyed on
822
+ // (pixel, dimension, frame). dimBase is this bounce's block; see Random.js budget.
823
+ pixelCoord, resolution, frame, dimBase,
864
824
  // Light data
865
825
  directionalLightsBuffer, numDirectionalLights,
866
826
  areaLightsBuffer, numAreaLights,
@@ -937,11 +897,8 @@ export const calculateDirectLightingUnified = Fn( ( [
937
897
 
938
898
  } );
939
899
 
940
- const stratRand1 = RandomValue( rngState ).toVar();
941
- const stratRand2 = RandomValue( rngState ).toVar();
942
-
943
900
  // Determine sampling technique: stochastic {lights, BRDF}
944
- const rand = stratRand1;
901
+ const rand = getRandomSample1D( pixelCoord, int( 0 ), dimBase, rngState, resolution, frame ).toVar();
945
902
  const sampleLights = tslBool( false ).toVar();
946
903
  const sampleBRDF = tslBool( false ).toVar();
947
904
 
@@ -973,9 +930,10 @@ export const calculateDirectLightingUnified = Fn( ( [
973
930
  If( sampleLights, () => {
974
931
 
975
932
  // Importance-weighted light sampling
976
- const lightRandom = vec2( stratRand2, RandomValue( rngState ) ).toVar();
933
+ const lightRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
977
934
  const lightSample = LightSample.wrap( sampleLightWithImportance(
978
935
  rayOrigin, hitNormal, material, lightRandom, bounceIndex, rngState,
936
+ pixelCoord, resolution, frame, dimBase,
979
937
  directionalLightsBuffer, numDirectionalLights,
980
938
  areaLightsBuffer, numAreaLights,
981
939
  pointLightsBuffer, numPointLights,
@@ -1172,9 +1130,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1172
1130
 
1173
1131
  If( enableEnvironmentLight, () => {
1174
1132
 
1175
- const env_r1 = RandomValue( rngState ).toVar();
1176
- const env_r2 = RandomValue( rngState ).toVar();
1177
- const envRandom = vec2( env_r1, env_r2 ).toVar();
1133
+ const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
1178
1134
  const envColor = vec3( 0.0 ).toVar();
1179
1135
 
1180
1136
  // Sample direction + PDF + color from importance-sampled environment