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.
@@ -4,14 +4,14 @@
4
4
  * Pure TSL: Fn(), If(), Loop(), .toVar(), .assign() — NO wgslFn()
5
5
  *
6
6
  * Direct lighting combines:
7
- * - Stochastic discrete light / BRDF selection (area, point, spot, directional)
7
+ * - Discrete light NEE + BSDF-hit, both evaluated per vertex (area, point, spot, directional)
8
8
  * - Deterministic environment NEE (always runs, two-strategy Veach MIS with implicit miss)
9
9
  *
10
10
  * Contains:
11
11
  * - sampleRectAreaLight — rectangle area light sampling
12
12
  * - sampleSpotLightWithRadius — spot light sampling with radius
13
13
  * - samplePointLightWithAttenuation — point light sampling with attenuation
14
- * - sampleLightWithImportance — importance-weighted light selection (3-pass)
14
+ * - sampleLightWithImportance — importance-weighted light selection (reservoir)
15
15
  * - calculateMaterialPDF — material PDF for MIS
16
16
  * - calculateDirectLightingUnified — unified direct lighting (main entry)
17
17
  */
@@ -61,7 +61,7 @@ import {
61
61
  sphQuadSolidAngle,
62
62
  } from './LightsCore.js';
63
63
 
64
- import { MISStrategy, DotProducts, DirectLightingDual } from './Struct.js';
64
+ import { DotProducts, DirectLightingDual } from './Struct.js';
65
65
  import {
66
66
  calculateDirectionalLightImportance,
67
67
  estimateLightImportance,
@@ -72,10 +72,9 @@ 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
- selectOptimalMISStrategy,
79
78
  PI,
80
79
  PI_INV,
81
80
  EPSILON,
@@ -223,6 +222,7 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
223
222
  distance: ls_distance,
224
223
  pdf: ls_pdf,
225
224
  lightType: ls_lightType,
225
+ selectionTotalWeight: float( 0.0 ),
226
226
  } );
227
227
 
228
228
  } );
@@ -281,6 +281,7 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
281
281
  distance: ls_distance,
282
282
  pdf: ls_pdf,
283
283
  lightType: ls_lightType,
284
+ selectionTotalWeight: float( 0.0 ),
284
285
  } );
285
286
 
286
287
  } );
@@ -324,6 +325,7 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
324
325
  distance: ls_distance,
325
326
  pdf: ls_pdf,
326
327
  lightType: ls_lightType,
328
+ selectionTotalWeight: float( 0.0 ),
327
329
  } );
328
330
 
329
331
  } );
@@ -340,6 +342,7 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
340
342
  // unbiased; PDF = winnerImportance / totalWeight, identical to the CDF form.
341
343
  export const sampleLightWithImportance = Fn( ( [
342
344
  rayOrigin, normal, material, randomSeed, bounceIndex, rngState,
345
+ pixelCoord, resolution, frame, dimBase,
343
346
  // Light buffers + counts
344
347
  directionalLightsBuffer, numDirectionalLights,
345
348
  areaLightsBuffer, numAreaLights,
@@ -354,12 +357,19 @@ export const sampleLightWithImportance = Fn( ( [
354
357
  const r_distance = float( 0.0 ).toVar();
355
358
  const r_pdf = float( 0.0 ).toVar();
356
359
  const r_lightType = int( LIGHT_TYPE_POINT ).toVar();
360
+ const totalWeight = float( 0.0 ).toVar();
357
361
 
358
362
  const totalLights = numDirectionalLights.add( numAreaLights ).add( numPointLights ).add( numSpotLights ).toVar();
359
363
 
360
364
  If( totalLights.greaterThan( int( 0 ) ), () => {
361
365
 
362
- const totalWeight = float( 0.0 ).toVar();
366
+ // One sampler dimension per light in the scene: the reservoir's replacement tests must
367
+ // be mutually independent, and a per-type block hands light 16 of one type the same
368
+ // variate as light 0 of the next.
369
+ const dimDir = int( SAMPLER_DIM_AUX_BASE + 64 ).toVar();
370
+ const dimArea = dimDir.add( numDirectionalLights ).toVar();
371
+ const dimPoint = dimArea.add( numAreaLights ).toVar();
372
+ const dimSpot = dimPoint.add( numPointLights ).toVar();
363
373
 
364
374
  // Reservoir state: winning light's type/index/importance.
365
375
  const selectedType = int( - 1 ).toVar(); // 0=dir, 1=area, 2=point, 3=spot
@@ -374,20 +384,18 @@ export const sampleLightWithImportance = Fn( ( [
374
384
 
375
385
  Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
376
386
 
377
- If( i.lessThan( int( 16 ) ), () => {
378
-
379
- const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
380
- const importance = calculateDirectionalLightImportance( light, normal, material, bounceIndex ).toVar();
381
- totalWeight.addAssign( importance );
382
- If( importance.greaterThan( 0.0 ).and(
383
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
384
- ), () => {
385
-
386
- selectedType.assign( 0 );
387
- selectedIdx.assign( i );
388
- selectedImportance.assign( importance );
389
-
390
- } );
387
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
388
+ const importance = calculateDirectionalLightImportance( light, normal, material, bounceIndex ).toVar();
389
+ totalWeight.addAssign( importance );
390
+ If( importance.greaterThan( 0.0 ).and(
391
+ getRandomSample1D( pixelCoord, int( 0 ),
392
+ dimDir.add( i ), rngState, resolution, frame
393
+ ).mul( totalWeight ).lessThan( importance )
394
+ ), () => {
395
+
396
+ selectedType.assign( 0 );
397
+ selectedIdx.assign( i );
398
+ selectedImportance.assign( importance );
391
399
 
392
400
  } );
393
401
 
@@ -399,20 +407,18 @@ export const sampleLightWithImportance = Fn( ( [
399
407
 
400
408
  Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
401
409
 
402
- If( i.lessThan( int( 16 ) ), () => {
403
-
404
- const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
405
- const importance = select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ).toVar();
406
- totalWeight.addAssign( importance );
407
- If( importance.greaterThan( 0.0 ).and(
408
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
409
- ), () => {
410
-
411
- selectedType.assign( 1 );
412
- selectedIdx.assign( i );
413
- selectedImportance.assign( importance );
414
-
415
- } );
410
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
411
+ const importance = select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ).toVar();
412
+ totalWeight.addAssign( importance );
413
+ If( importance.greaterThan( 0.0 ).and(
414
+ getRandomSample1D( pixelCoord, int( 0 ),
415
+ dimArea.add( i ), rngState, resolution, frame
416
+ ).mul( totalWeight ).lessThan( importance )
417
+ ), () => {
418
+
419
+ selectedType.assign( 1 );
420
+ selectedIdx.assign( i );
421
+ selectedImportance.assign( importance );
416
422
 
417
423
  } );
418
424
 
@@ -424,20 +430,18 @@ export const sampleLightWithImportance = Fn( ( [
424
430
 
425
431
  Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
426
432
 
427
- If( i.lessThan( int( 16 ) ), () => {
428
-
429
- const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
430
- const importance = calculatePointLightImportance( light, rayOrigin, normal, material ).toVar();
431
- totalWeight.addAssign( importance );
432
- If( importance.greaterThan( 0.0 ).and(
433
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
434
- ), () => {
435
-
436
- selectedType.assign( 2 );
437
- selectedIdx.assign( i );
438
- selectedImportance.assign( importance );
439
-
440
- } );
433
+ const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
434
+ const importance = calculatePointLightImportance( light, rayOrigin, normal, material ).toVar();
435
+ totalWeight.addAssign( importance );
436
+ If( importance.greaterThan( 0.0 ).and(
437
+ getRandomSample1D( pixelCoord, int( 0 ),
438
+ dimPoint.add( i ), rngState, resolution, frame
439
+ ).mul( totalWeight ).lessThan( importance )
440
+ ), () => {
441
+
442
+ selectedType.assign( 2 );
443
+ selectedIdx.assign( i );
444
+ selectedImportance.assign( importance );
441
445
 
442
446
  } );
443
447
 
@@ -449,20 +453,18 @@ export const sampleLightWithImportance = Fn( ( [
449
453
 
450
454
  Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
451
455
 
452
- If( i.lessThan( int( 16 ) ), () => {
453
-
454
- const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
455
- const importance = calculateSpotLightImportance( light, rayOrigin, normal, material ).toVar();
456
- totalWeight.addAssign( importance );
457
- If( importance.greaterThan( 0.0 ).and(
458
- RandomValue( rngState ).mul( totalWeight ).lessThan( importance )
459
- ), () => {
460
-
461
- selectedType.assign( 3 );
462
- selectedIdx.assign( i );
463
- selectedImportance.assign( importance );
464
-
465
- } );
456
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
457
+ const importance = calculateSpotLightImportance( light, rayOrigin, normal, material ).toVar();
458
+ totalWeight.addAssign( importance );
459
+ If( importance.greaterThan( 0.0 ).and(
460
+ getRandomSample1D( pixelCoord, int( 0 ),
461
+ dimSpot.add( i ), rngState, resolution, frame
462
+ ).mul( totalWeight ).lessThan( importance )
463
+ ), () => {
464
+
465
+ selectedType.assign( 3 );
466
+ selectedIdx.assign( i );
467
+ selectedImportance.assign( importance );
466
468
 
467
469
  } );
468
470
 
@@ -518,7 +520,7 @@ export const sampleLightWithImportance = Fn( ( [
518
520
 
519
521
  If( light.intensity.greaterThan( 0.0 ), () => {
520
522
 
521
- const uv = vec2( randomSeed.y, RandomValue( rngState ) ).toVar();
523
+ const uv = vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ).toVar();
522
524
  const areaSample = LightSample.wrap( sampleRectAreaLight( light, rayOrigin, uv, lightSelectionPdf ) );
523
525
  r_valid.assign( areaSample.valid );
524
526
  r_direction.assign( areaSample.direction );
@@ -607,7 +609,7 @@ export const sampleLightWithImportance = Fn( ( [
607
609
  const cosHalfAngle = cos( light.angle.mul( 0.5 ) ).toVar();
608
610
  const cosTheta = mix( cosHalfAngle, float( 1.0 ), randomSeed.y ).toVar();
609
611
  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();
612
+ const phi = float( TWO_PI ).mul( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 14 ) ), rngState, resolution, frame ) ).toVar();
611
613
 
612
614
  const w = normalize( light.direction ).toVar();
613
615
  const u = normalize( cross(
@@ -639,7 +641,7 @@ export const sampleLightWithImportance = Fn( ( [
639
641
  If( selectedType.equal( int( 1 ) ).and( selectedIdx.greaterThanEqual( int( 0 ) ) ), () => {
640
642
 
641
643
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, selectedIdx ) );
642
- const uv = vec2( randomSeed.y, RandomValue( rngState ) ).toVar();
644
+ const uv = vec2( randomSeed.y, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ).toVar();
643
645
  const areaSample = LightSample.wrap( sampleRectAreaLight( light, rayOrigin, uv, pdf ) );
644
646
  r_valid.assign( areaSample.valid );
645
647
  r_direction.assign( areaSample.direction );
@@ -689,6 +691,7 @@ export const sampleLightWithImportance = Fn( ( [
689
691
  distance: r_distance,
690
692
  pdf: r_pdf,
691
693
  lightType: r_lightType,
694
+ selectionTotalWeight: totalWeight,
692
695
  } );
693
696
 
694
697
  } );
@@ -700,66 +703,14 @@ export const sampleLightWithImportance = Fn( ( [
700
703
  // PDF computation given precomputed dot products. Use this when the caller
701
704
  // already has dots from a paired evaluateMaterialResponseFromDots invocation
702
705
  // to avoid recomputing H + dots.
706
+ // BSDF-sampling density for MIS — must be the exact function the sampler draws from, or the two
707
+ // strategies' weights stop summing to 1.
703
708
  export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
704
709
 
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 );
710
+ return max( calculateBSDFSamplingPDFFromMaterial( material, dots ), 1e-8 );
758
711
 
759
712
  } );
760
713
 
761
- // Wrapper that computes dots internally. Use this when the caller doesn't
762
- // already have dots; otherwise prefer calculateMaterialPDFFromDots.
763
714
  export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material ] ) => {
764
715
 
765
716
  const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
@@ -767,86 +718,6 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
767
718
 
768
719
  } );
769
720
 
770
- // Total light-selection weight at a shading point, replicating the NEE reservoir's
771
- // importance accumulation (same per-type importance fns, same per-type 16-light cap + order).
772
- // Used to reconstruct the exact NEE selection pdf for MIS on the BSDF-hit path —
773
- // the analogue of Cycles' light_tree_pdf re-walk. Without it, the BSDF-hit MIS
774
- // partner assumes uniform 1/N selection, which disagrees with the importance-
775
- // weighted NEE selection and biases the power-heuristic weights.
776
- export const computeTotalLightImportance = Fn( ( [
777
- rayOrigin, normal, material, bounceIndex,
778
- directionalLightsBuffer, numDirectionalLights,
779
- areaLightsBuffer, numAreaLights,
780
- pointLightsBuffer, numPointLights,
781
- spotLightsBuffer, numSpotLights,
782
- ] ) => {
783
-
784
- const totalWeight = float( 0.0 ).toVar();
785
-
786
- If( numDirectionalLights.greaterThan( int( 0 ) ), () => {
787
-
788
- Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
789
-
790
- If( i.lessThan( int( 16 ) ), () => {
791
-
792
- const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
793
- totalWeight.addAssign( calculateDirectionalLightImportance( light, normal, material, bounceIndex ) );
794
-
795
- } );
796
-
797
- } );
798
-
799
- } );
800
-
801
- If( numAreaLights.greaterThan( int( 0 ) ), () => {
802
-
803
- Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
804
-
805
- If( i.lessThan( int( 16 ) ), () => {
806
-
807
- const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
808
- totalWeight.addAssign( select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ) );
809
-
810
- } );
811
-
812
- } );
813
-
814
- } );
815
-
816
- If( numPointLights.greaterThan( int( 0 ) ), () => {
817
-
818
- Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
819
-
820
- If( i.lessThan( int( 16 ) ), () => {
821
-
822
- const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
823
- totalWeight.addAssign( calculatePointLightImportance( light, rayOrigin, normal, material ) );
824
-
825
- } );
826
-
827
- } );
828
-
829
- } );
830
-
831
- If( numSpotLights.greaterThan( int( 0 ) ), () => {
832
-
833
- Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
834
-
835
- If( i.lessThan( int( 16 ) ), () => {
836
-
837
- const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
838
- totalWeight.addAssign( calculateSpotLightImportance( light, rayOrigin, normal, material ) );
839
-
840
- } );
841
-
842
- } );
843
-
844
- } );
845
-
846
- return totalWeight;
847
-
848
- } );
849
-
850
721
  // =============================================================================
851
722
  // Unified Direct Lighting System
852
723
  // =============================================================================
@@ -861,6 +732,9 @@ export const calculateDirectLightingUnified = Fn( ( [
861
732
  brdfSampleDirection, brdfSamplePdf, brdfSampleValue,
862
733
  // Tracing context
863
734
  bounceIndex, rngState,
735
+ // Sampler context — NEE's continuous variates come off the sampler keyed on
736
+ // (pixel, dimension, frame). dimBase is this bounce's block; see Random.js budget.
737
+ pixelCoord, resolution, frame, dimBase,
864
738
  // Light data
865
739
  directionalLightsBuffer, numDirectionalLights,
866
740
  areaLightsBuffer, numAreaLights,
@@ -893,101 +767,40 @@ export const calculateDirectLightingUnified = Fn( ( [
893
767
  // Early exit for highly emissive surfaces
894
768
  If( material.emissiveIntensity.lessThanEqual( 10.0 ), () => {
895
769
 
896
- // Adaptive MIS Strategy Selection
897
- const currentThroughput = vec3( 1.0 ).toVar();
898
- const misResult = MISStrategy.wrap( selectOptimalMISStrategy(
899
- material.roughness, material.metalness, bounceIndex, currentThroughput
900
- ) );
901
-
902
- // Extract MIS fields to mutable variables
903
- // (env is handled deterministically below, not part of stochastic selection)
904
- const useBRDFSampling = misResult.useBRDFSampling.toVar();
905
- const useLightSampling = misResult.useLightSampling.toVar();
906
- const brdfWeight = misResult.brdfWeight.toVar();
907
- const lightWeight = misResult.lightWeight.toVar();
908
-
909
- // Adaptive light processing
770
+ // NEE and BSDF-hit both run at every vertex, one sample each, so the MIS weights are the
771
+ // plain power heuristic over the raw densities. Picking one strategy stochastically and
772
+ // dividing by the selection probability was unbiased but cost 3× RMSE on an area light.
910
773
  const totalLights = numDirectionalLights.add( numAreaLights ).add( numPointLights ).add( numSpotLights ).toVar();
911
-
912
- const importanceThreshold = float( 0.001 ).mul( float( 1.0 ).add( float( bounceIndex ).mul( 0.5 ) ) ).toVar();
913
-
914
- // Check if discrete lights exist
915
774
  const hasDiscreteLights = totalLights.greaterThan( int( 0 ) ).toVar();
916
775
 
917
- // Calculate total sampling weight for stochastic {lights, BRDF} selection
918
- const totalSamplingWeight = float( 0.0 ).toVar();
919
-
920
- If( useLightSampling.and( hasDiscreteLights ), () => {
921
-
922
- totalSamplingWeight.addAssign( lightWeight );
923
-
924
- } );
925
-
926
- If( useBRDFSampling, () => {
927
-
928
- totalSamplingWeight.addAssign( brdfWeight );
929
-
930
- } );
931
-
932
- If( totalSamplingWeight.lessThanEqual( 0.0 ), () => {
933
-
934
- totalSamplingWeight.assign( 1.0 );
935
- useBRDFSampling.assign( tslBool( true ) );
936
- brdfWeight.assign( 1.0 );
937
-
938
- } );
939
-
940
- const stratRand1 = RandomValue( rngState ).toVar();
941
- const stratRand2 = RandomValue( rngState ).toVar();
942
-
943
- // Determine sampling technique: stochastic {lights, BRDF}
944
- const rand = stratRand1;
945
- const sampleLights = tslBool( false ).toVar();
946
- const sampleBRDF = tslBool( false ).toVar();
947
-
948
- // Calculate effective weights for probability (only include light weight if lights exist)
949
- const effectiveLightWeight = select( hasDiscreteLights, lightWeight, float( 0.0 ) );
950
- // Guard division
951
- const invTotalSamplingWeight = float( 1.0 ).div( max( totalSamplingWeight, 1e-10 ) );
952
- const cumulativeLight = effectiveLightWeight.mul( invTotalSamplingWeight );
953
-
954
- If( rand.lessThan( cumulativeLight ).and( useLightSampling ).and( hasDiscreteLights ), () => {
955
-
956
- sampleLights.assign( tslBool( true ) );
957
-
958
- } ).ElseIf( useBRDFSampling, () => {
959
-
960
- sampleBRDF.assign( tslBool( true ) );
961
-
962
- } ).ElseIf( hasDiscreteLights, () => {
963
-
964
- // Fallback to light sampling only if lights exist
965
- sampleLights.assign( tslBool( true ) );
966
-
967
- } );
776
+ // Reservoir denominator from the NEE walk above; the BSDF-hit branch below reuses it
777
+ // rather than re-walking every light buffer to rebuild the same sum.
778
+ const neeTotalWeight = float( 0.0 ).toVar();
968
779
 
969
780
  // =====================================================================
970
781
  // LIGHT SAMPLING PATH
971
782
  // =====================================================================
972
783
 
973
- If( sampleLights, () => {
784
+ If( hasDiscreteLights, () => {
974
785
 
975
786
  // Importance-weighted light sampling
976
- const lightRandom = vec2( stratRand2, RandomValue( rngState ) ).toVar();
787
+ const lightRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
977
788
  const lightSample = LightSample.wrap( sampleLightWithImportance(
978
789
  rayOrigin, hitNormal, material, lightRandom, bounceIndex, rngState,
790
+ pixelCoord, resolution, frame, dimBase,
979
791
  directionalLightsBuffer, numDirectionalLights,
980
792
  areaLightsBuffer, numAreaLights,
981
793
  pointLightsBuffer, numPointLights,
982
794
  spotLightsBuffer, numSpotLights,
983
795
  ) );
984
796
 
797
+ neeTotalWeight.assign( lightSample.selectionTotalWeight );
798
+
985
799
  If( lightSample.valid.and( lightSample.pdf.greaterThan( 0.0 ) ), () => {
986
800
 
987
801
  const NoL = max( float( 0.0 ), dot( hitNormal, lightSample.direction ) ).toVar();
988
- const lightImportance = lightSample.emission.x.add( lightSample.emission.y ).add( lightSample.emission.z );
989
802
 
990
- If( NoL.greaterThan( 0.0 ).and( lightImportance.mul( NoL ).greaterThan( importanceThreshold ) ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: hitNormal } ) ), () => {
803
+ If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: hitNormal } ) ), () => {
991
804
 
992
805
  const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) );
993
806
  const visibility = shadow( rayOrigin, lightSample.direction, shadowDistance );
@@ -1000,28 +813,19 @@ export const calculateDirectLightingUnified = Fn( ( [
1000
813
  const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
1001
814
  const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
1002
815
 
816
+ // Power heuristic only for area lights — they are the only type the BRDF
817
+ // path can intersect, so elsewhere MIS would just delete energy.
1003
818
  const misW = float( 1.0 ).toVar();
1004
819
 
1005
- If( bPdf.greaterThan( 0.0 ).and( useBRDFSampling ), () => {
1006
-
1007
- const lightPdfWeighted = lightSample.pdf.mul( lightWeight );
1008
- const brdfPdfWeighted = bPdf.mul( brdfWeight );
1009
-
1010
- // Apply power heuristic only for area lights — the BRDF path can
1011
- // intersect area lights, so both strategies contribute and MIS is valid.
1012
- // Point/spot/directional lights are delta or non-intersectable by the
1013
- // BRDF path, so MIS would only reduce energy without compensation.
1014
- If( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ), () => {
820
+ If( bPdf.greaterThan( 0.0 ).and( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ) ), () => {
1015
821
 
1016
- misW.assign( powerHeuristic( { pdf1: lightPdfWeighted, pdf2: brdfPdfWeighted } ) );
1017
-
1018
- } );
822
+ misW.assign( powerHeuristic( { pdf1: lightSample.pdf, pdf2: bPdf } ) );
1019
823
 
1020
824
  } );
1021
825
 
1022
826
  // Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
1023
827
  // pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
1024
- const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( lightWeight, 1e-10 ) );
828
+ const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
1025
829
  totalContribution.addAssign( baseContribution.mul( visibility ) );
1026
830
  If( wantUnoccluded, () => {
1027
831
 
@@ -1041,129 +845,94 @@ export const calculateDirectLightingUnified = Fn( ( [
1041
845
  // BRDF SAMPLING PATH
1042
846
  // =====================================================================
1043
847
 
1044
- If( sampleBRDF, () => {
1045
-
1046
- If( brdfSamplePdf.greaterThan( 0.0 ).and( useBRDFSampling ), () => {
1047
-
1048
- const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
1049
-
1050
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: hitNormal } ) ), () => {
1051
-
1052
- // Check intersection with area lights
1053
- If( numAreaLights.greaterThan( int( 0 ) ), () => {
1054
-
1055
- const foundIntersection = tslBool( false ).toVar();
1056
- const maxImportance = float( 0.0 ).toVar();
1057
- const maxImportanceLight = int( - 1 ).toVar();
1058
-
1059
- // Track best match (no early break)
1060
- Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
1061
-
1062
- const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
1063
-
1064
- If( light.intensity.greaterThan( 0.0 ), () => {
848
+ If( brdfSamplePdf.greaterThan( 0.0 ).and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
1065
849
 
1066
- const lightImp = estimateLightImportance( light, hitPoint, hitNormal, material ).toVar();
850
+ const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
1067
851
 
1068
- If( lightImp.greaterThanEqual( importanceThreshold ), () => {
852
+ If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: hitNormal } ) ), () => {
1069
853
 
1070
- const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
854
+ // Nearest hit — the ray stops at the first emitter, so a brighter one behind it is
855
+ // the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
856
+ // energy the NEE partner's MIS weight already counted on, and estimateLightImportance
857
+ // reads zero across the terminator band of a large light (its CENTRE is below the
858
+ // horizon while half of it is still visible).
859
+ const nearestT = float( 1e30 ).toVar();
860
+ const nearestLight = int( - 1 ).toVar();
1071
861
 
1072
- If( hitDistance.greaterThan( 0.0 ), () => {
862
+ Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
1073
863
 
1074
- If( lightImp.greaterThan( maxImportance ), () => {
864
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
1075
865
 
1076
- maxImportance.assign( lightImp );
1077
- maxImportanceLight.assign( i );
866
+ If( light.intensity.greaterThan( 0.0 ), () => {
1078
867
 
1079
- } );
1080
- foundIntersection.assign( tslBool( true ) );
868
+ const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
1081
869
 
1082
- } );
870
+ If( hitDistance.greaterThan( 0.0 ).and( hitDistance.lessThan( nearestT ) ), () => {
1083
871
 
1084
- } );
1085
-
1086
- } );
872
+ nearestT.assign( hitDistance );
873
+ nearestLight.assign( i );
1087
874
 
1088
875
  } );
1089
876
 
1090
- If( foundIntersection.and( maxImportanceLight.greaterThanEqual( int( 0 ) ) ), () => {
1091
-
1092
- const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, maxImportanceLight ) );
1093
- const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
1094
-
1095
- If( hitDistance.greaterThan( 0.0 ), () => {
1096
-
1097
- const shadowDistance = min( hitDistance.sub( 0.001 ), float( 1000.0 ) );
1098
- const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
1099
-
1100
- If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1101
-
1102
- const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
1103
-
1104
- If( lightFacing.greaterThan( 0.0 ), () => {
1105
-
1106
- const lightDistSq = hitDistance.mul( hitDistance );
1107
-
1108
- // Directional pdf must match the NEE sampler: 1/S (spherical-rect)
1109
- // for rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
1110
- const corner = light.position.sub( light.u ).sub( light.v );
1111
- const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
1112
- const areaPdf = lightDistSq.div( max( light.area.mul( lightFacing ), EPSILON ) );
1113
- const dirPdf = select(
1114
- light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
1115
- float( 1.0 ).div( max( sAngle, 1e-10 ) ),
1116
- areaPdf,
1117
- ).toVar();
1118
-
1119
- // Selection pdf consistent with the importance-weighted NEE reservoir
1120
- // (Cycles light_tree_pdf re-walk). Falls back to uniform 1/N only when
1121
- // total importance is zero, matching the NEE fallback.
1122
- const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
1123
- const totalImp = computeTotalLightImportance(
1124
- rayOrigin, hitNormal, material, bounceIndex,
1125
- directionalLightsBuffer, numDirectionalLights,
1126
- areaLightsBuffer, numAreaLights,
1127
- pointLightsBuffer, numPointLights,
1128
- spotLightsBuffer, numSpotLights,
1129
- ).toVar();
1130
- const selPdf = select(
1131
- totalImp.greaterThan( 0.0 ),
1132
- selImp.div( max( totalImp, 1e-10 ) ),
1133
- float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
1134
- );
1135
- const lightPdf = dirPdf.mul( selPdf ).toVar();
1136
-
1137
- const brdfPdfWeighted = brdfSamplePdf.mul( brdfWeight );
1138
- const lightPdfWeighted = lightPdf.mul( lightWeight );
1139
- const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
877
+ } );
1140
878
 
1141
- const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
1142
- // Base contribution WITHOUT visibility (see discrete-light site).
1143
- const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
1144
- totalContribution.addAssign( baseContribution.mul( visibility ) );
1145
- If( wantUnoccluded, () => {
879
+ } );
1146
880
 
1147
- unoccludedContribution.addAssign( baseContribution );
881
+ If( nearestLight.greaterThanEqual( int( 0 ) ), () => {
1148
882
 
1149
- } );
883
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
884
+ const shadowDistance = min( nearestT.sub( 0.001 ), float( 1000.0 ) );
885
+ const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
1150
886
 
1151
- } );
887
+ If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1152
888
 
1153
- } );
889
+ const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
890
+
891
+ If( lightFacing.greaterThan( 0.0 ), () => {
892
+
893
+ // Directional pdf must match the NEE sampler: 1/S (spherical-rect) for
894
+ // rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
895
+ const corner = light.position.sub( light.u ).sub( light.v );
896
+ const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
897
+ const areaPdf = nearestT.mul( nearestT ).div( max( light.area.mul( lightFacing ), EPSILON ) );
898
+ const dirPdf = select(
899
+ light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
900
+ float( 1.0 ).div( max( sAngle, 1e-10 ) ),
901
+ areaPdf,
902
+ ).toVar();
903
+
904
+ // The NEE partner's selection pdf, rebuilt from the reservoir denominator it
905
+ // already accumulated. Uniform 1/N only when total importance is zero, matching
906
+ // the reservoir's own fallback; 0 for a light NEE can never pick, which hands
907
+ // this strategy the full MIS weight — as it must.
908
+ const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
909
+ const selPdf = select(
910
+ neeTotalWeight.greaterThan( 0.0 ),
911
+ selImp.div( max( neeTotalWeight, 1e-10 ) ),
912
+ float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
913
+ );
914
+ const lightPdf = dirPdf.mul( selPdf ).toVar();
915
+ const misW = powerHeuristic( { pdf1: brdfSamplePdf, pdf2: lightPdf } ).toVar();
916
+
917
+ const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
918
+ // Base contribution WITHOUT visibility (see discrete-light site).
919
+ const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) );
920
+ totalContribution.addAssign( baseContribution.mul( visibility ) );
921
+ If( wantUnoccluded, () => {
922
+
923
+ unoccludedContribution.addAssign( baseContribution );
1154
924
 
1155
925
  } );
1156
926
 
1157
927
  } );
1158
928
 
1159
- } ); // End numAreaLights > 0
929
+ } );
1160
930
 
1161
931
  } );
1162
932
 
1163
933
  } );
1164
934
 
1165
935
  } );
1166
-
1167
936
  // =====================================================================
1168
937
  // DETERMINISTIC ENVIRONMENT NEE
1169
938
  // Always runs (not stochastic) — forms a two-strategy Veach MIS
@@ -1172,9 +941,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1172
941
 
1173
942
  If( enableEnvironmentLight, () => {
1174
943
 
1175
- const env_r1 = RandomValue( rngState ).toVar();
1176
- const env_r2 = RandomValue( rngState ).toVar();
1177
- const envRandom = vec2( env_r1, env_r2 ).toVar();
944
+ const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
1178
945
  const envColor = vec3( 0.0 ).toVar();
1179
946
 
1180
947
  // Sample direction + PDF + color from importance-sampled environment