rayzee 9.4.1 → 9.5.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 (145) hide show
  1. package/README.md +226 -25
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-xuUJiklP.js +4301 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-C5PKjJoT.js +934 -0
  6. package/dist/ColorManagement-CheBCPaW.js +1488 -0
  7. package/dist/EngineDefaults-DH3bI5t-.js +844 -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-BnHghprG.js +97 -0
  12. package/dist/{NeuralRetouch-vkdXXyK2.js → NeuralRetouch-CW11yLZ_.js} +8 -6
  13. package/dist/{NeuralSuperRes-FVqRJKQV.js → NeuralSuperRes-BekaecYg.js} +55 -120
  14. package/dist/PathTracer-Ccl9KOuF.js +29729 -0
  15. package/dist/PhysicalSky-Dv2PMFGW.js +1219 -0
  16. package/dist/RayzeeRenderer-BQxnzCTD.js +3542 -0
  17. package/dist/SolarLimb-ken-JbDK.js +721 -0
  18. package/dist/ToneMapGPU-BHi5JzIq.js +177 -0
  19. package/dist/ViewTransforms-D0rTLBsX.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 +2266 -44125
  33. package/dist/rayzee.umd.js +485 -473
  34. package/dist/sceneCachePolicy-BmwYOYDu.js +183 -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/EngineDefaults.js +23 -14
  41. package/src/EngineEvents.js +2 -0
  42. package/src/EngineIssues.js +1 -0
  43. package/src/PathTracerApp.js +617 -4016
  44. package/src/Pipeline/PipelineContext.js +0 -4
  45. package/src/Platform.js +36 -0
  46. package/src/Processor/ArchiveCache.js +1 -1
  47. package/src/Processor/ArchiveImporter.js +1259 -0
  48. package/src/Processor/AssetLoader.js +322 -1746
  49. package/src/Processor/AtmosphereModel.js +1 -2
  50. package/src/Processor/BVHBuilder.js +4 -95
  51. package/src/Processor/EnvironmentExactTable.js +187 -0
  52. package/src/Processor/EquirectHDRInfo.js +30 -223
  53. package/src/Processor/FileFormats.js +116 -0
  54. package/src/Processor/GLTFDecoders.js +85 -0
  55. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  56. package/src/Processor/GeometryExtractor.js +12 -1
  57. package/src/Processor/IESParser.js +47 -35
  58. package/src/Processor/KernelManager.js +35 -2
  59. package/src/Processor/MaterialPacking.js +8 -7
  60. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  61. package/src/Processor/PBRT/PBRTParser.js +30 -17
  62. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  63. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  64. package/src/Processor/PBRT/PFM.js +59 -0
  65. package/src/Processor/PackedRayBuffer.js +116 -9
  66. package/src/Processor/ParallelBVHBuilder.js +3 -6
  67. package/src/Processor/PhysicalSky.js +5 -4
  68. package/src/Processor/PlatformImageLoader.js +50 -6
  69. package/src/Processor/QueueManager.js +12 -3
  70. package/src/Processor/ReinsertionOptimizer.js +2 -2
  71. package/src/Processor/SceneProcessor.js +15 -55
  72. package/src/Processor/ShaderBuilder.js +6 -16
  73. package/src/Processor/SimpleSky.js +1 -62
  74. package/src/Processor/SolarLimb.js +3 -0
  75. package/src/Processor/TextureCreator.js +1 -1
  76. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  77. package/src/Processor/Workers/BVHWorker.js +5 -18
  78. package/src/Processor/Workers/CDFWorker.js +6 -183
  79. package/src/Processor/archiveFormats.js +7 -0
  80. package/src/Processor/utils.js +2 -1
  81. package/src/RayzeeRenderer.js +3902 -0
  82. package/src/RenderSettings.js +49 -41
  83. package/src/Stages/ASVGF.js +5 -7
  84. package/src/Stages/AutoExposure.js +3 -12
  85. package/src/Stages/Compositor.js +8 -23
  86. package/src/Stages/NRD.js +9 -2
  87. package/src/Stages/NormalDepth.js +9 -6
  88. package/src/Stages/PathTracer.js +254 -38
  89. package/src/Stages/PathTracerStage.js +11 -16
  90. package/src/Storage/BLASCache.js +1 -1
  91. package/src/Storage/CDFCache.js +18 -17
  92. package/src/Storage/DownloadCache.js +1 -1
  93. package/src/Storage/SceneGraphCodec.js +46 -3
  94. package/src/Storage/SpillStore.js +1 -1
  95. package/src/Storage/StorageManager.js +2 -4
  96. package/src/Storage/areas.js +4 -0
  97. package/src/TSL/BVHTraversal.js +62 -24
  98. package/src/TSL/Bidirectional.js +204 -0
  99. package/src/TSL/BidirectionalLamps.js +132 -0
  100. package/src/TSL/Clearcoat.js +15 -6
  101. package/src/TSL/Common.js +19 -14
  102. package/src/TSL/ConnectKernel.js +147 -0
  103. package/src/TSL/Debugger.js +1 -1
  104. package/src/TSL/Displacement.js +4 -3
  105. package/src/TSL/EmissiveSampling.js +64 -17
  106. package/src/TSL/Environment.js +77 -106
  107. package/src/TSL/EnvironmentCDF.js +85 -39
  108. package/src/TSL/ExtendKernel.js +8 -3
  109. package/src/TSL/FinalWriteKernel.js +4 -4
  110. package/src/TSL/GenerateKernel.js +18 -4
  111. package/src/TSL/HitFacet.js +35 -5
  112. package/src/TSL/LightBVHSampling.js +40 -9
  113. package/src/TSL/LightGenerateKernel.js +293 -0
  114. package/src/TSL/LightGuide.js +198 -0
  115. package/src/TSL/LightSplatKernel.js +165 -0
  116. package/src/TSL/LightsCore.js +13 -38
  117. package/src/TSL/LightsDirect.js +28 -29
  118. package/src/TSL/LightsIndirect.js +3 -1
  119. package/src/TSL/LightsSampling.js +90 -38
  120. package/src/TSL/MaterialEvaluation.js +113 -31
  121. package/src/TSL/MaterialProperties.js +44 -17
  122. package/src/TSL/MaterialTransmission.js +52 -47
  123. package/src/TSL/MergeKernel.js +218 -0
  124. package/src/TSL/PathTracerCore.js +65 -29
  125. package/src/TSL/Random.js +4 -0
  126. package/src/TSL/SceneResources.js +51 -0
  127. package/src/TSL/ShadeKernel.js +881 -103
  128. package/src/TSL/Struct.js +10 -0
  129. package/src/TSL/Sun.js +1 -1
  130. package/src/TSL/TextureSampling.js +65 -60
  131. package/src/addons/archives.js +14 -0
  132. package/src/addons/bidirectional.js +13 -0
  133. package/src/addons/color.js +32 -0
  134. package/src/addons/formats.js +16 -0
  135. package/src/addons/physicalSky.js +14 -0
  136. package/src/addons/storage.js +19 -0
  137. package/src/core.js +35 -0
  138. package/src/index.js +3 -1
  139. package/src/integrators/BidirectionalIntegrator.js +832 -0
  140. package/src/managers/EnvironmentManager.js +94 -91
  141. package/src/managers/MaterialDataManager.js +42 -0
  142. package/src/managers/UniformManager.js +0 -1
  143. package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
  144. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  145. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -37,6 +37,7 @@ import {
37
37
  select,
38
38
  If,
39
39
  Loop,
40
+ sampler,
40
41
  } from 'three/tsl';
41
42
 
42
43
  import {
@@ -71,8 +72,8 @@ import {
71
72
  } from './LightsDirect.js';
72
73
 
73
74
  import { traverseBVHShadow } from './BVHTraversal.js';
74
- import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
75
- import { calculateBSDFSamplingPDFFromMaterial } from './MaterialProperties.js';
75
+ import { evaluateMaterialResponseFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
76
+ import { calculateBSDFSamplingPDFFromMaterial, diffuseTransmissionWeight, diffuseTransmissionPdf } from './MaterialProperties.js';
76
77
  import { getRandomSample1D, getRandomSample2D, SAMPLER_DIM_AUX_BASE } from './Random.js';
77
78
  import {
78
79
  PI,
@@ -84,9 +85,7 @@ import {
84
85
  offsetRayOrigin,
85
86
  SHADOW_END,
86
87
  } from './Common.js';
87
- import {
88
- sampleEquirectProbability,
89
- } from './Environment.js';
88
+ import { sampleEnvironment, sampleEnvironmentExact } from './Environment.js';
90
89
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
91
90
  import { sunRadianceToward, sampleSunDisc } from './Sun.js';
92
91
 
@@ -757,8 +756,20 @@ export const calculateMaterialPDFFromDots = Fn( ( [ material, dots ] ) => {
757
756
 
758
757
  export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material ] ) => {
759
758
 
760
- const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
761
- return calculateMaterialPDFFromDots( material, dots );
759
+ const pdf = float( 0.0 ).toVar();
760
+
761
+ If( dot( normal, lightDir ).lessThan( 0.0 ).and( material.diffuseTransmission.greaterThan( 0.0 ) ), () => {
762
+
763
+ pdf.assign( max( diffuseTransmissionPdf( diffuseTransmissionWeight( material ), dot( normal, lightDir ).negate() ), 1e-8 ) );
764
+
765
+ } ).Else( () => {
766
+
767
+ const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, lightDir, material ) );
768
+ pdf.assign( calculateMaterialPDFFromDots( material, dots ) );
769
+
770
+ } );
771
+
772
+ return pdf;
762
773
 
763
774
  } );
764
775
 
@@ -766,8 +777,9 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
766
777
  // Unified Direct Lighting System
767
778
  // =============================================================================
768
779
 
769
- // Optimized direct lighting function with importance-based sampling and better MIS
770
- export const calculateDirectLightingUnified = Fn( ( [
780
+ // Optimized direct lighting function with importance-based sampling and better MIS. `throughSurfaces` (compile
781
+ // time) adds light from behind a surface that passes it through diffusely; without it the code is exactly as before.
782
+ const makeDirectLighting = ( throughSurfaces ) => Fn( ( [
771
783
  // Surface hit data
772
784
  hitPoint, hitNormal, geomNormal, material,
773
785
  // View direction
@@ -791,7 +803,7 @@ export const calculateDirectLightingUnified = Fn( ( [
791
803
  // Environment resources
792
804
  envTexture, environmentIntensity, envMatrix,
793
805
  envCDFTexture,
794
- envTotalSum, envCompensationDelta, envResolution,
806
+ envTotalSum, envResolution,
795
807
  enableEnvironmentLight,
796
808
  // Shadow catcher: when true, also accumulate the unoccluded (visibility=1) reference
797
809
  // at every light/env site so the caller can form a shadow ratio. Dead path otherwise.
@@ -817,6 +829,46 @@ export const calculateDirectLightingUnified = Fn( ( [
817
829
  traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer )
818
830
  );
819
831
 
832
+ // Light from behind a surface that passes light through diffusely: its shadow ray leaves from the other side,
833
+ // and the BSDF is the transmission lobe.
834
+ const throughSurface = L => ( throughSurfaces
835
+ ? material.diffuseTransmission.greaterThan( 0.0 ).and( dot( hitNormal, L ).lessThan( 0.0 ) ).and( dot( L, geomNormal ).lessThan( 0.0 ) )
836
+ : tslBool( false ) );
837
+ const backOrigin = throughSurfaces ? offsetRayOrigin( hitPoint, facetNormal.negate() ) : null;
838
+ const inFront = L => dot( hitNormal, L ).greaterThan( 0.0 ).and( isDirectionValid( { direction: L, surfaceNormal: geomNormal } ) );
839
+ const reachesSurface = ( L, back ) => ( throughSurfaces ? back.or( inFront( L ) ) : inFront( L ) );
840
+ const cosineTo = L => ( throughSurfaces ? abs( dot( hitNormal, L ) ) : max( float( 0.0 ), dot( hitNormal, L ) ) );
841
+ const originFor = ( back, front ) => ( throughSurfaces ? select( back, backOrigin, front ) : front );
842
+ const reflection = L => {
843
+
844
+ // Share H + dot products between BRDF eval and PDF — otherwise each
845
+ // would recompute normalize(V+L) + 5 dot products independently.
846
+ const dots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, L, material ) );
847
+ return { value: evaluateMaterialResponseFromDots( material, dots ), pdf: calculateMaterialPDFFromDots( material, dots ).toVar() };
848
+
849
+ };
850
+
851
+ const bsdfToward = ( L, back ) => {
852
+
853
+ if ( ! throughSurfaces ) return reflection( L );
854
+ const value = vec3( 0.0 ).toVar();
855
+ const pdf = float( 0.0 ).toVar();
856
+ If( back, () => {
857
+
858
+ value.assign( evaluateDiffuseTransmission( material, max( dot( hitNormal, viewDir ), 0.001 ) ) );
859
+ pdf.assign( max( diffuseTransmissionPdf( diffuseTransmissionWeight( material ), dot( hitNormal, L ).negate() ), 1e-8 ) );
860
+
861
+ } ).Else( () => {
862
+
863
+ const r = reflection( L );
864
+ value.assign( r.value );
865
+ pdf.assign( r.pdf );
866
+
867
+ } );
868
+ return { value, pdf };
869
+
870
+ };
871
+
820
872
 
821
873
  // NEE and BSDF-hit both run at every vertex, one sample each, so the MIS weights are the
822
874
  // plain power heuristic over the raw densities. Picking one strategy stochastically and
@@ -849,24 +901,21 @@ export const calculateDirectLightingUnified = Fn( ( [
849
901
 
850
902
  If( lightSample.valid.and( lightSample.pdf.greaterThan( 0.0 ) ), () => {
851
903
 
852
- const NoL = max( float( 0.0 ), dot( hitNormal, lightSample.direction ) ).toVar();
904
+ const back = throughSurface( lightSample.direction ).toVar();
905
+ const NoL = cosineTo( lightSample.direction ).toVar();
853
906
 
854
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: geomNormal } ) ), () => {
907
+ If( reachesSurface( lightSample.direction, back ), () => {
855
908
 
856
909
  // Light geometry is measured from the hit point; only the visibility ray starts on the lifted
857
910
  // origin, aimed at the sampled point and stopped a relative hair short of it.
858
- const lightOrigin = lightShadowOrigin( lightSample.direction ).toVar();
911
+ const lightOrigin = originFor( back, lightShadowOrigin( lightSample.direction ) ).toVar();
859
912
  const toSample = hitPoint.add( lightSample.direction.mul( lightSample.distance ) ).sub( lightOrigin ).toVar();
860
913
  const shadowDistance = length( toSample ).toVar();
861
914
  const visibility = shadow( lightOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
862
915
 
863
916
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
864
917
 
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();
918
+ const { value: brdfValue, pdf: bPdf } = bsdfToward( lightSample.direction, back );
870
919
 
871
920
  // Power heuristic only for area lights — they are the only type the BRDF
872
921
  // path can intersect, so elsewhere MIS would just delete energy.
@@ -902,9 +951,10 @@ export const calculateDirectLightingUnified = Fn( ( [
902
951
 
903
952
  If( brdfSamplePdf.greaterThan( 0.0 ).and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
904
953
 
905
- const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
954
+ const back = throughSurface( brdfSampleDirection ).toVar();
955
+ const NoL = cosineTo( brdfSampleDirection ).toVar();
906
956
 
907
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: geomNormal } ) ), () => {
957
+ If( reachesSurface( brdfSampleDirection, back ), () => {
908
958
 
909
959
  // Nearest hit — the ray stops at the first emitter, so a brighter one behind it is
910
960
  // the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
@@ -936,9 +986,10 @@ export const calculateDirectLightingUnified = Fn( ( [
936
986
  If( nearestLight.greaterThanEqual( int( 0 ) ), () => {
937
987
 
938
988
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
939
- const toLight = hitPoint.add( brdfSampleDirection.mul( nearestT ) ).sub( rayOrigin ).toVar();
989
+ const hitOrigin = originFor( back, rayOrigin ).toVar();
990
+ const toLight = hitPoint.add( brdfSampleDirection.mul( nearestT ) ).sub( hitOrigin ).toVar();
940
991
  const shadowDistance = length( toLight ).toVar();
941
- const visibility = shadow( rayOrigin, toLight.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
992
+ const visibility = shadow( hitOrigin, toLight.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
942
993
 
943
994
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
944
995
 
@@ -1011,33 +1062,31 @@ export const calculateDirectLightingUnified = Fn( ( [
1011
1062
  radiance.assign( sunRadianceToward( direction, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ) );
1012
1063
  lightPdf.assign( select( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ), float( 1.0 ).div( sunParams.y ), float( 0.0 ) ) );
1013
1064
 
1014
- } ).Else( () => {
1065
+ } ).ElseIf( envTotalSum.greaterThan( 0.0 ), () => {
1015
1066
 
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 );
1067
+ // Drawn from the table its density is read back from; a total of 0 is no table, or a black map.
1068
+ const drawn = sampleEnvironmentExact( envCDFTexture, envMatrix, envResolution,
1069
+ getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ) );
1070
+ direction.assign( drawn.direction );
1071
+ radiance.assign( sampleEnvironment( {
1072
+ tex: envTexture, samp: sampler( envTexture ), direction, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight: float( 1.0 ),
1073
+ } ).xyz );
1074
+ lightPdf.assign( drawn.pdf );
1025
1075
 
1026
1076
  } );
1027
1077
 
1028
1078
  If( lightPdf.greaterThan( 0.0 ), () => {
1029
1079
 
1030
- const NoL = max( float( 0.0 ), dot( hitNormal, direction ) ).toVar();
1080
+ const back = throughSurface( direction ).toVar();
1081
+ const NoL = cosineTo( direction ).toVar();
1031
1082
 
1032
- If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction, surfaceNormal: geomNormal } ) ), () => {
1083
+ If( reachesSurface( direction, back ), () => {
1033
1084
 
1034
- const visibility = shadow( lightShadowOrigin( direction ), direction, float( 1e20 ) );
1085
+ const visibility = shadow( originFor( back, lightShadowOrigin( direction ) ), direction, float( 1e20 ) );
1035
1086
 
1036
1087
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1037
1088
 
1038
- const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, direction, material ) );
1039
- const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
1040
- const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
1089
+ const { value: brdfValue, pdf: bPdf } = bsdfToward( direction, back );
1041
1090
 
1042
1091
  // The map pairs with the balance heuristic — optimal for its MIS-compensated pdf
1043
1092
  // (Karlík et al. 2019); the sun, a small bright source, with the power heuristic.
@@ -1073,3 +1122,6 @@ export const calculateDirectLightingUnified = Fn( ( [
1073
1122
  return DirectLightingDual( { shadowed: totalContribution, unoccluded: unoccludedContribution } );
1074
1123
 
1075
1124
  } );
1125
+
1126
+ export const calculateDirectLightingUnified = /*@__PURE__*/ makeDirectLighting( false );
1127
+ export const calculateDirectLightingThroughSurfaces = /*@__PURE__*/ makeDirectLighting( true );
@@ -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
 
@@ -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 ] ) => {