rayzee 10.0.0 → 11.0.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.
Files changed (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -6,6 +6,7 @@ import { BRDFWeights, MaterialCache, MaterialClassification, DFGResult, BaseFres
6
6
  import { PI, PI_INV, TWO_PI, EPSILON, MIN_ROUGHNESS, MIN_CLEARCOAT_ROUGHNESS, MAX_ROUGHNESS, XYZ_TO_REC709, square, classifyMaterial } from './Common.js';
7
7
 
8
8
  import { fresnelSchlickFloat, fresnel0ToIor, iorToFresnel0Vec3, iorToFresnel0, dielectricF0 } from './Fresnel.js';
9
+ import { materialLayers } from './SceneResources.js';
9
10
 
10
11
  // -----------------------------------------------------------------------------
11
12
  // Microfacet Distribution Functions
@@ -407,8 +408,9 @@ export const evalIridescence = Fn( ( [ outsideIOR, eta2, cosTheta1, thinFilmThic
407
408
  // BRDF Weight Calculation
408
409
  // -----------------------------------------------------------------------------
409
410
 
410
- export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
411
+ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ], builder ) => {
411
412
 
413
+ const layers = materialLayers( builder );
412
414
  // Use precomputed values from cache
413
415
  const invRoughness = cache.invRoughness;
414
416
  const metalFactor = cache.metalFactor;
@@ -438,10 +440,10 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
438
440
  const diffuse = float( 1.0 ).sub( baseSpecularWeight )
439
441
  .mul( float( 1.0 ).sub( material.metalness ) )
440
442
  .mul( float( 1.0 ).sub( material.transmission ) ).toVar();
441
- const sheen = material.sheen.mul( cache.maxSheenColor ).toVar();
443
+ const sheen = ( layers.sheen ? material.sheen.mul( cache.maxSheenColor ) : float( 0.0 ) ).toVar();
442
444
 
443
445
  const clearcoat = float( 0.0 ).toVar();
444
- If( mc.hasClearcoat, () => {
446
+ if ( layers.clearcoat ) If( mc.hasClearcoat, () => {
445
447
 
446
448
  clearcoat.assign( material.clearcoat.mul( invRoughness ).mul( 0.4 ) );
447
449
 
@@ -471,17 +473,22 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
471
473
  // Iridescence: shifts energy from diffuse to specular since it modifies specular F0
472
474
  // This preserves the total weight (no inflation) while increasing specular importance
473
475
  const iridescenceBase = invRoughness.mul( mc.isSmooth.select( float( 0.6 ), float( 0.5 ) ) );
474
- const iridescenceWeight = material.iridescence.mul( iridescenceBase )
476
+ // Off, the weight is 0 but the shift is kept: min( 0, diffuse ) still moves a negative diffuse weight to specular.
477
+ const iridescenceWeight = layers.iridescence ? material.iridescence.mul( iridescenceBase )
475
478
  .mul( float( 0.5 ).add( float( 0.5 ).mul(
476
479
  material.iridescenceThicknessRange.y.sub( material.iridescenceThicknessRange.x ).div( 1000.0 )
477
480
  ) ) )
478
- .mul( float( 0.5 ).add( float( 0.5 ).mul( material.iridescenceIOR.div( 2.0 ) ) ) );
481
+ .mul( float( 0.5 ).add( float( 0.5 ).mul( material.iridescenceIOR.div( 2.0 ) ) ) ) : float( 0.0 );
479
482
  const iridescenceShift = min( iridescenceWeight, diffuse );
480
483
  specular.addAssign( iridescenceShift );
481
484
  diffuse.subAssign( iridescenceShift );
482
485
 
486
+ // The diffuse lobe's transmitted share is drawn on the other side, in proportion.
487
+ const diffuseTransmission = ( layers.diffuseTransmission ? diffuse.mul( material.diffuseTransmission ) : float( 0.0 ) ).toVar();
488
+ diffuse.subAssign( diffuseTransmission );
489
+
483
490
  // Single normalization pass
484
- const total = specular.add( diffuse ).add( sheen ).add( clearcoat ).add( transmission );
491
+ const total = specular.add( diffuse ).add( sheen ).add( clearcoat ).add( transmission ).add( diffuseTransmission );
485
492
  const invTotal = float( 1.0 ).div( max( total, 0.001 ) );
486
493
 
487
494
  return BRDFWeights( {
@@ -491,6 +498,7 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
491
498
  clearcoat: clearcoat.mul( invTotal ),
492
499
  transmission: transmission.mul( invTotal ),
493
500
  iridescence: float( 0.0 ),
501
+ diffuseTransmission: diffuseTransmission.mul( invTotal ),
494
502
  } );
495
503
 
496
504
  } );
@@ -508,8 +516,9 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
508
516
  //
509
517
  // Transmission is absent because it only produces NoL < 0. `weights` is normalised over all
510
518
  // lobes including transmission, so omitting its term is exact, not an approximation.
511
- export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
519
+ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ], builder ) => {
512
520
 
521
+ const layers = materialLayers( builder );
513
522
  const pdf = float( 0.0 ).toVar();
514
523
 
515
524
  If( dots.NoL.greaterThan( 0.0 ), () => {
@@ -517,21 +526,24 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
517
526
  pdf.addAssign( weights.diffuse.mul( dots.NoL ).mul( PI_INV ) );
518
527
 
519
528
  const specPdf = float( 0.0 ).toVar();
520
- If( material.anisotropy.greaterThan( 0.0 ), () => {
529
+ // Raw roughness, not clamped: sampleGGXVNDF is called with material.roughness, and a
530
+ // clamp here would report a density the sampler never drew from.
531
+ const isotropic = () => {
521
532
 
522
- const a = computeAnisoAlphas( material.roughness, material.anisotropy );
523
- specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
533
+ specPdf.assign( calculateVNDFPDF( dots.NoH, dots.NoV, material.roughness ) );
524
534
 
525
- } ).Else( () => {
535
+ };
526
536
 
527
- // Raw roughness, not clamped: sampleGGXVNDF is called with material.roughness, and a
528
- // clamp here would report a density the sampler never drew from.
529
- specPdf.assign( calculateVNDFPDF( dots.NoH, dots.NoV, material.roughness ) );
537
+ if ( ! layers.anisotropy ) isotropic();
538
+ else If( material.anisotropy.greaterThan( 0.0 ), () => {
530
539
 
531
- } );
540
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
541
+ specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
542
+
543
+ } ).Else( isotropic );
532
544
  pdf.addAssign( weights.specular.mul( specPdf ) );
533
545
 
534
- If( weights.sheen.greaterThan( 0.0 ), () => {
546
+ if ( layers.sheen ) If( weights.sheen.greaterThan( 0.0 ), () => {
535
547
 
536
548
  const sheenPdf = SheenDistribution( dots.NoH, material.sheenRoughness )
537
549
  .mul( dots.NoH ).div( max( float( 4.0 ).mul( dots.VoH ), EPSILON ) );
@@ -539,7 +551,7 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
539
551
 
540
552
  } );
541
553
 
542
- If( weights.clearcoat.greaterThan( 0.0 ), () => {
554
+ if ( layers.clearcoat ) If( weights.clearcoat.greaterThan( 0.0 ), () => {
543
555
 
544
556
  const ccRoughness = clamp( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS, MAX_ROUGHNESS );
545
557
  pdf.addAssign( weights.clearcoat.mul( calculateVNDFPDF( dots.NoH, dots.NoV, ccRoughness ) ) );
@@ -552,6 +564,21 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
552
564
 
553
565
  } );
554
566
 
567
+ // The sampling density below the surface (cosBelow = −N·L > 0): only the diffuse transmission lobe draws there,
568
+ // chosen with probability `weight` (BRDFWeights.diffuseTransmission).
569
+ export const diffuseTransmissionPdf = Fn( ( [ weight, cosBelow ] ) => {
570
+
571
+ return weight.mul( cosBelow ).mul( PI_INV );
572
+
573
+ } );
574
+
575
+ // The diffuse transmission lobe's selection probability, for samplers outside the classify-once path.
576
+ export const diffuseTransmissionWeight = Fn( ( [ material ] ) => {
577
+
578
+ return BRDFWeights.wrap( calculateBRDFWeightsFromMaterial( material ) ).diffuseTransmission;
579
+
580
+ } );
581
+
555
582
  // Selection probabilities from `material` alone, for samplers outside the wavefront's
556
583
  // classify-once path. Must be the same weights whose density calculateBSDFSamplingPDF reports.
557
584
  export const calculateBRDFWeightsFromMaterial = Fn( ( [ material ] ) => {
@@ -10,6 +10,7 @@ import { DistributionGGX } from './MaterialProperties.js';
10
10
  import { ImportanceSampleGGX } from './MaterialSampling.js';
11
11
  import { getRandomSample1D, getRandomSample2D, pcgHash } from './Random.js';
12
12
  import { handleSubsurfaceEntry, SubsurfaceEntryResult } from './Subsurface.js';
13
+ import { materialLayers } from './SceneResources.js';
13
14
 
14
15
  // ================================================================================
15
16
  // STRUCTS (local to transmission)
@@ -158,8 +159,9 @@ export const calculateBeerLawAbsorption = /*@__PURE__*/ wgslFn( `
158
159
  export const sampleMicrofacetTransmission = Fn( ( [
159
160
  V, N, ior, roughness, entering, dispersion, xi, rngState, pathWavelength,
160
161
  pixelCoord, resolution, frame, dimBase,
161
- ] ) => {
162
+ ], builder ) => {
162
163
 
164
+ const layers = materialLayers( builder );
163
165
  const result = MicrofacetTransmissionResult( {
164
166
  direction: vec3( 0.0 ),
165
167
  halfVector: vec3( 0.0 ),
@@ -169,48 +171,7 @@ export const sampleMicrofacetTransmission = Fn( ( [
169
171
  pathWavelength: pathWavelength,
170
172
  } ).toVar();
171
173
 
172
- // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
173
- If( roughness.lessThanEqual( 0.05 ).and( dispersion.greaterThan( 0.0 ) ), () => {
174
-
175
- result.halfVector.assign( N );
176
- result.didReflect.assign( false );
177
-
178
- const eta = ior;
179
- const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
180
-
181
- // Reuse the path's locked wavelength if any; else sample a new one and tint once.
182
- If( pathWavelength.greaterThan( 0.0 ), () => {
183
-
184
- const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
185
- etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
186
-
187
- } ).Else( () => {
188
-
189
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 5 ) ), rngState, resolution, frame ) ) );
190
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
191
- result.colorWeight.assign( spectralSample.colorWeight );
192
- result.pathWavelength.assign( spectralSample.wavelength );
193
-
194
- } );
195
-
196
- // Perfect refraction using surface normal
197
- const refractDir = refract( V.negate(), N, etaRatio ).toVar();
198
-
199
- // Check for total internal reflection
200
- If( dot( refractDir, refractDir ).lessThan( 0.001 ), () => {
201
-
202
- result.direction.assign( reflect( V.negate(), N ) );
203
- result.didReflect.assign( true );
204
- result.pdf.assign( 1.0 );
205
-
206
- } ).Else( () => {
207
-
208
- result.direction.assign( refractDir );
209
- result.pdf.assign( 1.0 );
210
-
211
- } );
212
-
213
- } ).Else( () => {
174
+ const microfacet = () => {
214
175
 
215
176
  // Use minimum roughness to avoid numerical issues for rough surfaces
216
177
  const transmissionRoughness = max( MIN_ROUGHNESS, roughness );
@@ -223,7 +184,7 @@ export const sampleMicrofacetTransmission = Fn( ( [
223
184
  const etaRatio = select( entering, float( 1.0 ).div( ior ), ior ).toVar();
224
185
 
225
186
  // Reuse the path's locked wavelength if any; else sample a new one and tint once.
226
- If( dispersion.greaterThan( 0.0 ), () => {
187
+ if ( layers.dispersion ) If( dispersion.greaterThan( 0.0 ), () => {
227
188
 
228
189
  If( pathWavelength.greaterThan( 0.0 ), () => {
229
190
 
@@ -278,7 +239,51 @@ export const sampleMicrofacetTransmission = Fn( ( [
278
239
 
279
240
  } );
280
241
 
281
- } );
242
+ };
243
+
244
+ // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
245
+ if ( ! layers.dispersion ) microfacet();
246
+ else If( roughness.lessThanEqual( 0.05 ).and( dispersion.greaterThan( 0.0 ) ), () => {
247
+
248
+ result.halfVector.assign( N );
249
+ result.didReflect.assign( false );
250
+
251
+ const eta = ior;
252
+ const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
253
+
254
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
255
+ If( pathWavelength.greaterThan( 0.0 ), () => {
256
+
257
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
258
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
259
+
260
+ } ).Else( () => {
261
+
262
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 5 ) ), rngState, resolution, frame ) ) );
263
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
264
+ result.colorWeight.assign( spectralSample.colorWeight );
265
+ result.pathWavelength.assign( spectralSample.wavelength );
266
+
267
+ } );
268
+
269
+ // Perfect refraction using surface normal
270
+ const refractDir = refract( V.negate(), N, etaRatio ).toVar();
271
+
272
+ // Check for total internal reflection
273
+ If( dot( refractDir, refractDir ).lessThan( 0.001 ), () => {
274
+
275
+ result.direction.assign( reflect( V.negate(), N ) );
276
+ result.didReflect.assign( true );
277
+ result.pdf.assign( 1.0 );
278
+
279
+ } ).Else( () => {
280
+
281
+ result.direction.assign( refractDir );
282
+ result.pdf.assign( 1.0 );
283
+
284
+ } );
285
+
286
+ } ).Else( microfacet );
282
287
 
283
288
  return result;
284
289
 
@@ -430,7 +435,7 @@ export const handleMaterialTransparency = Fn( ( [
430
435
  transmissiveTraversals,
431
436
  currentMediumIOR, previousMediumIOR,
432
437
  pathWavelength,
433
- ] ) => {
438
+ ], builder ) => {
434
439
 
435
440
  const result = MaterialInteractionResult( {
436
441
  continueRay: false,
@@ -534,7 +539,7 @@ export const handleMaterialTransparency = Fn( ( [
534
539
 
535
540
  // Subsurface (independent of transmission; works at transmission==0). Entry is a lottery
536
541
  // (prob = weight) so 1-weight falls through to the opaque BRDF; exit is deterministic.
537
- If( handled.not().and( material.subsurface.greaterThan( 0.0 ) ), () => {
542
+ if ( materialLayers( builder ).subsurface ) If( handled.not().and( material.subsurface.greaterThan( 0.0 ) ), () => {
538
543
 
539
544
  const entering = dot( ray.direction, normal ).lessThan( 0.0 );
540
545
  const doEnter = entering.not().or( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 8 ) ), rngState, resolution, frame ).lessThan( material.subsurface ) );
@@ -0,0 +1,218 @@
1
+ /**
2
+ * MergeKernel.js — vertex merging (Georgiev et al. 2012, "Light Transport Simulation with Vertex Connection and
3
+ * Merging"): each camera vertex Shade left pending gathers the light vertices within the merge radius, as photon
4
+ * mapping does, MIS-weighted against every other strategy. It is the one strategy for light no path can connect:
5
+ * a lamp behind glass seen in a mirror, a caustic seen through water.
6
+ *
7
+ * The radius is a few pixels' footprint where the camera vertex is (Bidirectional.js mergeRadiusAt), so it grows
8
+ * with the distance from the camera. The light vertex cache is filed by that radius in shells, each a hash grid of
9
+ * cells two of its largest radii wide: one list a cell, threaded through the cached records' spare lane (`next`),
10
+ * built after the light pass by mergeInsert. A gather searches the one or two shells its sphere reaches, 2³ cells each.
11
+ */
12
+
13
+ import {
14
+ Fn, float, vec3, vec4, int, uint, ivec3, If, Loop, instanceIndex, atomicLoad, atomicStore, atomicFunc, Return, dot, floor, max, min, select,
15
+ length, log2, exp2, Break,
16
+ } from 'three/tsl';
17
+
18
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
19
+ import { calculateMaterialPDF } from './LightsSampling.js';
20
+ import { luminance } from './Common.js';
21
+ import { regularizePathContribution } from './PathTracerCore.js';
22
+ import { resolveSurfaceMaterial, misMergePartial, misWeight, strategyWeight, mergeRadiusAt, mergeEta, STRATEGY } from './Bidirectional.js';
23
+ import { COUNTER } from '../Processor/QueueManager.js';
24
+ import {
25
+ readRayRadiance, writeRayRadiance, pendingVertex, cachedVertex, readVertexTag, readVertexRecord, readVertexPosition,
26
+ readVertexNext, writeVertexNext, readLightPathLength,
27
+ } from '../Processor/PackedRayBuffer.js';
28
+
29
+ const WG_SIZE = 256;
30
+ export const MERGE_EMPTY = 0xFFFFFFFF;
31
+ // A cell's list is walked at most this far: a bound on the work, never reached at the radius the radius scale gives.
32
+ const MAX_CELL_STEPS = 1024;
33
+
34
+ // Shells of radius ratio 1.25: a sphere whose radius changes by (1 ± slope) across it spans at most two.
35
+ export const MERGE_SHELL_RATIO = 1.25;
36
+ const LOG2_RATIO = Math.log2( MERGE_SHELL_RATIO );
37
+
38
+ const shellOf = ( bdpt, radius ) => int( max( floor( log2( radius.div( bdpt.mergeMin ) ).div( LOG2_RATIO ) ), 0.0 ) );
39
+
40
+ // Twice the largest radius whose sphere can reach the shell (its own largest, over 1 − slope).
41
+ const shellCell = ( bdpt, shell ) => bdpt.mergeMin.mul( exp2( float( shell.add( int( 1 ) ) ).mul( LOG2_RATIO ) ) ).mul( 2.0 )
42
+ .div( max( float( 1.0 ).sub( bdpt.mergeSlope ), 0.5 ) );
43
+
44
+ const cellHash = ( shell, cell, hashMask ) => uint( cell.x ).mul( uint( 73856093 ) )
45
+ .bitXor( uint( cell.y ).mul( uint( 19349663 ) ) )
46
+ .bitXor( uint( cell.z ).mul( uint( 83492791 ) ) )
47
+ .bitXor( uint( shell ).mul( uint( 2654435761 ) ) )
48
+ .bitAnd( hashMask );
49
+
50
+ // A light vertex's shell and cell.
51
+ const filing = ( bdpt, p ) => {
52
+
53
+ const shell = shellOf( bdpt, mergeRadiusAt( bdpt, p ) ).toVar();
54
+ return { shell, cell: ivec3( floor( p.div( shellCell( bdpt, shell ) ) ) ).toVar() };
55
+
56
+ };
57
+
58
+ /** Empties every list head. */
59
+ export function buildMergeClearKernel( { head, hashSize } ) {
60
+
61
+ return Fn( () => {
62
+
63
+ If( instanceIndex.lessThan( uint( hashSize ) ), () => {
64
+
65
+ atomicStore( head.element( instanceIndex ), uint( MERGE_EMPTY ) );
66
+
67
+ } );
68
+
69
+ } )().compute( Math.ceil( hashSize / WG_SIZE ) * WG_SIZE, [ WG_SIZE ] );
70
+
71
+ }
72
+
73
+ /** Files each light vertex of this frame at the head of its cell's list. */
74
+ export function buildMergeInsertKernel( { hitBufferRW, head, bidirectional: bdpt } ) {
75
+
76
+ const { lightPaths, slotsPerPath, lightTag, hashMask } = bdpt;
77
+
78
+ return Fn( () => {
79
+
80
+ const slot = instanceIndex.toVar();
81
+ If( slot.greaterThanEqual( lightPaths.mul( slotsPerPath ) ), () => {
82
+
83
+ Return();
84
+
85
+ } );
86
+
87
+ const depth = slot.mod( slotsPerPath ).toVar();
88
+ If( depth.greaterThanEqual( readLightPathLength( hitBufferRW, cachedVertex( slot.sub( depth ) ), lightTag ) ), () => {
89
+
90
+ Return();
91
+
92
+ } );
93
+
94
+ const at = cachedVertex( slot );
95
+ const { shell, cell } = filing( bdpt, readVertexPosition( hitBufferRW, at ) );
96
+ const bucket = cellHash( shell, cell, hashMask );
97
+ writeVertexNext( hitBufferRW, at, atomicFunc( 'atomicExchange', head.element( bucket ), slot ) );
98
+
99
+ } );
100
+
101
+ }
102
+
103
+ export function buildMergeKernel( params ) {
104
+
105
+ const {
106
+ rayBufferRW, hitBufferRO, activeIndicesRO, counters, head, materialBuffer, bidirectional: bdpt,
107
+ maxBounceCount, globalIlluminationIntensity, fireflyThreshold, accumFrame,
108
+ } = params;
109
+ const { passTag, slotsPerPath, strategyView, hashMask } = bdpt;
110
+
111
+ return Fn( () => {
112
+
113
+ const tid = instanceIndex;
114
+ If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
115
+
116
+ Return();
117
+
118
+ } );
119
+
120
+ const rayID = activeIndicesRO.element( tid ).toVar();
121
+ const pending = pendingVertex( rayID );
122
+ If( readVertexTag( hitBufferRO, pending ).notEqual( passTag ), () => {
123
+
124
+ Return();
125
+
126
+ } );
127
+
128
+ const cam = readVertexRecord( hitBufferRO, pending );
129
+ const cameraDepth = int( cam.extra ).toVar();
130
+ // A merged path of k scattering vertices: the camera's cameraDepth + 1 and the light's l share one.
131
+ const lightBudget = int( maxBounceCount ).add( int( 1 ) ).sub( cameraDepth ).toVar();
132
+ const material = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
133
+ const gathered = vec3( 0.0 ).toVar();
134
+
135
+ const radius = mergeRadiusAt( bdpt, cam.position ).toVar();
136
+ const radius2 = radius.mul( radius ).toVar();
137
+ const eta = mergeEta( bdpt, radius ).toVar();
138
+ const trusted = eta.mul( bdpt.mergeTrust );
139
+ const vc = float( 1.0 ).div( max( trusted.mul( trusted ), 1e-30 ) ).toVar();
140
+ // The shells the sphere reaches: its radius at its nearest and farthest points from the camera.
141
+ const distance = length( cam.position.sub( bdpt.cameraPosition ) ).toVar();
142
+ const nearShell = shellOf( bdpt, max( bdpt.mergeConst.add( bdpt.mergeSlope.mul( max( distance.sub( radius ), 0.0 ) ) ), bdpt.mergeMin ) ).toVar();
143
+ const farShell = min( shellOf( bdpt, bdpt.mergeConst.add( bdpt.mergeSlope.mul( distance.add( radius ) ) ) ), nearShell.add( int( 1 ) ) ).toVar();
144
+
145
+ // Per shell, the 2 × 2 × 2 cells the sphere can reach, the cells being at least two radii wide.
146
+ Loop( { start: int( 0 ), end: int( 16 ), type: 'int', condition: '<' }, ( { i } ) => {
147
+
148
+ const shell = nearShell.add( i.shiftRight( int( 3 ) ) ).toVar();
149
+ If( shell.greaterThan( farShell ), () => {
150
+
151
+ Break();
152
+
153
+ } );
154
+ const cellSize = shellCell( bdpt, shell ).toVar();
155
+ const corner = i.bitAnd( int( 7 ) );
156
+ const cell = ivec3( floor( cam.position.div( cellSize ).sub( 0.5 ) ) )
157
+ .add( ivec3( corner.bitAnd( int( 1 ) ), corner.shiftRight( int( 1 ) ).bitAnd( int( 1 ) ), corner.shiftRight( int( 2 ) ) ) ).toVar();
158
+ const slot = atomicLoad( head.element( cellHash( shell, cell, hashMask ) ) ).toVar();
159
+ const steps = int( 0 ).toVar();
160
+
161
+ Loop( slot.notEqual( uint( MERGE_EMPTY ) ).and( steps.lessThan( int( MAX_CELL_STEPS ) ) ), () => {
162
+
163
+ const at = cachedVertex( slot );
164
+ const position = readVertexPosition( hitBufferRO, at ).toVar();
165
+ const offset = position.sub( cam.position );
166
+ const l = int( slot.mod( slotsPerPath ) ).add( int( 1 ) ).toVar();
167
+ If( dot( offset, offset ).lessThan( radius2 ).and( l.lessThanEqual( lightBudget ) ), () => {
168
+
169
+ // Its own shell and cell only: two lists sharing a bucket (a hash collision) must not count it twice.
170
+ const own = filing( bdpt, position );
171
+ If( own.shell.equal( shell ).and( own.cell.x.equal( cell.x ) ).and( own.cell.y.equal( cell.y ) ).and( own.cell.z.equal( cell.z ) ), () => {
172
+
173
+ const light = readVertexRecord( hitBufferRO, at );
174
+ const cosShading = dot( light.V, cam.N ).toVar();
175
+ const cosFacet = dot( light.V, cam.facetN ).toVar();
176
+ // Light arriving on this side. A corner's other wall stays in: its light vertices stand in for the
177
+ // part of the sphere past the corner, which the density divides by too.
178
+ If( cosShading.greaterThan( 0.0 ).and( cosFacet.greaterThan( 0.0 ) ), () => {
179
+
180
+ const f = evaluateMaterialResponse( cam.V, light.V, cam.N, material );
181
+ const wLight = misMergePartial( light, calculateMaterialPDF( cam.V, light.V, cam.N, material ), vc );
182
+ const wCamera = misMergePartial( cam, calculateMaterialPDF( light.V, cam.V, cam.N, material ), vc );
183
+ const scattering = cameraDepth.add( l );
184
+ // Alone, a path of k scattering vertices is reached by k merges.
185
+ const weight = strategyWeight( strategyView, STRATEGY.MERGE, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering ) ) );
186
+ const gi = select( scattering.greaterThan( int( 1 ) ), globalIlluminationIntensity, float( 1.0 ) );
187
+ // The photon's flux is per unit area; the BSDF wants it per projected solid angle (Veach 5.3.2).
188
+ const correction = cosShading.div( max( cosFacet, 1e-4 ) );
189
+ gathered.addAssign( regularizePathContribution(
190
+ cam.throughput.mul( f ).mul( light.throughput ).mul( weight.mul( correction ).div( eta ).mul( gi ) ),
191
+ float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),
192
+ ) );
193
+
194
+ } );
195
+
196
+ } );
197
+
198
+ } );
199
+
200
+ slot.assign( readVertexNext( hitBufferRO, at ) );
201
+ steps.addAssign( int( 1 ) );
202
+
203
+ } );
204
+
205
+ } );
206
+
207
+ If( luminance( gathered ).greaterThan( 0.0 ), () => {
208
+
209
+ const radiance = readRayRadiance( rayBufferRW, rayID ).toVar();
210
+ writeRayRadiance( rayBufferRW, rayID, vec4( radiance.xyz.add( gathered ), radiance.w ) );
211
+
212
+ } );
213
+
214
+ } );
215
+
216
+ }
217
+
218
+ export { WG_SIZE as MERGE_WG_SIZE };
@@ -45,8 +45,10 @@ import {
45
45
  computeAnisoAlphas,
46
46
  calculateBRDFWeights,
47
47
  sheenSamplingRoughness,
48
+ diffuseTransmissionPdf,
48
49
  } from './MaterialProperties.js';
49
- import { evaluateMaterialResponseFromDots, evaluateSpecularDeltaFromDots } from './MaterialEvaluation.js';
50
+ import { evaluateMaterialResponseFromDots, evaluateSpecularDeltaFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
51
+ import { materialLayers } from './SceneResources.js';
50
52
 
51
53
  import {
52
54
  ImportanceSampleCosine,
@@ -71,14 +73,16 @@ export const generateSampledDirection = Fn( ( [
71
73
  mc,
72
74
  weightsComputed, cachedBrdfWeights,
73
75
  materialCacheCached, cachedMaterialCache,
74
- ] ) => {
76
+ ], builder ) => {
75
77
 
78
+ const layers = materialLayers( builder );
76
79
  const resultDirection = vec3( 0.0 ).toVar();
77
80
  const resultValue = vec3( 0.0 ).toVar();
78
81
  const resultPdf = float( 0.0 ).toVar();
79
82
  const resultIsTransmission = tslBool( false ).toVar();
80
83
  const resultColorWeight = vec3( 1.0 ).toVar();
81
84
  const resultIsDelta = tslBool( false ).toVar();
85
+ const resultIsDiffuseTransmission = tslBool( false ).toVar();
82
86
 
83
87
  // Compute BRDF weights
84
88
  const weights = cachedBrdfWeights.toVar();
@@ -114,17 +118,42 @@ export const generateSampledDirection = Fn( ( [
114
118
  const cumulativeDiffuse = weights.diffuse.toVar();
115
119
  const cumulativeSpecular = cumulativeDiffuse.add( weights.specular ).toVar();
116
120
  const cumulativeSheen = cumulativeSpecular.add( weights.sheen ).toVar();
117
- const cumulativeClearcoat = cumulativeSheen.add( weights.clearcoat );
121
+ const cumulativeClearcoat = cumulativeSheen.add( weights.clearcoat ).toVar();
122
+ const cumulativeDiffuseTransmission = cumulativeClearcoat.add( weights.diffuseTransmission );
118
123
 
119
124
  // Chained If/ElseIf so emitted WGSL becomes a single mutually-exclusive branch
120
- // (replaces five separate If blocks gated on a `sampled` flag — divergence hotspot)
121
- If( rand.lessThan( cumulativeDiffuse ), () => {
125
+ // (replaces five separate If blocks gated on a `sampled` flag — divergence hotspot). A layer no material uses
126
+ // has weight 0, so its link could never be taken and is left out.
127
+ const isotropicSpecular = () => {
128
+
129
+ If( material.roughness.greaterThan( 0.0 ), () => {
130
+
131
+ const TBN = constructTBN( { N } );
132
+ const localV = TBN.transpose().mul( V );
133
+
134
+ // VNDF sampling
135
+ const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
136
+ H.assign( TBN.mul( localH ) );
137
+
138
+ resultDirection.assign( reflect( V.negate(), H ) );
139
+
140
+ } ).Else( () => {
141
+
142
+ resultDirection.assign( reflect( V.negate(), N ) );
143
+ resultIsDelta.assign( true );
144
+
145
+ } );
146
+
147
+ };
148
+
149
+ let lobes = If( rand.lessThan( cumulativeDiffuse ), () => {
122
150
 
123
151
  resultDirection.assign( ImportanceSampleCosine( { N, xi } ) );
124
152
 
125
153
  } ).ElseIf( rand.lessThan( cumulativeSpecular ), () => {
126
154
 
127
- If( material.anisotropy.greaterThan( 0.0 ), () => {
155
+ if ( ! layers.anisotropy ) isotropicSpecular();
156
+ else If( material.anisotropy.greaterThan( 0.0 ), () => {
128
157
 
129
158
  // Shared frame → sampler and eval/PDF stay bit-identical (MIS consistency)
130
159
  const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
@@ -138,25 +167,11 @@ export const generateSampledDirection = Fn( ( [
138
167
 
139
168
  resultDirection.assign( reflect( V.negate(), H ) );
140
169
 
141
- } ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
142
-
143
- const TBN = constructTBN( { N } );
144
- const localV = TBN.transpose().mul( V );
145
-
146
- // VNDF sampling
147
- const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
148
- H.assign( TBN.mul( localH ) );
149
-
150
- resultDirection.assign( reflect( V.negate(), H ) );
151
-
152
- } ).Else( () => {
153
-
154
- resultDirection.assign( reflect( V.negate(), N ) );
155
- resultIsDelta.assign( true );
170
+ } ).Else( isotropicSpecular );
156
171
 
157
- } );
172
+ } );
158
173
 
159
- } ).ElseIf( rand.lessThan( cumulativeSheen ), () => {
174
+ if ( layers.sheen ) lobes = lobes.ElseIf( rand.lessThan( cumulativeSheen ), () => {
160
175
 
161
176
  H.assign( ImportanceSampleGGX( { N, roughness: sheenSamplingRoughness( material.sheenRoughness ), Xi: xi } ) );
162
177
  resultDirection.assign( reflect( V.negate(), H ) );
@@ -165,7 +180,9 @@ export const generateSampledDirection = Fn( ( [
165
180
  // would add a P(reject)·cos term to the true density that calculateBSDFSamplingPDF cannot
166
181
  // model, and dividing by the smaller modelled density inflated the lobe 23 %.
167
182
 
168
- } ).ElseIf( rand.lessThan( cumulativeClearcoat ), () => {
183
+ } );
184
+
185
+ if ( layers.clearcoat ) lobes = lobes.ElseIf( rand.lessThan( cumulativeClearcoat ), () => {
169
186
 
170
187
  // VNDF, not ImportanceSampleGGX: the mixture density reports this lobe with
171
188
  // calculateVNDFPDF, and sampling the half-vector GGX distribution instead would make the
@@ -175,7 +192,16 @@ export const generateSampledDirection = Fn( ( [
175
192
  H.assign( ccTBN.mul( sampleGGXVNDF( { V: ccTBN.transpose().mul( V ), roughness: clearcoatRoughness, Xi: xi } ) ) );
176
193
  resultDirection.assign( reflect( V.negate(), H ) );
177
194
 
178
- } ).Else( () => {
195
+ } );
196
+
197
+ if ( layers.diffuseTransmission ) lobes = lobes.ElseIf( rand.lessThan( cumulativeDiffuseTransmission ), () => {
198
+
199
+ resultDirection.assign( ImportanceSampleCosine( { N: N.negate(), xi } ) );
200
+ resultIsDiffuseTransmission.assign( true );
201
+
202
+ } );
203
+
204
+ lobes.Else( () => {
179
205
 
180
206
  const entering = dot( V, N ).greaterThan( 0.0 );
181
207
  // pathWavelength=0 — the spectral lock happens downstream; colorWeight is carried out on
@@ -193,10 +219,11 @@ export const generateSampledDirection = Fn( ( [
193
219
 
194
220
  } );
195
221
 
196
- // One mixture density for every reflection lobe: any of them could have produced this
197
- // direction, so the chosen lobe's own pdf is not the density we sampled from.
198
- If( resultIsTransmission.not(), () => {
222
+ // Below the surface only the diffuse transmission lobe draws, so its own density is the mixture's.
223
+ const reflected = () => {
199
224
 
225
+ // One mixture density for every reflection lobe: any of them could have produced this
226
+ // direction, so the chosen lobe's own pdf is not the density we sampled from.
200
227
  const dotsOut = DotProducts.wrap( computeDotProductsAniso( N, V, resultDirection, material ) );
201
228
  If( resultIsDelta, () => {
202
229
 
@@ -213,7 +240,15 @@ export const generateSampledDirection = Fn( ( [
213
240
 
214
241
  } );
215
242
 
216
- } );
243
+ };
244
+
245
+ if ( ! layers.diffuseTransmission ) If( resultIsTransmission.not(), reflected );
246
+ else If( resultIsDiffuseTransmission, () => {
247
+
248
+ resultValue.assign( evaluateDiffuseTransmission( material, max( dot( N, V ), 0.001 ) ) );
249
+ resultPdf.assign( diffuseTransmissionPdf( weights.diffuseTransmission, dot( N, resultDirection ).negate() ) );
250
+
251
+ } ).ElseIf( resultIsTransmission.not(), reflected );
217
252
 
218
253
  resultPdf.assign( max( resultPdf, MIN_PDF ) );
219
254
 
@@ -223,6 +258,7 @@ export const generateSampledDirection = Fn( ( [
223
258
  pdf: resultPdf,
224
259
  isTransmission: resultIsTransmission,
225
260
  colorWeight: resultColorWeight,
261
+ isDiffuseTransmission: resultIsDiffuseTransmission,
226
262
  } );
227
263
 
228
264
  } );