rayzee 9.4.2 → 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 (144) hide show
  1. package/README.md +223 -21
  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 -44079
  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 +22 -8
  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/SolarLimb.js +3 -0
  74. package/src/Processor/TextureCreator.js +1 -1
  75. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  76. package/src/Processor/Workers/BVHWorker.js +5 -18
  77. package/src/Processor/Workers/CDFWorker.js +6 -183
  78. package/src/Processor/archiveFormats.js +7 -0
  79. package/src/Processor/utils.js +2 -1
  80. package/src/RayzeeRenderer.js +3902 -0
  81. package/src/RenderSettings.js +49 -41
  82. package/src/Stages/ASVGF.js +5 -7
  83. package/src/Stages/AutoExposure.js +3 -12
  84. package/src/Stages/Compositor.js +8 -23
  85. package/src/Stages/NRD.js +9 -2
  86. package/src/Stages/NormalDepth.js +9 -6
  87. package/src/Stages/PathTracer.js +254 -38
  88. package/src/Stages/PathTracerStage.js +11 -16
  89. package/src/Storage/BLASCache.js +1 -1
  90. package/src/Storage/CDFCache.js +18 -17
  91. package/src/Storage/DownloadCache.js +1 -1
  92. package/src/Storage/SceneGraphCodec.js +46 -3
  93. package/src/Storage/SpillStore.js +1 -1
  94. package/src/Storage/StorageManager.js +2 -4
  95. package/src/Storage/areas.js +4 -0
  96. package/src/TSL/BVHTraversal.js +62 -24
  97. package/src/TSL/Bidirectional.js +204 -0
  98. package/src/TSL/BidirectionalLamps.js +132 -0
  99. package/src/TSL/Clearcoat.js +15 -6
  100. package/src/TSL/Common.js +19 -14
  101. package/src/TSL/ConnectKernel.js +147 -0
  102. package/src/TSL/Debugger.js +1 -1
  103. package/src/TSL/Displacement.js +4 -3
  104. package/src/TSL/EmissiveSampling.js +64 -17
  105. package/src/TSL/Environment.js +77 -106
  106. package/src/TSL/EnvironmentCDF.js +85 -39
  107. package/src/TSL/ExtendKernel.js +8 -3
  108. package/src/TSL/FinalWriteKernel.js +4 -4
  109. package/src/TSL/GenerateKernel.js +18 -4
  110. package/src/TSL/HitFacet.js +35 -5
  111. package/src/TSL/LightBVHSampling.js +40 -9
  112. package/src/TSL/LightGenerateKernel.js +293 -0
  113. package/src/TSL/LightGuide.js +198 -0
  114. package/src/TSL/LightSplatKernel.js +165 -0
  115. package/src/TSL/LightsCore.js +13 -38
  116. package/src/TSL/LightsDirect.js +28 -29
  117. package/src/TSL/LightsIndirect.js +3 -1
  118. package/src/TSL/LightsSampling.js +90 -38
  119. package/src/TSL/MaterialEvaluation.js +113 -31
  120. package/src/TSL/MaterialProperties.js +44 -17
  121. package/src/TSL/MaterialTransmission.js +52 -47
  122. package/src/TSL/MergeKernel.js +218 -0
  123. package/src/TSL/PathTracerCore.js +65 -29
  124. package/src/TSL/Random.js +4 -0
  125. package/src/TSL/SceneResources.js +51 -0
  126. package/src/TSL/ShadeKernel.js +881 -103
  127. package/src/TSL/Struct.js +10 -0
  128. package/src/TSL/Sun.js +1 -1
  129. package/src/TSL/TextureSampling.js +65 -60
  130. package/src/addons/archives.js +14 -0
  131. package/src/addons/bidirectional.js +13 -0
  132. package/src/addons/color.js +32 -0
  133. package/src/addons/formats.js +16 -0
  134. package/src/addons/physicalSky.js +14 -0
  135. package/src/addons/storage.js +19 -0
  136. package/src/core.js +35 -0
  137. package/src/index.js +3 -1
  138. package/src/integrators/BidirectionalIntegrator.js +832 -0
  139. package/src/managers/EnvironmentManager.js +85 -33
  140. package/src/managers/MaterialDataManager.js +42 -0
  141. package/src/managers/UniformManager.js +0 -1
  142. package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
  143. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  144. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The analytic lights as bidirectional sources: the camera side's lamp choice, made as the path tracer
3
+ * makes it, and the densities a light path needs to weigh the strategies against it.
4
+ */
5
+
6
+ import { float, int, If, Loop, select, cos, max, min } from 'three/tsl';
7
+
8
+ import { struct } from './patches.js';
9
+ import {
10
+ DirectionalLight, AreaLight, PointLight, SpotLight,
11
+ getDirectionalLight, getAreaLight, getPointLight, getSpotLight,
12
+ LIGHT_TYPE_DIRECTIONAL, LIGHT_TYPE_AREA, LIGHT_TYPE_POINT, sphQuadSolidAngle,
13
+ } from './LightsCore.js';
14
+ import {
15
+ calculateDirectionalLightImportance, estimateLightImportance, calculatePointLightImportance, calculateSpotLightImportance,
16
+ } from './LightsDirect.js';
17
+ import { SAMPLER_DIM_AUX_BASE } from './Random.js';
18
+ import { PI } from './Common.js';
19
+
20
+ export const LampPick = struct( { kind: 'int', index: 'int', pdf: 'float', total: 'float' } );
21
+
22
+ /** @param lamps { directional, numDirectional, area, numArea, point, numPoint, spot, numSpot } */
23
+ export const lampCount = ( lamps ) => lamps.numDirectional.add( lamps.numArea ).add( lamps.numPoint ).add( lamps.numSpot );
24
+
25
+ export const lampTypeAt = ( lamps, flat ) => select( flat.lessThan( lamps.numDirectional ), int( LIGHT_TYPE_DIRECTIONAL ),
26
+ select( flat.lessThan( lamps.numDirectional.add( lamps.numArea ) ), int( LIGHT_TYPE_AREA ),
27
+ select( flat.lessThan( lamps.numDirectional.add( lamps.numArea ).add( lamps.numPoint ) ), int( LIGHT_TYPE_POINT ), int( 3 ) ) ) );
28
+
29
+ const typeStart = ( lamps, type ) => select( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), int( 0 ),
30
+ select( type.equal( int( LIGHT_TYPE_AREA ) ), lamps.numDirectional,
31
+ select( type.equal( int( LIGHT_TYPE_POINT ) ), lamps.numDirectional.add( lamps.numArea ),
32
+ lamps.numDirectional.add( lamps.numArea ).add( lamps.numPoint ) ) ) );
33
+
34
+ // The path tracer's importance (LightsDirect.js) without its bounce-depth factor, which a light path cannot know.
35
+ const walkLamps = ( lamps, hitPoint, N, material, visit ) => {
36
+
37
+ const each = ( type, count, read ) => If( count.greaterThan( int( 0 ) ), () => {
38
+
39
+ Loop( { start: int( 0 ), end: count, type: 'int', condition: '<' }, ( { i } ) => {
40
+
41
+ visit( int( type ), i, read( i ).toVar() );
42
+
43
+ } );
44
+
45
+ } );
46
+ each( LIGHT_TYPE_DIRECTIONAL, lamps.numDirectional, ( i ) =>
47
+ calculateDirectionalLightImportance( DirectionalLight.wrap( getDirectionalLight( lamps.directional, i ) ), N, material, int( 0 ) ) );
48
+ each( LIGHT_TYPE_AREA, lamps.numArea, ( i ) => {
49
+
50
+ const light = AreaLight.wrap( getAreaLight( lamps.area, i ) );
51
+ return select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, hitPoint, N, material ), float( 0.0 ) );
52
+
53
+ } );
54
+ each( LIGHT_TYPE_POINT, lamps.numPoint, ( i ) =>
55
+ calculatePointLightImportance( PointLight.wrap( getPointLight( lamps.point, i ) ), hitPoint, N, material ) );
56
+ each( 3, lamps.numSpot, ( i ) =>
57
+ calculateSpotLightImportance( SpotLight.wrap( getSpotLight( lamps.spot, i ) ), hitPoint, N, material ) );
58
+
59
+ };
60
+
61
+ /**
62
+ * One lamp, as the path tracer's reservoir picks it, on the same dimensions; uniform when nothing has importance.
63
+ * `sample1D( dim )` draws a 1D variate, `u` is the fallback's.
64
+ */
65
+ export function pickLamp( lamps, hitPoint, N, material, u, sample1D ) {
66
+
67
+ const type = int( - 1 ).toVar();
68
+ const index = int( - 1 ).toVar();
69
+ const chosen = float( 0.0 ).toVar();
70
+ const total = float( 0.0 ).toVar();
71
+ walkLamps( lamps, hitPoint, N, material, ( t, i, w ) => {
72
+
73
+ total.addAssign( w );
74
+ If( w.greaterThan( 0.0 ).and( sample1D( int( SAMPLER_DIM_AUX_BASE + 64 ).add( typeStart( lamps, t ) ).add( i ) ).mul( total ).lessThan( w ) ), () => {
75
+
76
+ type.assign( t );
77
+ index.assign( i );
78
+ chosen.assign( w );
79
+
80
+ } );
81
+
82
+ } );
83
+
84
+ const count = lampCount( lamps ).toVar();
85
+ const pdf = chosen.div( max( total, 1e-10 ) ).toVar();
86
+ If( total.lessThanEqual( 0.0 ), () => {
87
+
88
+ const flat = min( int( u.mul( float( count ) ) ), count.sub( int( 1 ) ) ).toVar();
89
+ type.assign( lampTypeAt( lamps, flat ) );
90
+ index.assign( flat.sub( typeStart( lamps, type ) ) );
91
+ pdf.assign( float( 1.0 ).div( max( float( count ), 1.0 ) ) );
92
+
93
+ } );
94
+
95
+ return LampPick( { kind: type, index, pdf, total } );
96
+
97
+ }
98
+
99
+ /** The chance `pickLamp` at this point takes lamp (type, index). */
100
+ export function lampPickPdf( lamps, type, index, hitPoint, N, material ) {
101
+
102
+ const total = float( 0.0 ).toVar();
103
+ const own = float( 0.0 ).toVar();
104
+ walkLamps( lamps, hitPoint, N, material, ( t, i, w ) => {
105
+
106
+ total.addAssign( w );
107
+ If( t.equal( type ).and( i.equal( index ) ), () => {
108
+
109
+ own.assign( w );
110
+
111
+ } );
112
+
113
+ } );
114
+ return select( total.greaterThan( 0.0 ), own.div( total ), float( 1.0 ).div( max( float( lampCount( lamps ) ), 1.0 ) ) );
115
+
116
+ }
117
+
118
+ // Solid angle of a spot's cone, over which its light paths leave uniformly.
119
+ export const spotConeSolidAngle = ( light ) => float( 2 * PI ).mul( max( float( 1.0 ).sub( cos( light.angle ) ), 1e-10 ) );
120
+
121
+ // A soft directional light's cone (angle = angular diameter), as sampleDirectionalLight computes it.
122
+ export const directionalConeSolidAngle = ( light ) => float( 2 * PI ).mul( max( float( 1.0 ).sub( cos( light.angle.mul( 0.5 ) ) ), 1e-10 ) );
123
+
124
+ /** Area-light NEE's solid-angle density for any point of the light, seen from `from` (sampleRectAreaLight). */
125
+ export const areaLightDirectPdfW = ( light, from, dist, cosLight ) => {
126
+
127
+ const corner = light.position.sub( light.u ).sub( light.v );
128
+ const solidAngle = sphQuadSolidAngle( from, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
129
+ return select( light.shape.lessThan( 0.5 ).and( solidAngle.greaterThan( 1e-5 ) ), float( 1.0 ).div( solidAngle ),
130
+ dist.mul( dist ).div( max( light.area.mul( max( cosLight, 0.001 ) ), 1e-10 ) ) );
131
+
132
+ };
@@ -7,6 +7,7 @@ import {
7
7
  reflect,
8
8
  max,
9
9
  If,
10
+ bool as tslBool,
10
11
  } from 'three/tsl';
11
12
 
12
13
  import { struct } from './patches.js';
@@ -15,15 +16,16 @@ import { AnisoFrame, DotProducts,
15
16
  BRDFWeights,
16
17
  } from './Struct.js';
17
18
  import { MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso, anisoTangentFrame, constructTBN } from './Common.js';
18
- import { computeAnisoAlphas, calculateBRDFWeightsFromMaterial, calculateBSDFSamplingPDF } from './MaterialProperties.js';
19
+ import { computeAnisoAlphas, calculateBRDFWeightsFromMaterial, calculateBSDFSamplingPDF, diffuseTransmissionPdf } from './MaterialProperties.js';
19
20
  import { ImportanceSampleCosine, sampleGGXVNDF, sampleGGXVNDFAniso } from './MaterialSampling.js';
20
- import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
21
+ import { evaluateMaterialResponseFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
21
22
  import { getRandomSample1D } from './Random.js';
22
23
 
23
24
  export const ClearcoatResult = struct( {
24
25
  brdf: 'vec3',
25
26
  L: 'vec3',
26
27
  pdf: 'float',
28
+ isDiffuseTransmission: 'bool',
27
29
  } );
28
30
 
29
31
  // Improved clearcoat sampling function
@@ -55,6 +57,7 @@ export const sampleClearcoat = Fn( ( [
55
57
 
56
58
  const L = vec3( 0.0 ).toVar();
57
59
  const H = vec3( 0.0 ).toVar();
60
+ const transmitted = tslBool( false ).toVar();
58
61
 
59
62
  If( rand.lessThan( clearcoatWeight ), () => {
60
63
 
@@ -83,6 +86,11 @@ export const sampleClearcoat = Fn( ( [
83
86
  } );
84
87
  L.assign( reflect( V.negate(), H ) );
85
88
 
89
+ } ).ElseIf( rand.lessThan( clearcoatWeight.add( specularWeight ).add( weights.diffuseTransmission ) ), () => {
90
+
91
+ L.assign( ImportanceSampleCosine( { N: N.negate(), xi: randomSample } ) );
92
+ transmitted.assign( true );
93
+
86
94
  } ).Else( () => {
87
95
 
88
96
  // Sample diffuse
@@ -94,16 +102,17 @@ export const sampleClearcoat = Fn( ( [
94
102
  // Calculate dot products (aniso-aware: also projects onto the anisotropy frame)
95
103
  const dots = DotProducts.wrap( computeDotProductsAniso( N, V, L, material ) );
96
104
 
97
- // One density for every MIS site — see calculateBSDFSamplingPDF.
98
- const pdf = max( calculateBSDFSamplingPDF( material, weights, dots ), 0.001 );
105
+ // One density for every MIS site — see calculateBSDFSamplingPDF. Below the surface only the
106
+ // diffuse transmission lobe draws.
107
+ const pdf = max( transmitted.select( diffuseTransmissionPdf( weights.diffuseTransmission, dot( N, L ).negate() ), calculateBSDFSamplingPDF( material, weights, dots ) ), 0.001 );
99
108
 
100
109
  // Evaluate complete BRDF
101
110
  // The same BRDF every other site evaluates — see the clearcoat note in MaterialEvaluation.
102
- const brdf = evaluateMaterialResponseFromDots( material, dots );
111
+ const brdf = transmitted.select( evaluateDiffuseTransmission( material, max( dot( N, V ), 0.001 ) ), evaluateMaterialResponseFromDots( material, dots ) );
103
112
 
104
113
  // Return brdf, L direction, and pdf packed together
105
114
  // Caller needs L and pdf - return as struct-like output
106
115
  // We pack: result.xyz = brdf, result.w = pdf, L stored in separate output
107
- return ClearcoatResult( { brdf, L, pdf } );
116
+ return ClearcoatResult( { brdf, L, pdf, isDiffuseTransmission: transmitted } );
108
117
 
109
118
  } );
package/src/TSL/Common.js CHANGED
@@ -432,6 +432,13 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
432
432
  const data30 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_A ), int( MATERIAL_SLOTS ) ).toVar();
433
433
  const data31 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_B ), int( MATERIAL_SLOTS ) ).toVar();
434
434
  const data32 = getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_C ), int( MATERIAL_SLOTS ) ).toVar();
435
+ // Only a material with diffuse transmission has maps for it.
436
+ const data33 = vec4( - 1.0, - 1.0, 0.0, 0.0 ).toVar();
437
+ If( data5.a.greaterThan( 0.0 ), () => {
438
+
439
+ data33.assign( getDatafromStorageBuffer( materialBuffer, materialIndex, int( S.EXT_MAP_INDICES_D ), int( MATERIAL_SLOTS ) ) );
440
+
441
+ } );
435
442
 
436
443
  return RayTracingMaterial( {
437
444
  color: vec4( data0.rgb, 1.0 ),
@@ -470,6 +477,10 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
470
477
  iridescenceThicknessMapIndex: int( data31.b ),
471
478
  specularIntensityMapIndex: int( data31.a ),
472
479
  specularColorMapIndex: int( data32.r ),
480
+ diffuseTransmission: data5.a,
481
+ diffuseTransmissionColor: data32.gba,
482
+ diffuseTransmissionMapIndex: int( data33.r ),
483
+ diffuseTransmissionColorMapIndex: int( data33.g ),
473
484
  albedoMapIndex: int( data8.r ),
474
485
  normalMapIndex: int( data8.g ),
475
486
  roughnessMapIndex: int( data8.b ),
@@ -569,6 +580,10 @@ export const diffuseGroundMaterial = Fn( () => {
569
580
  iridescenceThicknessMapIndex: int( - 1 ),
570
581
  specularIntensityMapIndex: int( - 1 ),
571
582
  specularColorMapIndex: int( - 1 ),
583
+ diffuseTransmission: float( 0.0 ),
584
+ diffuseTransmissionColor: vec3( 1.0 ),
585
+ diffuseTransmissionMapIndex: int( - 1 ),
586
+ diffuseTransmissionColorMapIndex: int( - 1 ),
572
587
  } );
573
588
 
574
589
  } );
@@ -612,22 +627,12 @@ export const getShadowMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
612
627
  * rides along in the rest — no second binding, which matters on a backend that
613
628
  * allows ten storage buffers per stage.
614
629
  */
615
- // Runtime uniform that toggles alpha-cutout shadows; set by ShaderBuilder before the kernels build.
616
- let _enableAlphaShadows = null;
617
-
618
- export function setAlphaShadowsUniform( node ) {
619
-
620
- _enableAlphaShadows = node;
621
-
622
- }
623
-
624
- export const getAlphaShadowsUniform = () => _enableAlphaShadows;
625
-
626
- // Does a hit on a triangle with these flags settle the shadow ray without a material fetch?
627
- export const shadowFlagsSettle = ( flags ) => {
630
+ // Does a hit on a triangle with these flags settle the shadow ray without a material fetch? `alphaShadows` is the
631
+ // kernel's switch (SceneResources); null means alpha is never tested.
632
+ export const shadowFlagsSettle = ( flags, alphaShadows ) => {
628
633
 
629
634
  const bit = ( shift ) => flags.shiftRight( uint( shift ) ).bitAnd( uint( 1 ) ).equal( uint( 1 ) );
630
- const alphaOff = _enableAlphaShadows ? _enableAlphaShadows.equal( int( 0 ) ) : tslBool( true );
635
+ const alphaOff = alphaShadows ? alphaShadows.equal( int( 0 ) ) : tslBool( true );
631
636
  return bit( TRI_BLOCKER_SHIFT ).or( bit( TRI_BLOCKER_ALPHA_SHIFT ).and( alphaOff ) );
632
637
 
633
638
  };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ConnectKernel.js — connects each camera vertex Shade left pending to one vertex of a random light
3
+ * path, weighted by how many of that path's vertices fit the bounce budget (Davidovič et al. 2014).
4
+ */
5
+
6
+ import {
7
+ Fn, float, vec2, vec4, int, uint, If, instanceIndex, atomicLoad, Return, dot, sqrt, min, max, length,
8
+ } from 'three/tsl';
9
+
10
+ import { getRandomSample1D, pcgHash, SAMPLER_DIMS_PER_BOUNCE } from './Random.js';
11
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
12
+ import { calculateMaterialPDF } from './LightsSampling.js';
13
+ import { traceShadowRayRefractiveOpaque } from './LightsDirect.js';
14
+ import { traverseBVHShadow } from './BVHTraversal.js';
15
+ import { offsetRayOrigin, SHADOW_END, luminance } from './Common.js';
16
+ import { regularizePathContribution } from './PathTracerCore.js';
17
+ import {
18
+ resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, STRATEGY, DIM_CONNECT_PATH, DIM_CONNECT_VERTEX,
19
+ } from './Bidirectional.js';
20
+ import { COUNTER } from '../Processor/QueueManager.js';
21
+ import {
22
+ readRayRadiance, writeRayRadiance, readRngState,
23
+ pendingVertex, cachedVertex, readVertexTag, readVertexRecord, readLightPathLength,
24
+ } from '../Processor/PackedRayBuffer.js';
25
+
26
+ const WG_SIZE = 256;
27
+
28
+ export function buildConnectKernel( params ) {
29
+
30
+ const {
31
+ rayBufferRW, hitBufferRO, activeIndicesRO, counters,
32
+ bvhBuffer, triangleBuffer, materialBuffer,
33
+ resolution, frame, accumFrame, currentBounce, chunkRowBase,
34
+ lightPaths, slotsPerPath, lightTag, passTag, strategyView,
35
+ maxBounceCount, globalIlluminationIntensity, fireflyThreshold,
36
+ // Vertex merging's η² at a point (Bidirectional.js mergeVmAt), or null without it.
37
+ mergeVm = null,
38
+ } = params;
39
+
40
+ return Fn( () => {
41
+
42
+ const tid = instanceIndex;
43
+ If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
44
+
45
+ Return();
46
+
47
+ } );
48
+
49
+ const rayID = activeIndicesRO.element( tid ).toVar();
50
+ const pending = pendingVertex( rayID );
51
+ If( readVertexTag( hitBufferRO, pending ).notEqual( passTag ), () => {
52
+
53
+ Return();
54
+
55
+ } );
56
+
57
+ const camRecord = readVertexRecord( hitBufferRO, pending );
58
+ const cam = { ...camRecord, vm: mergeVm?.( camRecord.position ) ?? null };
59
+ const cameraVertices = int( cam.extra ).add( int( 1 ) ).toVar();
60
+
61
+ const resX = int( resolution.x );
62
+ const globalPixel = int( rayID ).add( chunkRowBase.mul( resX ) );
63
+ const pixel = vec2( float( globalPixel.mod( resX ) ).add( 0.5 ), float( globalPixel.div( resX ) ).add( 0.5 ) ).toVar();
64
+ const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
65
+ // Only PCG reads it; hashed so the path's own stream, already saved by Shade, is not replayed.
66
+ const rng = pcgHash( { state: readRngState( hitBufferRO, rayID ).bitXor( uint( 0x9E3779B9 ) ) } ).toVar();
67
+ const uPath = getRandomSample1D( pixel, int( 0 ), dimBase.add( int( DIM_CONNECT_PATH ) ), rng, resolution, frame ).toVar();
68
+ const uVertex = getRandomSample1D( pixel, int( 0 ), dimBase.add( int( DIM_CONNECT_VERTEX ) ), rng, resolution, frame ).toVar();
69
+
70
+ const path = min( uint( float( lightPaths ).mul( uPath ) ), lightPaths.sub( uint( 1 ) ) ).toVar();
71
+ const first = path.mul( slotsPerPath ).toVar();
72
+ const stored = int( readLightPathLength( hitBufferRO, cachedVertex( first ), lightTag ) );
73
+ const usable = min( stored, int( maxBounceCount ).add( int( 1 ) ).sub( cameraVertices ) ).toVar();
74
+ If( usable.lessThan( int( 1 ) ), () => {
75
+
76
+ Return();
77
+
78
+ } );
79
+
80
+ const c = min( int( float( usable ).mul( uVertex ) ), usable.sub( int( 1 ) ) ).toVar();
81
+ const lightRecord = readVertexRecord( hitBufferRO, cachedVertex( first.add( uint( c ) ) ) );
82
+ const light = { ...lightRecord, vm: mergeVm?.( lightRecord.position ) ?? null };
83
+
84
+ const toLight = light.position.sub( cam.position ).toVar();
85
+ const dist2 = max( dot( toLight, toLight ), 1e-12 ).toVar();
86
+ const dir = toLight.div( sqrt( dist2 ) ).toVar();
87
+
88
+ const cosCamera = dot( dir, cam.N ).toVar();
89
+ const cosCameraFacet = dot( dir, cam.facetN ).toVar();
90
+ const cosLightFacet = dot( dir, light.facetN ).negate().toVar();
91
+ // The camera end tests as NEE does; at the light end the camera vertex is the viewer, which only the facet bounds.
92
+ If( cosCamera.lessThanEqual( 0.0 ).or( cosCameraFacet.lessThanEqual( 0.0 ) ).or( cosLightFacet.lessThanEqual( 0.0 ) ), () => {
93
+
94
+ Return();
95
+
96
+ } );
97
+
98
+ const cameraMaterial = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
99
+ const lightMaterial = resolveSurfaceMaterial( light.materialIndex, light.uv, light.N, materialBuffer );
100
+ const toCamera = dir.negate();
101
+
102
+ const fCamera = evaluateMaterialResponse( cam.V, dir, cam.N, cameraMaterial ).toVar();
103
+ const fLight = evaluateMaterialResponse( toCamera, light.V, light.N, lightMaterial ).toVar();
104
+ const geometry = cosCamera.mul( lightEndCosine( light.V, light.N, light.facetN, toCamera ) ).div( dist2 );
105
+
106
+ const cameraForward = calculateMaterialPDF( cam.V, dir, cam.N, cameraMaterial );
107
+ const cameraReverse = calculateMaterialPDF( dir, cam.V, cam.N, cameraMaterial );
108
+ const lightForward = calculateMaterialPDF( light.V, toCamera, light.N, lightMaterial );
109
+ const lightReverse = calculateMaterialPDF( toCamera, light.V, light.N, lightMaterial );
110
+
111
+ // Georgiev (40)-(41): each side's sum, from the other side's density of reaching it.
112
+ const wLight = misPartial( cameraForward.mul( cosLightFacet ).div( dist2 ), light, lightReverse );
113
+ const wCamera = misPartial( lightForward.mul( cosCameraFacet ).div( dist2 ), cam, cameraReverse );
114
+
115
+ const scattering = cameraVertices.add( c ).add( int( 1 ) );
116
+ // Alone, a path of k scattering vertices is reached by k − 1 connections.
117
+ const weight = strategyWeight( strategyView, STRATEGY.CONNECT, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering.sub( int( 1 ) ) ) ) );
118
+ const contribution = regularizePathContribution(
119
+ cam.throughput.mul( fCamera ).mul( fLight ).mul( light.throughput )
120
+ .mul( geometry.mul( weight ).mul( float( usable ) ).mul( globalIlluminationIntensity ) ),
121
+ float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),
122
+ ).toVar();
123
+
124
+ If( luminance( contribution ).greaterThan( 0.0 ), () => {
125
+
126
+ const origin = offsetRayOrigin( cam.position, cam.facetN ).toVar();
127
+ const segment = offsetRayOrigin( light.position, light.facetN ).sub( origin ).toVar();
128
+ const segmentLength = length( segment ).toVar();
129
+ const visibility = traceShadowRayRefractiveOpaque(
130
+ origin, segment.div( segmentLength ), segmentLength.mul( SHADOW_END ),
131
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
132
+ );
133
+
134
+ If( visibility.greaterThan( 0.0 ), () => {
135
+
136
+ const radiance = readRayRadiance( rayBufferRW, rayID ).toVar();
137
+ writeRayRadiance( rayBufferRW, rayID, vec4( radiance.xyz.add( contribution.mul( visibility ) ), radiance.w ) );
138
+
139
+ } );
140
+
141
+ } );
142
+
143
+ } );
144
+
145
+ }
146
+
147
+ export { WG_SIZE as CONNECT_WG_SIZE };
@@ -72,7 +72,7 @@ export const TraceDebugMode = Fn( ( [
72
72
  visMode, debugVisScale,
73
73
  // Screen info
74
74
  pixelCoord, resolution,
75
- // Material textures are read from the module-level bucket nodes (setMaterialBucketTextures).
75
+ // Material textures come from the kernel's SceneResources.
76
76
  // Camera matrices (for depth debug mode)
77
77
  cameraProjectionMatrix, cameraViewMatrix,
78
78
  // Frame counter (for stochastic debug modes)
@@ -1,8 +1,9 @@
1
+ import { sceneResources } from './SceneResources.js';
1
2
  import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, uintBitsToFloat } from 'three/tsl';
2
3
 
3
4
  import { struct } from './patches.js';
4
5
  import { triangleRow, instanceRows, instanceDirToWorld } from './Common.js';
5
- import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
+ import { sampleDisplacementMap, bucketTexelSize } from './TextureSampling.js';
6
7
 
7
8
  // Ray-displacement intersection configuration
8
9
  const MAX_MARCH_STEPS = 32;
@@ -32,7 +33,7 @@ export const DisplacementResult = struct( {
32
33
  */
33
34
  export const refineDisplacedIntersection = Fn( ( [
34
35
  ray, hitInfo, triangleBuffer, material, bounceIndex, bvhBuffer, instanceLeaf
35
- ] ) => {
36
+ ], builder ) => {
36
37
 
37
38
  const resultHitPoint = hitInfo.hitPoint.toVar();
38
39
  const resultUV = hitInfo.uv.toVar();
@@ -205,7 +206,7 @@ export const refineDisplacedIntersection = Fn( ( [
205
206
 
206
207
  // Compute displaced normal from height-field gradients using UV tangent vectors.
207
208
  // Finite-difference step = one texel of the SELECTED bucket array's real dimensions.
208
- const texel = bucketTexelSize( getLinearBucketTextures(), material.displacementMapIndex ).toVar();
209
+ const texel = bucketTexelSize( sceneResources( builder ).linearBuckets, material.displacementMapIndex ).toVar();
209
210
  const hC = finalHeight;
210
211
  const hU = sampleDisplacementMap(
211
212
  material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
@@ -35,8 +35,10 @@ import {
35
35
  import { TRI_MATERIAL_MASK } from '../EngineDefaults.js';
36
36
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
37
37
  import { calculateMaterialPDFFromDots } from './LightsSampling.js';
38
- import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
38
+ import { evaluateMaterialResponseFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
39
+ import { diffuseTransmissionWeight, diffuseTransmissionPdf } from './MaterialProperties.js';
39
40
  import { DotProducts } from './Struct.js';
41
+ import { triangleSide, sideAccepts } from './BVHTraversal.js';
40
42
 
41
43
  // ================================================================================
42
44
  // STRUCTS
@@ -54,6 +56,21 @@ export const EmissiveSample = struct( {
54
56
  valid: 'bool', // Whether sample is valid
55
57
  } );
56
58
 
59
+ // EmissiveSample plus the triangle it landed on, for the bidirectional MIS weight.
60
+ export const EmissiveSampleIndexed = struct( {
61
+ position: 'vec3',
62
+ normal: 'vec3',
63
+ emission: 'vec3',
64
+ direction: 'vec3',
65
+ distance: 'float',
66
+ pdf: 'float',
67
+ area: 'float',
68
+ cosThetaLight: 'float',
69
+ valid: 'bool',
70
+ triangleIndex: 'int',
71
+ faceNormal: 'vec3', // winding normal
72
+ } );
73
+
57
74
  export const EmissiveContributionResult = struct( {
58
75
  contribution: 'vec3',
59
76
  hasEmissive: 'bool',
@@ -285,6 +302,7 @@ export const TriangleData = struct( {
285
302
  v0: 'vec3', v1: 'vec3', v2: 'vec3',
286
303
  n0: 'vec3', n1: 'vec3', n2: 'vec3',
287
304
  materialIndex: 'int',
305
+ side: 'int', // 0 front, 1 back, 2 double (BVHTraversal.js triangleSide)
288
306
  } );
289
307
 
290
308
  // Fetch triangle vertices from storage buffer
@@ -323,6 +341,7 @@ export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer, bvhBuffe
323
341
  return TriangleData( {
324
342
  v0, v1, v2, n0, n1, n2,
325
343
  materialIndex: int( uvMat.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ),
344
+ side: triangleSide( uvMat.z ),
326
345
  } );
327
346
 
328
347
  } );
@@ -359,14 +378,16 @@ export const calculateEmissiveLightPdf = Fn( ( [
359
378
 
360
379
  // Area: PDF = (power/totalPower) / area, converted to solid angle
361
380
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
362
- const cosLight = max( dot( rayDir.negate(), geoNormal ), 0.001 );
381
+ const cosLight = max( abs( dot( rayDir, geoNormal ) ), 0.001 );
363
382
  const distSq = hitDistance.mul( hitDistance );
364
383
  const pdfArea = selectionPdf.div( area );
365
384
  result.assign( pdfArea.mul( distSq ).div( cosLight ) );
366
385
 
367
386
  } );
368
387
 
369
- return max( result, MIN_PDF );
388
+ // The sampler draws only the sides the triangle emits from (a unit normal: sideAccepts has a threshold).
389
+ const winding = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
390
+ return select( sideAccepts( triData.side, dot( rayDir, winding ) ), max( result, MIN_PDF ), float( 0.0 ) );
370
391
 
371
392
  } );
372
393
 
@@ -378,7 +399,7 @@ export const calculateEmissiveLightPdf = Fn( ( [
378
399
  // CDF values are stored in the .b channel of the emissive buffer.
379
400
  // `emissiveOffset` is the vec4-element offset into the packed light buffer
380
401
  // where emissive entries start (0 if using a non-packed buffer).
381
- const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissiveTriangleCount, rand ] ) => {
402
+ export const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissiveTriangleCount, rand ] ) => {
382
403
 
383
404
  const lo = int( 0 ).toVar();
384
405
  const hi = emissiveTriangleCount.sub( 1 ).toVar();
@@ -407,15 +428,17 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissive
407
428
  // Sample from emissive triangle index using CDF importance sampling.
408
429
  // `emissiveTriangleBuffer` may be the shared packed light buffer; `emissiveVec4Offset`
409
430
  // gives the vec4 offset where emissive entries begin.
410
- export const sampleEmissiveTriangle = Fn( ( [
431
+ const makeSampleEmissiveTriangle = ( indexed ) => Fn( ( [
411
432
  hitPoint, surfaceNormal,
412
433
  rngState,
413
434
  pixelCoord, resolution, frame, dimBase,
414
435
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
415
436
  triangleBuffer, bvhBuffer,
437
+ // The surface passes light through diffusely: a point behind it is as good as one in front.
438
+ throughSurface,
416
439
  ] ) => {
417
440
 
418
- const result = EmissiveSample( {
441
+ const fields = {
419
442
  position: vec3( 0.0 ),
420
443
  normal: vec3( 0.0 ),
421
444
  emission: vec3( 0.0 ),
@@ -425,7 +448,10 @@ export const sampleEmissiveTriangle = Fn( ( [
425
448
  area: float( 0.0 ),
426
449
  cosThetaLight: float( 0.0 ),
427
450
  valid: false,
428
- } ).toVar();
451
+ };
452
+ const result = ( indexed
453
+ ? EmissiveSampleIndexed( { ...fields, triangleIndex: int( - 1 ), faceNormal: vec3( 0.0 ) } )
454
+ : EmissiveSample( fields ) ).toVar();
429
455
 
430
456
  // Check if we have emissive triangles
431
457
  If( emissiveTriangleCount.greaterThan( int( 0 ) ), () => {
@@ -469,9 +495,9 @@ export const sampleEmissiveTriangle = Fn( ( [
469
495
  const samplePos = sphResult.position;
470
496
 
471
497
  const surfaceFacing = dot( dir, surfaceNormal );
472
- const emissiveFacing = dot( dir, geoNormal.negate() );
498
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
473
499
 
474
- If( surfaceFacing.greaterThan( 0.0 ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
500
+ If( surfaceFacing.greaterThan( 0.0 ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ), () => {
475
501
 
476
502
  // Interpolate normal at sampled point via barycentric coords
477
503
  const bary = barycentricFromPoint( samplePos, triData.v0, triData.v1, triData.v2 );
@@ -495,6 +521,12 @@ export const sampleEmissiveTriangle = Fn( ( [
495
521
  result.area.assign( area );
496
522
  result.cosThetaLight.assign( emissiveFacing );
497
523
  result.valid.assign( true );
524
+ if ( indexed ) {
525
+
526
+ result.triangleIndex.assign( triangleIndex );
527
+ result.faceNormal.assign( geoNormal );
528
+
529
+ }
498
530
 
499
531
  } );
500
532
 
@@ -512,9 +544,11 @@ export const sampleEmissiveTriangle = Fn( ( [
512
544
  const dir = toEmissive.div( dist );
513
545
 
514
546
  const surfaceFacing = dot( dir, surfaceNormal );
515
- const emissiveFacing = dot( dir, sampleNormal.negate() );
547
+ // The facet's cosine, not the shading normal's: it is what turns an area density into a solid-angle one.
548
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
516
549
 
517
- If( surfaceFacing.greaterThan( 0.0 ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
550
+ // Edge-on (a two-sided emitter passes its side test there) the density is infinite and the light zero.
551
+ If( surfaceFacing.greaterThan( 0.0 ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
518
552
 
519
553
  // PDF: CDF selection (power/totalPower) * uniform area (1/area)
520
554
  // Converted to solid angle: pdfArea * distSq / cosLight
@@ -530,6 +564,12 @@ export const sampleEmissiveTriangle = Fn( ( [
530
564
  result.area.assign( area );
531
565
  result.cosThetaLight.assign( emissiveFacing );
532
566
  result.valid.assign( true );
567
+ if ( indexed ) {
568
+
569
+ result.triangleIndex.assign( triangleIndex );
570
+ result.faceNormal.assign( geoNormal );
571
+
572
+ }
533
573
 
534
574
  } );
535
575
 
@@ -541,6 +581,9 @@ export const sampleEmissiveTriangle = Fn( ( [
541
581
 
542
582
  } );
543
583
 
584
+ export const sampleEmissiveTriangle = /*@__PURE__*/ makeSampleEmissiveTriangle( false );
585
+ export const sampleEmissiveTriangleIndexed = /*@__PURE__*/ makeSampleEmissiveTriangle( true );
586
+
544
587
  // ================================================================================
545
588
  // EMISSIVE TRIANGLE DIRECT LIGHTING CONTRIBUTION
546
589
  // ================================================================================
@@ -573,11 +616,12 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
573
616
  // down-weights BSDF hits deletes emitter energy from deep GI.
574
617
 
575
618
  // Sample emissive triangle (CDF importance-weighted)
619
+ const transmits = material.diffuseTransmission.greaterThan( 0.0 );
576
620
  const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
577
621
  hitPoint, normal, rngState,
578
622
  pixelCoord, resolution, frame, dimBase,
579
623
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
580
- triangleBuffer, bvhBuffer,
624
+ triangleBuffer, bvhBuffer, transmits,
581
625
  ) );
582
626
 
583
627
  If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
@@ -586,10 +630,12 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
586
630
  result.emissionOnly.assign( emissiveSample.emission );
587
631
  result.distance.assign( emissiveSample.distance );
588
632
 
589
- // Check geometric validity
590
- const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
633
+ // Check geometric validity: in front, or behind a surface that passes light through diffusely
634
+ const back = transmits.and( dot( normal, emissiveSample.direction ).lessThan( 0.0 ) )
635
+ .and( dot( emissiveSample.direction, geomNormal ).lessThan( 0.0 ) ).toVar();
636
+ const NoL = abs( dot( normal, emissiveSample.direction ) );
591
637
 
592
- If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ), () => {
638
+ If( back.or( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ) ), () => {
593
639
 
594
640
  // Aimed at the sampled point and stopped a relative hair short, as for area lights.
595
641
  const rayOrigin = hitPoint.add( calculateRayOffsetFn( hitPoint, geomNormal, emissiveSample.direction ) );
@@ -602,8 +648,9 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
602
648
  // Share H + dot products between BRDF eval and PDF (computeDotProducts
603
649
  // would otherwise run twice with identical inputs).
604
650
  const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, emissiveSample.direction, material ) );
605
- const brdfValue = evaluateMaterialResponseFromDots( material, dots );
606
- const brdfPdf = calculateMaterialPDFFromDots( material, dots );
651
+ const brdfValue = select( back, evaluateDiffuseTransmission( material, max( dot( normal, viewDir ), 0.001 ) ), evaluateMaterialResponseFromDots( material, dots ) );
652
+ const brdfPdf = select( back,
653
+ diffuseTransmissionPdf( diffuseTransmissionWeight( material ), NoL ), calculateMaterialPDFFromDots( material, dots ) );
607
654
 
608
655
  // MIS weight: balance light sampling vs BRDF sampling
609
656
  const misWeight = select(