rayzee 10.0.0 → 11.1.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 (176) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-Hn35o6Z5.js +5145 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -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-BzNpiVHZ.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-Bk7bHg2B.js} +55 -120
  14. package/dist/PathTracer-72XufKAr.js +29976 -0
  15. package/dist/PhysicalSky-BgjdV88k.js +1219 -0
  16. package/dist/RayzeeRenderer-1QRO97eq.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-tnAoUL1j.js +94 -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-C67jbci-.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 +20 -7
  52. package/src/Processor/ArchiveImporter.js +1265 -0
  53. package/src/Processor/ArchiveReader.js +22 -4
  54. package/src/Processor/AssetLoader.js +327 -1763
  55. package/src/Processor/AtmosphereModel.js +1 -2
  56. package/src/Processor/BVHBuilder.js +14 -126
  57. package/src/Processor/BVHLeafFold.js +4 -6
  58. package/src/Processor/BVHRefitter.js +67 -46
  59. package/src/Processor/BufferLayout.js +291 -0
  60. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  61. package/src/Processor/EnvironmentExactTable.js +187 -0
  62. package/src/Processor/EquirectHDRInfo.js +30 -223
  63. package/src/Processor/FileFormats.js +116 -0
  64. package/src/Processor/GLTFDecoders.js +85 -0
  65. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  66. package/src/Processor/GeometryExtractor.js +15 -5
  67. package/src/Processor/HostMemory.js +7 -1
  68. package/src/Processor/IESParser.js +47 -35
  69. package/src/Processor/InstanceTable.js +1 -1
  70. package/src/Processor/KernelManager.js +35 -2
  71. package/src/Processor/MaterialPacking.js +10 -8
  72. package/src/Processor/PBRT/PBRTMaterials.js +254 -42
  73. package/src/Processor/PBRT/PBRTParser.js +49 -19
  74. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  75. package/src/Processor/PBRT/PBRTSceneBuilder.js +1050 -75
  76. package/src/Processor/PBRT/PBRTTextureBake.js +179 -0
  77. package/src/Processor/PBRT/PFM.js +59 -0
  78. package/src/Processor/PackedRayBuffer.js +116 -9
  79. package/src/Processor/ParallelBVHBuilder.js +3 -6
  80. package/src/Processor/PhysicalSky.js +5 -4
  81. package/src/Processor/PlatformImageLoader.js +50 -6
  82. package/src/Processor/QueueManager.js +39 -5
  83. package/src/Processor/ReinsertionOptimizer.js +2 -2
  84. package/src/Processor/SceneMetadata.js +30 -5
  85. package/src/Processor/SceneProcessor.js +53 -62
  86. package/src/Processor/ShaderBuilder.js +6 -16
  87. package/src/Processor/SimpleSky.js +1 -62
  88. package/src/Processor/SolarLimb.js +3 -0
  89. package/src/Processor/SplitBLAS.js +1 -1
  90. package/src/Processor/StorageTexturePool.js +22 -1
  91. package/src/Processor/TLASBuilder.js +1 -3
  92. package/src/Processor/TextureBuckets.js +237 -0
  93. package/src/Processor/TextureCreator.js +181 -8
  94. package/src/Processor/ToneMapGPU.js +64 -15
  95. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  96. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  97. package/src/Processor/Workers/BVHWorker.js +5 -18
  98. package/src/Processor/Workers/CDFWorker.js +6 -183
  99. package/src/Processor/Workers/TexturesWorker.js +103 -18
  100. package/src/Processor/archiveFormats.js +7 -0
  101. package/src/Processor/utils.js +2 -1
  102. package/src/RayzeeRenderer.js +3900 -0
  103. package/src/RenderSettings.js +68 -62
  104. package/src/Stages/ASVGF.js +13 -14
  105. package/src/Stages/AutoExposure.js +17 -17
  106. package/src/Stages/BilateralFilter.js +2 -1
  107. package/src/Stages/Compositor.js +8 -23
  108. package/src/Stages/DenoiserSettings.js +182 -0
  109. package/src/Stages/EdgeFilter.js +2 -1
  110. package/src/Stages/MotionVector.js +1 -1
  111. package/src/Stages/NRD.js +11 -5
  112. package/src/Stages/NormalDepth.js +12 -8
  113. package/src/Stages/PathTracer.js +336 -43
  114. package/src/Stages/PathTracerStage.js +69 -66
  115. package/src/Stages/Variance.js +1 -1
  116. package/src/Storage/BLASCache.js +1 -1
  117. package/src/Storage/CDFCache.js +18 -17
  118. package/src/Storage/DownloadCache.js +1 -1
  119. package/src/Storage/SceneGraphCodec.js +46 -3
  120. package/src/Storage/SpillStore.js +1 -1
  121. package/src/Storage/StorageManager.js +45 -9
  122. package/src/Storage/areas.js +4 -0
  123. package/src/TSL/BVHTraversal.js +50 -25
  124. package/src/TSL/Bidirectional.js +208 -0
  125. package/src/TSL/BidirectionalLamps.js +132 -0
  126. package/src/TSL/Clearcoat.js +15 -6
  127. package/src/TSL/Common.js +20 -15
  128. package/src/TSL/ConnectKernel.js +167 -0
  129. package/src/TSL/Debugger.js +1 -1
  130. package/src/TSL/Displacement.js +4 -3
  131. package/src/TSL/EmissiveSampling.js +65 -18
  132. package/src/TSL/Environment.js +77 -106
  133. package/src/TSL/EnvironmentCDF.js +85 -39
  134. package/src/TSL/ExtendKernel.js +8 -3
  135. package/src/TSL/FinalWriteKernel.js +4 -4
  136. package/src/TSL/GenerateKernel.js +18 -4
  137. package/src/TSL/HitFacet.js +35 -5
  138. package/src/TSL/LightBVHSampling.js +40 -9
  139. package/src/TSL/LightGenerateKernel.js +293 -0
  140. package/src/TSL/LightGuide.js +198 -0
  141. package/src/TSL/LightSplatKernel.js +180 -0
  142. package/src/TSL/LightVisibility.js +111 -0
  143. package/src/TSL/LightsCore.js +13 -38
  144. package/src/TSL/LightsDirect.js +28 -29
  145. package/src/TSL/LightsIndirect.js +3 -1
  146. package/src/TSL/LightsSampling.js +239 -51
  147. package/src/TSL/MaterialEvaluation.js +113 -31
  148. package/src/TSL/MaterialProperties.js +44 -17
  149. package/src/TSL/MaterialTransmission.js +52 -47
  150. package/src/TSL/MergeKernel.js +224 -0
  151. package/src/TSL/PathTracerCore.js +65 -29
  152. package/src/TSL/Random.js +5 -1
  153. package/src/TSL/SceneResources.js +51 -0
  154. package/src/TSL/ShadeKernel.js +985 -101
  155. package/src/TSL/Struct.js +10 -0
  156. package/src/TSL/Sun.js +1 -1
  157. package/src/TSL/TextureSampling.js +66 -61
  158. package/src/addons/archives.js +14 -0
  159. package/src/addons/bidirectional.js +13 -0
  160. package/src/addons/color.js +32 -0
  161. package/src/addons/formats.js +16 -0
  162. package/src/addons/physicalSky.js +14 -0
  163. package/src/addons/storage.js +19 -0
  164. package/src/core.js +31 -0
  165. package/src/index.js +13 -23
  166. package/src/integrators/BidirectionalIntegrator.js +838 -0
  167. package/src/managers/AnimationManager.js +88 -6
  168. package/src/managers/CameraManager.js +103 -46
  169. package/src/managers/DenoisingManager.js +14 -10
  170. package/src/managers/EnvironmentManager.js +125 -103
  171. package/src/managers/GoboManager.js +9 -0
  172. package/src/managers/MaterialDataManager.js +48 -37
  173. package/src/managers/UniformManager.js +32 -10
  174. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  175. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  176. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -35,8 +35,10 @@ import {
35
35
  length,
36
36
  mix,
37
37
  select,
38
+ min,
38
39
  If,
39
40
  Loop,
41
+ sampler,
40
42
  } from 'three/tsl';
41
43
 
42
44
  import {
@@ -71,8 +73,8 @@ import {
71
73
  } from './LightsDirect.js';
72
74
 
73
75
  import { traverseBVHShadow } from './BVHTraversal.js';
74
- import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
75
- import { calculateBSDFSamplingPDFFromMaterial } from './MaterialProperties.js';
76
+ import { evaluateMaterialResponseFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
77
+ import { calculateBSDFSamplingPDFFromMaterial, diffuseTransmissionWeight, diffuseTransmissionPdf } from './MaterialProperties.js';
76
78
  import { getRandomSample1D, getRandomSample2D, SAMPLER_DIM_AUX_BASE } from './Random.js';
77
79
  import {
78
80
  PI,
@@ -83,12 +85,12 @@ import {
83
85
  computeDotProductsAniso,
84
86
  offsetRayOrigin,
85
87
  SHADOW_END,
88
+ REC709_LUMINANCE_COEFFICIENTS,
86
89
  } from './Common.js';
87
- import {
88
- sampleEquirectProbability,
89
- } from './Environment.js';
90
+ import { sampleEnvironment, sampleEnvironmentExact } from './Environment.js';
90
91
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
91
92
  import { sunRadianceToward, sampleSunDisc } from './Sun.js';
93
+ import { VISIBILITY_KIND, visibilityShare, recordVisibility } from './LightVisibility.js';
92
94
 
93
95
  const TWO_PI = 2.0 * PI;
94
96
 
@@ -757,17 +759,114 @@ export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
757
759
 
758
760
  export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material ] ) => {
759
761
 
760
- const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
761
- return calculateMaterialPDFFromDots( material, dots );
762
+ const pdf = float( 0.0 ).toVar();
763
+
764
+ If( dot( normal, lightDir ).lessThan( 0.0 ).and( material.diffuseTransmission.greaterThan( 0.0 ) ), () => {
765
+
766
+ pdf.assign( max( diffuseTransmissionPdf( diffuseTransmissionWeight( material ), dot( normal, lightDir ).negate() ), 1e-8 ) );
767
+
768
+ } ).Else( () => {
769
+
770
+ const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
771
+ pdf.assign( calculateMaterialPDFFromDots( material, dots ) );
772
+
773
+ } );
774
+
775
+ return pdf;
776
+
777
+ } );
778
+
779
+ // Fewer shadow rays a hit (setting shadowRays). Each light kind offers its sample unshadowed; `variant`:
780
+ // 'one' — keeps one with chance ∝ luminance × √(its kind's learned visibility), traced and divided by that chance;
781
+ // 'two' — always traces the strongest as well, and picks one of the rest.
782
+ const ONE_BELOW = 0.99999994;
762
783
 
784
+ const slotOf = () => ( {
785
+ kind: int( 0 ).toVar(), weight: float( 0.0 ).toVar(), value: vec3( 0.0 ).toVar(),
786
+ origin: vec3( 0.0 ).toVar(), direction: vec3( 0.0, 1.0, 0.0 ).toVar(), distance: float( 0.0 ).toVar(),
763
787
  } );
764
788
 
789
+ const FIELDS = [ 'kind', 'weight', 'value', 'origin', 'direction', 'distance' ];
790
+ const assignSlot = ( slot, from ) => FIELDS.forEach( ( f ) => slot[ f ].assign( from[ f ] ) );
791
+
792
+ // `learned` (LightVisibility.js) gives `share( kind )` and `record( kind, visible )`.
793
+ export const lightPick = ( u0, learned = null, variant = 'one' ) => {
794
+
795
+ const u = float( u0 ).toVar();
796
+ const total = float( 0.0 ).toVar();
797
+ const kept = slotOf();
798
+ const best = variant === 'two' ? slotOf() : null;
799
+
800
+ const keep = ( c ) => {
801
+
802
+ If( c.weight.greaterThan( 0.0 ), () => {
803
+
804
+ total.addAssign( c.weight );
805
+ const p = c.weight.div( total ).toVar();
806
+ If( u.lessThan( p ), () => {
807
+
808
+ u.assign( min( u.div( p ), ONE_BELOW ) );
809
+ assignSlot( kept, c );
810
+
811
+ } ).Else( () => {
812
+
813
+ u.assign( min( u.sub( p ).div( float( 1.0 ).sub( p ) ), ONE_BELOW ) );
814
+
815
+ } );
816
+
817
+ } );
818
+
819
+ };
820
+
821
+ const offer = ( contribution, from, dir, maxDist, lightKind = 0 ) => {
822
+
823
+ const luminance = dot( contribution, REC709_LUMINANCE_COEFFICIENTS );
824
+ const c = {
825
+ kind: int( lightKind ), value: contribution, origin: from, direction: dir, distance: maxDist,
826
+ weight: ( learned ? luminance.mul( learned.share( lightKind ) ) : luminance ).toVar(),
827
+ };
828
+ if ( ! best ) return keep( c );
829
+
830
+ const better = c.weight.greaterThan( best.weight ).toVar();
831
+ const loser = Object.fromEntries( FIELDS.map( ( f ) => [ f, select( better, best[ f ], c[ f ] ).toVar() ] ) );
832
+ keep( loser );
833
+ If( better, () => assignSlot( best, c ) );
834
+
835
+ };
836
+
837
+ // The direct light, tracing what the variant keeps through `shadow( origin, direction, distance )`.
838
+ const trace = ( shadow ) => {
839
+
840
+ const result = vec3( 0.0 ).toVar();
841
+ Loop( { start: int( 0 ), end: int( best ? 2 : 1 ), type: 'int', condition: '<' }, ( { i } ) => {
842
+
843
+ const isBest = best ? i.equal( int( 0 ) ) : tslBool( false );
844
+ const pickOf = ( f ) => ( best ? select( isBest, best[ f ], kept[ f ] ) : kept[ f ] );
845
+ const weight = pickOf( 'weight' ).toVar();
846
+ If( weight.greaterThan( 0.0 ), () => {
847
+
848
+ const visibility = shadow( pickOf( 'origin' ), pickOf( 'direction' ), pickOf( 'distance' ) ).toVar();
849
+ if ( learned ) learned.record( pickOf( 'kind' ), visibility.greaterThan( 0.0 ) );
850
+ result.addAssign( pickOf( 'value' ).mul( visibility ).mul( select( isBest, float( 1.0 ), total.div( weight ) ) ) );
851
+
852
+ } );
853
+
854
+ } );
855
+ return result;
856
+
857
+ };
858
+
859
+ return { offer, total, trace };
860
+
861
+ };
862
+
765
863
  // =============================================================================
766
864
  // Unified Direct Lighting System
767
865
  // =============================================================================
768
866
 
769
- // Optimized direct lighting function with importance-based sampling and better MIS
770
- export const calculateDirectLightingUnified = Fn( ( [
867
+ // Optimized direct lighting function with importance-based sampling and better MIS. `throughSurfaces` (compile
868
+ // time) adds light from behind a surface that passes it through diffusely; without it the code is exactly as before.
869
+ const makeDirectLighting = ( throughSurfaces, picked = null ) => Fn( ( [
771
870
  // Surface hit data
772
871
  hitPoint, hitNormal, geomNormal, material,
773
872
  // View direction
@@ -791,7 +890,7 @@ export const calculateDirectLightingUnified = Fn( ( [
791
890
  // Environment resources
792
891
  envTexture, environmentIntensity, envMatrix,
793
892
  envCDFTexture,
794
- envTotalSum, envCompensationDelta, envResolution,
893
+ envTotalSum, envResolution,
795
894
  enableEnvironmentLight,
796
895
  // Shadow catcher: when true, also accumulate the unoccluded (visibility=1) reference
797
896
  // at every light/env site so the caller can form a shadow ratio. Dead path otherwise.
@@ -801,6 +900,9 @@ export const calculateDirectLightingUnified = Fn( ( [
801
900
  terminatorLift, facetNormal, terminatorCutoff,
802
901
  // The physical sky's sun (Sun.js); its BSDF-sampled partner is the miss branch in ShadeKernel.
803
902
  hasSun, sunDirection, sunRadiance, sunParams,
903
+ // picked: the caller's emitter sample, unshadowed, and the learned visibility (LightVisibility.js) at this point
904
+ emissiveValue, emissiveOrigin, emissiveDirection, emissiveDistance,
905
+ visibilityCounters, visibilityCell,
804
906
  ] ) => {
805
907
 
806
908
  const totalContribution = vec3( 0.0 ).toVar();
@@ -817,6 +919,46 @@ export const calculateDirectLightingUnified = Fn( ( [
817
919
  traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer )
818
920
  );
819
921
 
922
+ // Light from behind a surface that passes light through diffusely: its shadow ray leaves from the other side,
923
+ // and the BSDF is the transmission lobe.
924
+ const throughSurface = L => ( throughSurfaces
925
+ ? material.diffuseTransmission.greaterThan( 0.0 ).and( dot( hitNormal, L ).lessThan( 0.0 ) ).and( dot( L, geomNormal ).lessThan( 0.0 ) )
926
+ : tslBool( false ) );
927
+ const backOrigin = throughSurfaces ? offsetRayOrigin( hitPoint, facetNormal.negate() ) : null;
928
+ const inFront = L => dot( hitNormal, L ).greaterThan( 0.0 ).and( isDirectionValid( { direction: L, surfaceNormal: geomNormal } ) );
929
+ const reachesSurface = ( L, back ) => ( throughSurfaces ? back.or( inFront( L ) ) : inFront( L ) );
930
+ const cosineTo = L => ( throughSurfaces ? abs( dot( hitNormal, L ) ) : max( float( 0.0 ), dot( hitNormal, L ) ) );
931
+ const originFor = ( back, front ) => ( throughSurfaces ? select( back, backOrigin, front ) : front );
932
+ const reflection = L => {
933
+
934
+ // Share H + dot products between BRDF eval and PDF — otherwise each
935
+ // would recompute normalize(V+L) + 5 dot products independently.
936
+ const dots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, L, material ) );
937
+ return { value: evaluateMaterialResponseFromDots( material, dots ), pdf: calculateMaterialPDFFromDots( material, dots ).toVar() };
938
+
939
+ };
940
+
941
+ const bsdfToward = ( L, back ) => {
942
+
943
+ if ( ! throughSurfaces ) return reflection( L );
944
+ const value = vec3( 0.0 ).toVar();
945
+ const pdf = float( 0.0 ).toVar();
946
+ If( back, () => {
947
+
948
+ value.assign( evaluateDiffuseTransmission( material, max( dot( hitNormal, viewDir ), 0.001 ) ) );
949
+ pdf.assign( max( diffuseTransmissionPdf( diffuseTransmissionWeight( material ), dot( hitNormal, L ).negate() ), 1e-8 ) );
950
+
951
+ } ).Else( () => {
952
+
953
+ const r = reflection( L );
954
+ value.assign( r.value );
955
+ pdf.assign( r.pdf );
956
+
957
+ } );
958
+ return { value, pdf };
959
+
960
+ };
961
+
820
962
 
821
963
  // NEE and BSDF-hit both run at every vertex, one sample each, so the MIS weights are the
822
964
  // plain power heuristic over the raw densities. Picking one strategy stochastically and
@@ -828,6 +970,13 @@ export const calculateDirectLightingUnified = Fn( ( [
828
970
  // rather than re-walking every light buffer to rebuild the same sum.
829
971
  const neeTotalWeight = float( 0.0 ).toVar();
830
972
 
973
+ const learned = picked ? {
974
+ share: ( kind ) => visibilityShare( visibilityCounters, visibilityCell, kind ),
975
+ record: ( kind, visible ) => recordVisibility( visibilityCounters, visibilityCell, kind, visible ),
976
+ } : null;
977
+ const pick = picked ? lightPick( getRandomSample1D( pixelCoord, int( 0 ), dimBase, rngState, resolution, frame ), learned, picked ) : null;
978
+ if ( pick ) pick.offer( emissiveValue, emissiveOrigin, emissiveDirection, emissiveDistance, VISIBILITY_KIND.EMITTERS );
979
+
831
980
  // =====================================================================
832
981
  // LIGHT SAMPLING PATH
833
982
  // =====================================================================
@@ -849,24 +998,19 @@ export const calculateDirectLightingUnified = Fn( ( [
849
998
 
850
999
  If( lightSample.valid.and( lightSample.pdf.greaterThan( 0.0 ) ), () => {
851
1000
 
852
- const NoL = max( float( 0.0 ), dot( hitNormal, lightSample.direction ) ).toVar();
1001
+ const back = throughSurface( lightSample.direction ).toVar();
1002
+ const NoL = cosineTo( lightSample.direction ).toVar();
853
1003
 
854
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: geomNormal } ) ), () => {
1004
+ If( reachesSurface( lightSample.direction, back ), () => {
855
1005
 
856
1006
  // Light geometry is measured from the hit point; only the visibility ray starts on the lifted
857
1007
  // origin, aimed at the sampled point and stopped a relative hair short of it.
858
- const lightOrigin = lightShadowOrigin( lightSample.direction ).toVar();
1008
+ const lightOrigin = originFor( back, lightShadowOrigin( lightSample.direction ) ).toVar();
859
1009
  const toSample = hitPoint.add( lightSample.direction.mul( lightSample.distance ) ).sub( lightOrigin ).toVar();
860
1010
  const shadowDistance = length( toSample ).toVar();
861
- const visibility = shadow( lightOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
1011
+ const lampLight = () => {
862
1012
 
863
- If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
864
-
865
- // Share H + dot products between BRDF eval and PDF — otherwise each
866
- // would recompute normalize(V+L) + 5 dot products independently.
867
- const sharedDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, lightSample.direction, material ) );
868
- const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
869
- const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
1013
+ const { value: brdfValue, pdf: bPdf } = bsdfToward( lightSample.direction, back );
870
1014
 
871
1015
  // Power heuristic only for area lights — they are the only type the BRDF
872
1016
  // path can intersect, so elsewhere MIS would just delete energy.
@@ -878,17 +1022,33 @@ export const calculateDirectLightingUnified = Fn( ( [
878
1022
 
879
1023
  } );
880
1024
 
881
- // Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
882
- // pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
883
- const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
884
- totalContribution.addAssign( baseContribution.mul( visibility ) );
885
- If( wantUnoccluded, () => {
1025
+ return lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
1026
+
1027
+ };
1028
+
1029
+ if ( pick ) {
1030
+
1031
+ pick.offer( lampLight(), lightOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ), VISIBILITY_KIND.LAMPS );
1032
+
1033
+ } else {
1034
+
1035
+ const visibility = shadow( lightOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
1036
+
1037
+ If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1038
+
1039
+ // Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
1040
+ // pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
1041
+ const baseContribution = lampLight();
1042
+ totalContribution.addAssign( baseContribution.mul( visibility ) );
1043
+ If( wantUnoccluded, () => {
1044
+
1045
+ unoccludedContribution.addAssign( baseContribution );
886
1046
 
887
- unoccludedContribution.addAssign( baseContribution );
1047
+ } );
888
1048
 
889
1049
  } );
890
1050
 
891
- } );
1051
+ }
892
1052
 
893
1053
  } );
894
1054
 
@@ -902,9 +1062,10 @@ export const calculateDirectLightingUnified = Fn( ( [
902
1062
 
903
1063
  If( brdfSamplePdf.greaterThan( 0.0 ).and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
904
1064
 
905
- const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
1065
+ const back = throughSurface( brdfSampleDirection ).toVar();
1066
+ const NoL = cosineTo( brdfSampleDirection ).toVar();
906
1067
 
907
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: geomNormal } ) ), () => {
1068
+ If( reachesSurface( brdfSampleDirection, back ), () => {
908
1069
 
909
1070
  // Nearest hit — the ray stops at the first emitter, so a brighter one behind it is
910
1071
  // the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
@@ -936,9 +1097,10 @@ export const calculateDirectLightingUnified = Fn( ( [
936
1097
  If( nearestLight.greaterThanEqual( int( 0 ) ), () => {
937
1098
 
938
1099
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
939
- const toLight = hitPoint.add( brdfSampleDirection.mul( nearestT ) ).sub( rayOrigin ).toVar();
1100
+ const hitOrigin = originFor( back, rayOrigin ).toVar();
1101
+ const toLight = hitPoint.add( brdfSampleDirection.mul( nearestT ) ).sub( hitOrigin ).toVar();
940
1102
  const shadowDistance = length( toLight ).toVar();
941
- const visibility = shadow( rayOrigin, toLight.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
1103
+ const visibility = shadow( hitOrigin, toLight.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
942
1104
 
943
1105
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
944
1106
 
@@ -1011,33 +1173,29 @@ export const calculateDirectLightingUnified = Fn( ( [
1011
1173
  radiance.assign( sunRadianceToward( direction, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ) );
1012
1174
  lightPdf.assign( select( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ), float( 1.0 ).div( sunParams.y ), float( 0.0 ) ) );
1013
1175
 
1014
- } ).Else( () => {
1176
+ } ).ElseIf( envTotalSum.greaterThan( 0.0 ), () => {
1015
1177
 
1016
- const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
1017
- const envColor = vec3( 0.0 ).toVar();
1018
- const envSampleResult = sampleEquirectProbability(
1019
- envTexture, envCDFTexture,
1020
- envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
1021
- ).toVar();
1022
- direction.assign( envSampleResult.xyz );
1023
- radiance.assign( envColor );
1024
- lightPdf.assign( envSampleResult.w );
1178
+ // Drawn from the table its density is read back from; a total of 0 is no table, or a black map.
1179
+ const drawn = sampleEnvironmentExact( envCDFTexture, envMatrix, envResolution,
1180
+ getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ) );
1181
+ direction.assign( drawn.direction );
1182
+ radiance.assign( sampleEnvironment( {
1183
+ tex: envTexture, samp: sampler( envTexture ), direction, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight: float( 1.0 ),
1184
+ } ).xyz );
1185
+ lightPdf.assign( drawn.pdf );
1025
1186
 
1026
1187
  } );
1027
1188
 
1028
1189
  If( lightPdf.greaterThan( 0.0 ), () => {
1029
1190
 
1030
- const NoL = max( float( 0.0 ), dot( hitNormal, direction ) ).toVar();
1031
-
1032
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction, surfaceNormal: geomNormal } ) ), () => {
1191
+ const back = throughSurface( direction ).toVar();
1192
+ const NoL = cosineTo( direction ).toVar();
1033
1193
 
1034
- const visibility = shadow( lightShadowOrigin( direction ), direction, float( 1e20 ) );
1194
+ If( reachesSurface( direction, back ), () => {
1035
1195
 
1036
- If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1196
+ const skyLight = () => {
1037
1197
 
1038
- const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, direction, material ) );
1039
- const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
1040
- const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
1198
+ const { value: brdfValue, pdf: bPdf } = bsdfToward( direction, back );
1041
1199
 
1042
1200
  // The map pairs with the balance heuristic — optimal for its MIS-compensated pdf
1043
1201
  // (Karlík et al. 2019); the sun, a small bright source, with the power heuristic.
@@ -1047,8 +1205,24 @@ export const calculateDirectLightingUnified = Fn( ( [
1047
1205
  float( 1.0 )
1048
1206
  ).toVar();
1049
1207
 
1208
+ return radiance.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightPdf, 1e-10 ) );
1209
+
1210
+ };
1211
+
1212
+ if ( pick ) {
1213
+
1214
+ pick.offer( skyLight(), originFor( back, lightShadowOrigin( direction ) ), direction, float( 1e20 ),
1215
+ select( isSun, int( VISIBILITY_KIND.SUN ), int( VISIBILITY_KIND.ENVIRONMENT ) ) );
1216
+ return;
1217
+
1218
+ }
1219
+
1220
+ const visibility = shadow( originFor( back, lightShadowOrigin( direction ) ), direction, float( 1e20 ) );
1221
+
1222
+ If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1223
+
1050
1224
  // Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
1051
- const baseContribution = radiance.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightPdf, 1e-10 ) );
1225
+ const baseContribution = skyLight();
1052
1226
  totalContribution.addAssign( baseContribution.mul( visibility ) );
1053
1227
  If( wantUnoccluded, () => {
1054
1228
 
@@ -1066,6 +1240,8 @@ export const calculateDirectLightingUnified = Fn( ( [
1066
1240
 
1067
1241
  } );
1068
1242
 
1243
+ if ( pick ) totalContribution.addAssign( pick.trace( shadow ) );
1244
+
1069
1245
  // EMISSIVE TRIANGLE DIRECT LIGHTING
1070
1246
  // NOTE: Emissive triangle sampling is handled separately in pathtracer_core.fs
1071
1247
  // to bypass firefly suppression. Do not add it here to avoid double-counting.
@@ -1073,3 +1249,15 @@ export const calculateDirectLightingUnified = Fn( ( [
1073
1249
  return DirectLightingDual( { shadowed: totalContribution, unoccluded: unoccludedContribution } );
1074
1250
 
1075
1251
  } );
1252
+
1253
+ export const calculateDirectLightingUnified = /*@__PURE__*/ makeDirectLighting( false );
1254
+ export const calculateDirectLightingThroughSurfaces = /*@__PURE__*/ makeDirectLighting( true );
1255
+ const pickedLighting = new Map();
1256
+ /** The direct light with fewer shadow rays a hit; `variant` as lightPick takes it. */
1257
+ export const calculateDirectLightingPicked = ( throughSurfaces, variant ) => {
1258
+
1259
+ const key = `${throughSurfaces}:${variant}`;
1260
+ if ( ! pickedLighting.has( key ) ) pickedLighting.set( key, makeDirectLighting( throughSurfaces, variant ) );
1261
+ return pickedLighting.get( key );
1262
+
1263
+ };
@@ -1,11 +1,12 @@
1
1
  import {
2
- Fn, float, vec3,
2
+ Fn, float, vec3, dot,
3
3
  If, max, min, clamp, mix
4
4
  } from 'three/tsl';
5
5
 
6
6
  import { DotProducts, DFGResult, BaseFresnel } from './Struct.js';
7
7
  import { PI_INV, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso } from './Common.js';
8
8
  import { fresnelSchlick, fresnelDielectric, dielectricFresnelWeight } from './Fresnel.js';
9
+ import { materialLayers } from './SceneResources.js';
9
10
  import {
10
11
  DistributionGGX, SheenDistribution, VisibilitySheen, VisibilityGGXSmithCorrelated,
11
12
  sheenDirectionalAlbedo, evaluateSpecularDFG, baseFresnelParams,
@@ -21,27 +22,32 @@ import { evalIridescence } from './MaterialProperties.js';
21
22
  // Main Material Response Evaluation
22
23
  // -----------------------------------------------------------------------------
23
24
 
25
+ // A rough plain dielectric: its BSDF is a bare Lambert term (no specular lobe, no DFG budget).
26
+ const isLambertian = ( material ) => material.roughness.greaterThan( 0.98 )
27
+ .and( material.metalness.lessThan( 0.02 ) )
28
+ .and( material.transmission.equal( 0.0 ) )
29
+ .and( material.clearcoat.equal( 0.0 ) )
30
+ .and( material.sheen.equal( 0.0 ) )
31
+ .and( material.iridescence.equal( 0.0 ) );
32
+
24
33
  // Body of evaluateMaterialResponse taking precomputed dot products. Callers
25
34
  // that also need calculateMaterialPDF for the same (V, L, N) should share dots
26
35
  // to save one computeDotProducts call.
27
36
  // Roughness 0 marks an exact mirror (ShadeKernel): its delta lobe is left out of this BSDF, and
28
37
  // deltaOnly returns that lobe's reflectance instead, under the same sheen and coat attenuation.
29
- const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] ) => {
38
+ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ], builder ) => {
30
39
 
40
+ const layers = materialLayers( builder );
31
41
  const result = vec3( 0.0 ).toVar();
32
42
 
33
43
  // Early exit for purely diffuse materials (skip if iridescent)
34
44
  // `sheen` is in the guard because this fast path returns a bare Lambert term: without it a
35
45
  // rough sheen material lost its sheen lobe entirely — silently, since the fast path is only
36
46
  // taken above roughness 0.98.
37
- If( material.roughness.greaterThan( 0.98 )
38
- .and( material.metalness.lessThan( 0.02 ) )
39
- .and( material.transmission.equal( 0.0 ) )
40
- .and( material.clearcoat.equal( 0.0 ) )
41
- .and( material.sheen.equal( 0.0 ) )
42
- .and( material.iridescence.equal( 0.0 ) ), () => {
47
+ If( isLambertian( material ), () => {
43
48
 
44
- result.assign( material.color.rgb.mul( float( 1.0 ).sub( material.metalness ) ).mul( PI_INV ) );
49
+ result.assign( material.color.rgb.mul( float( 1.0 ).sub( material.metalness ) )
50
+ .mul( float( 1.0 ).sub( material.diffuseTransmission ) ).mul( PI_INV ) );
45
51
 
46
52
  } ).Else( () => {
47
53
 
@@ -51,7 +57,7 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
51
57
 
52
58
  // Modify material color for dispersive materials to enhance color separation
53
59
  const materialColor = material.color.rgb.toVar();
54
- If( material.dispersion.greaterThan( 0.0 ).and( material.transmission.greaterThan( 0.5 ) ), () => {
60
+ if ( layers.dispersion ) If( material.dispersion.greaterThan( 0.0 ).and( material.transmission.greaterThan( 0.5 ) ), () => {
55
61
 
56
62
  // For highly dispersive transmissive materials, boost color saturation
57
63
  const dispersionEffect = clamp( material.dispersion.mul( 0.1 ), 0.0, 0.8 );
@@ -67,7 +73,7 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
67
73
  } );
68
74
 
69
75
  // Add iridescence effect if enabled
70
- If( material.iridescence.greaterThan( 0.0 ), () => {
76
+ if ( layers.iridescence ) If( material.iridescence.greaterThan( 0.0 ), () => {
71
77
 
72
78
  // Per glTF KHR_materials_iridescence spec: use max thickness when no texture
73
79
  const thickness = material.iridescenceThicknessRange.y;
@@ -78,29 +84,29 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
78
84
 
79
85
  // Dielectric on the exact Fresnel curve, metal and iridescence on Schlick.
80
86
  const Fd = mix( bf.f0, vec3( bf.f90 ), dielectricFresnelWeight( dots.VoH, bf.eta ) );
81
- const F = mix(
82
- mix( Fd, fresnelSchlick( dots.VoH, bf.F0m ), material.metalness ),
83
- fresnelSchlick( dots.VoH, iridF ),
84
- material.iridescence,
85
- ).toVar();
87
+ const Fbase = mix( Fd, fresnelSchlick( dots.VoH, bf.F0m ), material.metalness );
88
+ const F = ( layers.iridescence ? mix( Fbase, fresnelSchlick( dots.VoH, iridF ), material.iridescence ) : Fbase ).toVar();
86
89
 
87
90
  // Single-scatter specular BRDF (anisotropic when material.anisotropy > 0; the aniso
88
91
  // visibility term already carries the 1/(4·NoV·NoL) denominator)
89
92
  const specularSS = vec3( 0.0 ).toVar();
90
- If( material.anisotropy.greaterThan( 0.0 ), () => {
93
+ const isotropic = () => {
94
+
95
+ const D = DistributionGGX( dots.NoH, material.roughness );
96
+ const Vis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, material.roughness );
97
+ specularSS.assign( D.mul( Vis ).mul( F ) );
98
+
99
+ };
100
+
101
+ if ( layers.anisotropy ) If( material.anisotropy.greaterThan( 0.0 ), () => {
91
102
 
92
103
  const a = computeAnisoAlphas( material.roughness, material.anisotropy );
93
104
  const Da = DistributionGGXAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH );
94
105
  const Va = VisibilityGGXAniso( a.x, a.y, dots.ToV, dots.BoV, dots.ToL, dots.BoL, dots.NoV, dots.NoL );
95
106
  specularSS.assign( F.mul( Da.mul( Va ) ) );
96
107
 
97
- } ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
98
-
99
- const D = DistributionGGX( dots.NoH, material.roughness );
100
- const Vis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, material.roughness );
101
- specularSS.assign( D.mul( Vis ).mul( F ) );
102
-
103
- } );
108
+ } ).ElseIf( material.roughness.greaterThan( 0.0 ), isotropic );
109
+ else If( material.roughness.greaterThan( 0.0 ), isotropic );
104
110
 
105
111
  // Compensation factor and total directional albedo from the same table fetch.
106
112
  const dfg = DFGResult.wrap( evaluateSpecularDFG(
@@ -112,15 +118,17 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
112
118
  // Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
113
119
  // Transmission removes energy from diffuse just as metalness does — KHR_materials_transmission
114
120
  // defines transmission as replacing the diffuse component.
121
+ // Diffuse transmission takes its share of the diffuse lobe to the other side (evaluateDiffuseTransmission).
115
122
  const kD = vec3( 1.0 ).sub( dfg.E_total )
116
123
  .mul( float( 1.0 ).sub( material.metalness ) )
117
- .mul( float( 1.0 ).sub( material.transmission ) );
124
+ .mul( float( 1.0 ).sub( material.transmission ) )
125
+ .mul( float( 1.0 ).sub( material.diffuseTransmission ) );
118
126
  const diffuse = deltaOnly ? vec3( 0.0 ) : kD.mul( materialColor ).mul( PI_INV );
119
127
 
120
128
  const baseLayer = diffuse.add( specular ).toVar();
121
129
 
122
- // Optimize sheen calculation
123
- If( material.sheen.greaterThan( 0.0 ), () => {
130
+ if ( ! layers.sheen ) result.assign( baseLayer );
131
+ else If( material.sheen.greaterThan( 0.0 ), () => {
124
132
 
125
133
  // D · V, not · NoL — callers apply the cosine when they integrate.
126
134
  const sheenDist = SheenDistribution( dots.NoH, material.sheenRoughness );
@@ -143,7 +151,7 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
143
151
 
144
152
  // The coat lives here, not in a separate layered-BRDF function: every strategy has to
145
153
  // evaluate the same integrand or MIS is biased however good the weights are.
146
- If( material.clearcoat.greaterThan( 0.0 ), () => {
154
+ if ( layers.clearcoat ) If( material.clearcoat.greaterThan( 0.0 ), () => {
147
155
 
148
156
  const ccRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
149
157
  const ccF0 = vec3( 0.04 ); // IOR 1.5
@@ -176,12 +184,86 @@ const makeEvaluateMaterialResponse = ( deltaOnly ) => Fn( ( [ material, dots ] )
176
184
  export const evaluateMaterialResponseFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( false );
177
185
  export const evaluateSpecularDeltaFromDots = /*@__PURE__*/ makeEvaluateMaterialResponse( true );
178
186
 
187
+ /**
188
+ * KHR_materials_diffuse_transmission's BTDF, for light arriving from the far side of a thin surface: the diffuse
189
+ * budget the reflected diffuse lobe has (what the specular lobes leave, less metal and specular transmission) × the
190
+ * transmitted share × its colour / π, under the same sheen and coat attenuation. Zero when the material has none.
191
+ */
192
+ export const evaluateDiffuseTransmission = Fn( ( [ material, NoV ], builder ) => {
193
+
194
+ const layers = materialLayers( builder );
195
+ const result = vec3( 0.0 ).toVar();
196
+
197
+ if ( layers.diffuseTransmission ) If( material.diffuseTransmission.greaterThan( 0.0 ), () => {
198
+
199
+ const kD = vec3( float( 1.0 ).sub( material.metalness ).mul( float( 1.0 ).sub( material.transmission ) ) ).toVar();
200
+
201
+ If( isLambertian( material ).not(), () => {
202
+
203
+ const bf = BaseFresnel.wrap( baseFresnelParams( material, material.color.rgb ) );
204
+ const F0 = bf.F0.toVar();
205
+ const iridF = vec3( 0.0 ).toVar();
206
+ if ( layers.iridescence ) If( material.iridescence.greaterThan( 0.0 ), () => {
207
+
208
+ // No half vector across the surface: the iridescent F0 at normal incidence to the view.
209
+ iridF.assign( evalIridescence( float( 1.0 ), material.iridescenceIOR, NoV, material.iridescenceThicknessRange.y, F0 ) );
210
+ F0.assign( mix( F0, iridF, material.iridescence ) );
211
+
212
+ } );
213
+ const dfg = DFGResult.wrap( evaluateSpecularDFG(
214
+ bf.f0, bf.f90, bf.eta, bf.F0m, material.metalness, iridF, material.iridescence, F0,
215
+ NoV, material.roughness,
216
+ ) );
217
+ kD.mulAssign( vec3( 1.0 ).sub( dfg.E_total ) );
218
+
219
+ } );
220
+
221
+ result.assign( kD.mul( material.diffuseTransmission ).mul( material.diffuseTransmissionColor ).mul( PI_INV ) );
222
+
223
+ if ( layers.sheen ) If( material.sheen.greaterThan( 0.0 ), () => {
224
+
225
+ const sheenE = sheenDirectionalAlbedo( NoV, material.sheenRoughness );
226
+ result.mulAssign( vec3( 1.0 ).sub( clamp( material.sheenColor.mul( material.sheen ).mul( sheenE ), vec3( 0.0 ), vec3( 1.0 ) ) ) );
227
+
228
+ } );
229
+
230
+ if ( layers.clearcoat ) If( material.clearcoat.greaterThan( 0.0 ), () => {
231
+
232
+ const ccDfg = DFGResult.wrap( evaluateSpecularDFG(
233
+ vec3( 0.04 ), float( 1.0 ), float( 1.5 ), vec3( 0.0 ), float( 0.0 ), vec3( 0.0 ), float( 0.0 ), vec3( 0.04 ),
234
+ NoV, max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS ),
235
+ ) );
236
+ result.mulAssign( vec3( 1.0 ).sub( ccDfg.E_total.mul( material.clearcoat ) ) );
237
+
238
+ } );
239
+
240
+ } );
241
+
242
+ return result;
243
+
244
+ } );
245
+
179
246
  // Wrapper that computes dot products internally. Use this when you don't already
180
247
  // have dots; otherwise prefer evaluateMaterialResponseFromDots to share the work.
181
- export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ] ) => {
248
+ // L below N reaches only the diffuse transmission lobe.
249
+ export const evaluateMaterialResponse = Fn( ( [ V, L, N, material ], builder ) => {
250
+
251
+ const result = vec3( 0.0 ).toVar();
252
+ const reflected = () => {
182
253
 
183
- const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
184
- return evaluateMaterialResponseFromDots( material, dots );
254
+ const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
255
+ result.assign( evaluateMaterialResponseFromDots( material, dots ) );
256
+
257
+ };
258
+
259
+ if ( ! materialLayers( builder ).diffuseTransmission ) reflected();
260
+ else If( dot( N, L ).lessThan( 0.0 ).and( material.diffuseTransmission.greaterThan( 0.0 ) ), () => {
261
+
262
+ result.assign( evaluateDiffuseTransmission( material, max( dot( N, V ), 0.001 ) ) );
263
+
264
+ } ).Else( reflected );
265
+
266
+ return result;
185
267
 
186
268
  } );
187
269