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
@@ -7,35 +7,46 @@
7
7
  import {
8
8
  Fn, float, vec2, vec3, vec4, int, uint,
9
9
  bool as tslBool,
10
- If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep, acos,
10
+ If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep, acos, abs, cross,
11
11
  instanceIndex,
12
12
  sampler,
13
13
  atomicLoad, atomicStore,
14
14
  Return,
15
15
  } from 'three/tsl';
16
16
 
17
- import { sampleEnvironment, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
18
- import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END, triangleRow, MIN_ROUGHNESS } from './Common.js';
17
+ import { sampleEnvironment, groundProjectedEnvDir, sampleEnvironmentExact, environmentPdfExact } from './Environment.js';
18
+ import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END, triangleRow, MIN_ROUGHNESS, MIN_PDF } from './Common.js';
19
19
  import { cosineWeightedSample } from './MaterialSampling.js';
20
20
  import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV, triangleUVTangent } from './TextureSampling.js';
21
21
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
22
- import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
23
- import { traceShadowRay } from './LightsDirect.js';
22
+ import {
23
+ calculateDirectLightingUnified, calculateDirectLightingThroughSurfaces, calculateMaterialPDF,
24
+ sampleDirectionalLight, sampleRectAreaLight, samplePointLightWithAttenuation, sampleSpotLightWithRadius,
25
+ areaLightRadiance, areaLightSpreadAttenuation,
26
+ } from './LightsSampling.js';
27
+ import { traceShadowRay, traceShadowRayRefractiveOpaque, estimateLightImportance } from './LightsDirect.js';
24
28
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
25
- import { unpackHitFacet } from './HitFacet.js';
29
+ import { hitFacet, unpackHitFacet, windingNormal } from './HitFacet.js';
26
30
  import { traverseBVHShadow, triangleSide, sideAccepts } from './BVHTraversal.js';
27
31
  import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
28
32
  import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
33
+ import { ALL_MATERIAL_LAYERS } from './SceneResources.js';
29
34
  import { calculateIndirectLighting } from './LightsIndirect.js';
30
- import { IndirectLightingResult, sampleCone } from './LightsCore.js';
31
- import { sunRadianceToward } from './Sun.js';
32
- import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
35
+ import {
36
+ IndirectLightingResult, sampleCone, LightSample, DirectionalLight, AreaLight, PointLight, SpotLight,
37
+ getDirectionalLight, getAreaLight, getPointLight, getSpotLight, intersectAreaLight, getDistanceAttenuation, sampleDirectionalGoboMask,
38
+ LIGHT_TYPE_DIRECTIONAL, LIGHT_TYPE_AREA, LIGHT_TYPE_POINT, LIGHT_TYPE_SPOT,
39
+ } from './LightsCore.js';
40
+ import { sunRadianceToward, sampleSunDisc } from './Sun.js';
41
+ import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette, DELTA_PDF } from './PathTracerCore.js';
33
42
  import { evaluateSpecularDFG, baseFresnelParams } from './MaterialProperties.js';
34
- import { NRD_HIT_DIST_A, NRD_HIT_DIST_B } from '../EngineDefaults.js';
35
43
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
36
44
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
37
- import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
38
- import { sampleLightBVHTriangle, calculateLightBVHPdf } from './LightBVHSampling.js';
45
+ import {
46
+ calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample, EmissiveSampleIndexed,
47
+ sampleEmissiveTriangleIndexed, fetchTriangleData, TriangleData,
48
+ } from './EmissiveSampling.js';
49
+ import { sampleLightBVHTriangle, sampleLightBVHTriangleIndexed, calculateLightBVHPdf } from './LightBVHSampling.js';
39
50
  import {
40
51
  Ray,
41
52
  HitInfo,
@@ -65,6 +76,7 @@ import {
65
76
  readRayRadiance,
66
77
  readFeatureThroughput, writeFeatureThroughput,
67
78
  readRngState, writeRngState,
79
+ readMisState, writeRngMis, pendingVertex, cachedVertex, writeVertexRecord, writeLightPathLength, readLightOrigin,
68
80
  } from '../Processor/PackedRayBuffer.js';
69
81
 
70
82
  const WG_SIZE = 256;
@@ -88,7 +100,7 @@ export function buildShadeKernel( params ) {
88
100
  enableEnvironmentLight,
89
101
  groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
90
102
  enableGroundCatcher, groundCatcherHeight,
91
- envTotalSum, envCompensationDelta, envResolution,
103
+ envTotalSum, envResolution,
92
104
  directionalLightsBuffer, numDirectionalLights,
93
105
  areaLightsBuffer, numAreaLights,
94
106
  pointLightsBuffer, numPointLights,
@@ -110,15 +122,33 @@ export function buildShadeKernel( params ) {
110
122
  emissiveBoost, enableEmissiveTriangleSampling,
111
123
  lightBVHNodeCount, reverseMapVec4Offset,
112
124
  maxRayCount,
113
- // Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Gated by a
114
- // live uniform (1 = denoiser on) so the wavefront skips these writes when nothing consumes them.
115
- auxGBufferEnabled,
125
+ // Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Compiled in only when
126
+ // requested (PathTracer.requestOutput( 'gBuffer' )); then a live uniform (1 = denoiser on) skips the writes
127
+ // while nothing consumes them.
128
+ auxGBufferEnabled, gBuffer = true,
116
129
  hasSun, sunDirection, sunRadiance, sunParams,
130
+ // Bidirectional: light subpaths shade here too; camera vertices leave connections for ConnectKernel.
131
+ bidirectional: bdpt = null,
132
+ // The hit-distance output, when a stage asked for it (PathTracer.requestOutput).
133
+ hitDistanceEncode = null,
134
+ // The material layers some material uses (MaterialDataManager.materialLayers); the rest compile out. Bidirectional
135
+ // always leaves diffuse transmission out: its connections and light tracing do not cross surfaces yet.
136
+ materialLayers: layers = ALL_MATERIAL_LAYERS,
117
137
  } = params;
138
+ const diffuseTransmission = layers.diffuseTransmission;
118
139
 
119
- const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
140
+ // The bidirectional shading functions come with the integrator's uniforms (BidirectionalIntegrator), only used where bdpt is.
141
+ const {
142
+ mis, misOnHit, misOnSpecular, misOnScatter, misPartial, misWeight, emitterSideProbability, emitterAreaPdf, lightEndCosine, mergeVmAt, strategyWeight, sunEmissionPdf, STRATEGY, LIGHT_PIXEL_ROW_OFFSET, sourcePick, lampSource, sourceLampType, sourceLampIndex, SOURCE, guidedDiscPdf, recordEscape, LampPick, pickLamp, lampPickPdf, lampCount, spotConeSolidAngle, directionalConeSolidAngle, areaLightDirectPdfW,
143
+ } = bdpt?.lib ?? {};
144
+
145
+ const auxOn = gBuffer ? auxGBufferEnabled.greaterThan( uint( 0 ) ) : null;
120
146
 
121
147
  const useEmissiveNEE = lightBuffer !== undefined;
148
+ const lamps = {
149
+ directional: directionalLightsBuffer, numDirectional: numDirectionalLights, area: areaLightsBuffer, numArea: numAreaLights,
150
+ point: pointLightsBuffer, numPoint: numPointLights, spot: spotLightsBuffer, numSpot: numSpotLights,
151
+ };
122
152
 
123
153
  // Stochastic cone-jitter blur of an env backdrop lookup. Plain JS inliner (NOT a Fn — an rng Fn-param
124
154
  // would freeze; see TSL pitfalls) so it mutates the caller's rngState .toVar() directly. Shared by the
@@ -150,6 +180,29 @@ export function buildShadeKernel( params ) {
150
180
 
151
181
  };
152
182
 
183
+ // Emissive NEE's solid-angle density for a triangle's point, from whichever sampler it runs.
184
+ const emissiveNEEPdf = ( triangle, dist, toLight, from, instanceLeaf ) => {
185
+
186
+ const pdf = float( 0.0 ).toVar();
187
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
188
+
189
+ pdf.assign( calculateLightBVHPdf(
190
+ triangle, dist, toLight, from,
191
+ lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer, bvhBuffer,
192
+ ) );
193
+
194
+ } ).Else( () => {
195
+
196
+ pdf.assign( calculateEmissiveLightPdf(
197
+ triangle, dist, toLight, from,
198
+ triangleBuffer, materialBuffer, emissiveTotalPower, bvhBuffer, instanceLeaf,
199
+ ) );
200
+
201
+ } );
202
+ return pdf;
203
+
204
+ };
205
+
153
206
  const computeFn = Fn( () => {
154
207
 
155
208
  const threadIdx = instanceIndex;
@@ -186,6 +239,11 @@ export function buildShadeKernel( params ) {
186
239
 
187
240
  } );
188
241
 
242
+ // A light subpath gathers nothing; camera-only work is gated on it.
243
+ const isLight = bdpt ? flags.bitAnd( uint( RAY_FLAG.LIGHT_PATH ) ).notEqual( uint( 0 ) ).toVar() : null;
244
+ const cameraOnly = ( cond ) => ( bdpt ? cond.and( isLight.not() ) : cond );
245
+ const aux = auxOn ? cameraOnly( auxOn ) : null;
246
+
189
247
  // Backdrop-view = the ray still travels the original camera direction (only alpha/transparent passthrough
190
248
  // since the camera, REDIRECTED still clear). Captured at ARRIVAL (before the opaque/redirect bitOr below)
191
249
  // so it stays valid for both the miss branch and the emissive-hit scale. This — not bounceIndex==0 — is
@@ -200,6 +258,11 @@ export function buildShadeKernel( params ) {
200
258
  // One ray per pixel: rayID is the pixel index.
201
259
  const pixelIndex = rayID;
202
260
  const rngState = readRngState( hitBufferRW, rayID ).toVar();
261
+ const subpathState = bdpt ? readMisState( hitBufferRW, rayID ) : null;
262
+ const subpath = bdpt ? { dVCM: subpathState.dVCM.toVar(), dVC: subpathState.dVC.toVar(), vm: null } : null;
263
+ const persistRng = () => ( bdpt
264
+ ? writeRngMis( hitBufferRW, rayID, rngState, subpath.dVCM, subpath.dVC )
265
+ : writeRngState( hitBufferRW, rayID, rngState ) );
203
266
 
204
267
  // Samples key on the GLOBAL pixel. pixelIndex is the LOCAL path slot; the global pixel
205
268
  // = chunkRowBase·W + localSlot, so sequences stay aligned across row-band chunks
@@ -207,9 +270,10 @@ export function buildShadeKernel( params ) {
207
270
  // Hoisted here rather than at the BSDF draw because the ground catcher runs NEE far earlier.
208
271
  const _resX = int( resolution.x ).toVar();
209
272
  const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
273
+ const _pixelRow = float( _globalPixel.div( _resX ) ).add( 0.5 );
210
274
  const _pixelCoord = vec2(
211
275
  float( _globalPixel.mod( _resX ) ).add( 0.5 ),
212
- float( _globalPixel.div( _resX ) ).add( 0.5 ),
276
+ bdpt ? _pixelRow.add( select( isLight, float( LIGHT_PIXEL_ROW_OFFSET ), float( 0.0 ) ) ) : _pixelRow,
213
277
  );
214
278
  // This bounce's block of sampler dimensions — see the budget table in Random.js.
215
279
  const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
@@ -217,7 +281,7 @@ export function buildShadeKernel( params ) {
217
281
  // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
218
282
  // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
219
283
  // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
220
- const featCarry = readFeatureThroughput( rayBufferRW, rayID ).toVar();
284
+ const featCarry = aux ? readFeatureThroughput( rayBufferRW, rayID ).toVar() : null;
221
285
 
222
286
  const hitDist = readHitDistance( hitBufferRW, rayID ).toVar();
223
287
  const hitNormal = readHitNormal( hitBufferRW, rayID ).toVar();
@@ -234,14 +298,13 @@ export function buildShadeKernel( params ) {
234
298
  const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
235
299
  const transparentCount = readTransparentCount( rayBufferRW, rayID ).toVar();
236
300
 
237
- // NRD guide: first segment after the primary scatter, plus any alpha-skip run so a cutout hole
301
+ // Hit distance: first segment after the primary scatter, plus any alpha-skip run so a cutout hole
238
302
  // doesn't shorten it. Unconditional at depth 1, so the post-skip segment wins.
239
- If( auxOn.and( cameraDepth.equal( 1 ) ), () => {
303
+ if ( hitDistanceEncode && aux ) If( aux.and( cameraDepth.equal( 1 ) ), () => {
240
304
 
241
305
  const scatterViewZ = cameraViewMatrix.mul( vec4( origin, 1.0 ) ).z.abs();
242
- const norm = scatterViewZ.mul( NRD_HIT_DIST_B ).add( NRD_HIT_DIST_A );
243
306
  const total = readMisRayT( rayBufferRW, rayID ).add( min( hitDist, float( 1e6 ) ) );
244
- writeGBufferHitDist( gBufferRW, pixelIndex, total.div( max( norm, float( 1e-4 ) ) ).clamp( 0.0, 1.0 ) );
307
+ writeGBufferHitDist( gBufferRW, pixelIndex, hitDistanceEncode( total, scatterViewZ ) );
245
308
 
246
309
  } );
247
310
 
@@ -252,7 +315,7 @@ export function buildShadeKernel( params ) {
252
315
  // ratio in alpha (rgb = 0) so it composites as bg·ratio over a transparent background.
253
316
  // Shadows are caught by the EXISTING NEE shadow ray into real geometry — the plane itself
254
317
  // never enters the BVH. Secondary bounces ignore it entirely.
255
- If( enableGroundCatcher.and( bounceIndex.equal( 0 ) ), () => {
318
+ If( cameraOnly( enableGroundCatcher.and( bounceIndex.equal( 0 ) ) ), () => {
256
319
 
257
320
  const dirY = direction.y.toVar();
258
321
  If( dirY.abs().greaterThan( float( EPSILON ) ), () => {
@@ -286,7 +349,7 @@ export function buildShadeKernel( params ) {
286
349
  bvhBuffer, triangleBuffer, materialBuffer,
287
350
  envTexture, environmentIntensity, envMatrix,
288
351
  envCDFTexture,
289
- envTotalSum, envCompensationDelta, envResolution,
352
+ envTotalSum, envResolution,
290
353
  enableEnvironmentLight,
291
354
  tslBool( true ), // wantUnoccluded
292
355
  vec3( 0.0 ), planeN, float( 0.0 ),
@@ -351,7 +414,7 @@ export function buildShadeKernel( params ) {
351
414
  // The catcher is a real ground surface for the denoiser — write the plane's normal/depth
352
415
  // + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
353
416
  // valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
354
- If( auxOn, () => {
417
+ if ( aux ) If( aux, () => {
355
418
 
356
419
  const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
357
420
  writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
@@ -361,7 +424,7 @@ export function buildShadeKernel( params ) {
361
424
  writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
362
425
  // DDFA: the plane's aux (written above) is a committed surface — lock it (cosmetic; ray dies here).
363
426
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ).bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
364
- writeRngState( hitBufferRW, rayID, rngState );
427
+ persistRng();
365
428
  Return();
366
429
 
367
430
  } );
@@ -372,6 +435,13 @@ export function buildShadeKernel( params ) {
372
435
 
373
436
  If( hitDist.greaterThan( MISS_DIST ), () => {
374
437
 
438
+ if ( bdpt ) If( isLight, () => {
439
+
440
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
441
+ Return();
442
+
443
+ } );
444
+
375
445
  // Background and environment-lighting are decoupled (independent axes):
376
446
  // • Visible backdrop: a PRIMARY ray draws the env image only when showBackground — regardless
377
447
  // of enableEnvironmentLight (so you can show the HDRI without it lighting the scene).
@@ -381,12 +451,19 @@ export function buildShadeKernel( params ) {
381
451
  // is the direct backdrop", so env seen through alpha-cutout foliage holes is treated identically to
382
452
  // the open sky (blur, intensity, show/hide, color-mode, ground projection all match).
383
453
  // isBackdropView was captured at arrival (above) so it survives the REDIRECTED bitOr on opaque hits.
454
+ // A camera path out to the sky: where light from infinity gets in (LightGuide.js).
455
+ if ( bdpt ) If( isBackdropView.not().and( isLight.not() ), () => {
456
+
457
+ recordEscape( counters, bdpt, direction, origin );
458
+
459
+ } );
460
+
384
461
  const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
385
462
  const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
386
463
 
387
464
  // DDFA: env colour a redirected ray escaping to the environment sees (e.g. smooth glass over
388
465
  // sky). Committed (tinted by featCarry) into the aux G-buffer before the miss Return below.
389
- const escapedAuxAlbedo = vec3( 0.0 ).toVar();
466
+ const escapedAuxAlbedo = aux ? vec3( 0.0 ).toVar() : null;
390
467
 
391
468
  If( wantBackdrop.or( wantEnvLight ), () => {
392
469
 
@@ -428,19 +505,27 @@ export function buildShadeKernel( params ) {
428
505
 
429
506
  // DDFA: remember the (clamped) env colour this escaped ray sees, for the redirected-escape
430
507
  // aux commit below (tinted by the accumulated see-through throughput).
431
- escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
508
+ if ( aux ) escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
432
509
 
433
510
  // MIS weight for implicit env hit — prevents double-counting with NEE
434
511
  const envMisWeight = float( 1.0 ).toVar();
435
- If( isBackdropView.not(), () => {
512
+ if ( bdpt ) If( isBackdropView.not(), () => {
513
+
514
+ // Georgiev (43) for the environment at infinity, from the exact table its NEE and light paths draw from.
515
+ const pdfW = select( bdpt.envTable.greaterThan( uint( 0 ) ), environmentPdfExact( envCDFTexture, envMatrix, envResolution, direction ), float( 0.0 ) ).toVar();
516
+ const directPdfW = select( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ), pdfW, float( 0.0 ) );
517
+ const emissionPdfW = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
518
+ bdpt.envPick.mul( pdfW ).mul( guidedDiscPdf( bdpt, bdpt.guideTexture, direction, origin ) ), float( 0.0 ) );
519
+ envMisWeight.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
520
+ misWeight( float( 0.0 ), mis( directPdfW ).mul( subpath.dVCM ).add( mis( emissionPdfW ).mul( subpath.dVC ) ) ) ) );
521
+
522
+ } );
523
+ else If( isBackdropView.not(), () => {
436
524
 
437
525
  const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
438
- If( prevBouncePdf.greaterThan( 0.0 ), () => {
526
+ If( prevBouncePdf.greaterThan( 0.0 ).and( envTotalSum.greaterThan( 0.0 ) ), () => {
439
527
 
440
- const envEval = sampleEquirect(
441
- envTexture, direction, envMatrix, envTotalSum, envCompensationDelta, envResolution,
442
- );
443
- const envPdf = envEval.w;
528
+ const envPdf = environmentPdfExact( envCDFTexture, envMatrix, envResolution, direction ).toVar();
444
529
  If( envPdf.greaterThan( 0.0 ), () => {
445
530
 
446
531
  envMisWeight.assign( balanceHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) ); // megakernel parity (PathTracerCore.js:774): env NEE also uses balance
@@ -498,11 +583,29 @@ export function buildShadeKernel( params ) {
498
583
  If( sunL.x.add( sunL.y ).add( sunL.z ).greaterThan( 0.0 ), () => {
499
584
 
500
585
  const sunW = float( 1.0 ).toVar();
501
- If( isBackdropView.not().and( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ) ), () => {
586
+ if ( bdpt ) {
502
587
 
503
- sunW.assign( powerHeuristic( { pdf1: readRayPdf( rayBufferRW, rayID ), pdf2: float( 1.0 ).div( sunParams.y ) } ) );
588
+ // Georgiev (43) for a light at infinity: no distance, and NEE's density is solid angle.
589
+ If( isBackdropView.not(), () => {
590
+
591
+ const directPdfW = select( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ), float( 1.0 ).div( sunParams.y ), float( 0.0 ) );
592
+ const emissionPdfW = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
593
+ sunEmissionPdf( bdpt, sunParams, guidedDiscPdf( bdpt, bdpt.guideTexture, direction, origin ) ), float( 0.0 ) );
594
+ sunW.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
595
+ misWeight( float( 0.0 ), mis( directPdfW ).mul( subpath.dVCM ).add( mis( emissionPdfW ).mul( subpath.dVC ) ) ) ) );
596
+
597
+ } );
598
+
599
+ } else {
600
+
601
+ If( isBackdropView.not().and( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ) ), () => {
602
+
603
+ sunW.assign( powerHeuristic( { pdf1: readRayPdf( rayBufferRW, rayID ), pdf2: float( 1.0 ).div( sunParams.y ) } ) );
604
+
605
+ } );
606
+
607
+ }
504
608
 
505
- } );
506
609
  const vertex = max( bounceIndex.sub( 1 ), int( 0 ) );
507
610
  const sunScale = select( isBackdropView, backgroundIntensity,
508
611
  sunW.mul( select( vertex.greaterThan( int( 0 ) ), globalIlluminationIntensity, float( 1.0 ) ) ) );
@@ -553,7 +656,7 @@ export function buildShadeKernel( params ) {
553
656
  // per pixel → OIDN sees structure tracking the refracted view); depth kept at the primary hit.
554
657
  // AUX_LOCKED-clear alone means "never committed" — the old black-probe is redundant. A direct
555
658
  // backdrop (REDIRECTED clear) keeps Generate's black/far default (regression-safe sky guide).
556
- If( auxOn
659
+ if ( aux ) If( aux
557
660
  .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
558
661
  .and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
559
662
 
@@ -568,8 +671,110 @@ export function buildShadeKernel( params ) {
568
671
 
569
672
  } );
570
673
 
674
+ // On the triangle's plane: Extend stores the distance to it (ExtendKernel).
571
675
  const hitPoint = origin.add( direction.mul( hitDist ) ).toVar();
676
+ // Vertex merging: a merge here, weighed in every sum this vertex ends or scatters.
677
+ if ( bdpt?.merging ) subpath.vm = mergeVmAt( bdpt, hitPoint ).toVar();
572
678
  const N = normalize( hitNormal ).toVar();
679
+ // The hit record's facet is 11-bit; light tracing's and connections' grazing geometry terms need it exact.
680
+ const exactFacetN = bdpt ? hitFacet( {
681
+ triangleBuffer, bvhBuffer, triIdx: int( hitTriIdx ), instanceLeaf: hitInstance, hitPoint,
682
+ smoothNormal: N, viewDir: direction.negate(), didHit: tslBool( true ), liftEnabled: tslBool( false ),
683
+ } ).faceN : null;
684
+
685
+ // Where a lamp's light path first lands: its falloff and a directional gobo apply here.
686
+ const lampLanding = bdpt ? { source: int( - 1 ).toVar(), dist: float( 0.0 ).toVar() } : null;
687
+ const landLampPath = ( dist ) => {
688
+
689
+ const start = readLightOrigin( hitBufferRW, rayID );
690
+ const source = int( - 1 ).sub( start.triangle ).toVar();
691
+ If( start.triangle.lessThan( int( 0 ) ).and( source.greaterThanEqual( int( SOURCE.LAMPS ) ) ), () => {
692
+
693
+ const type = sourceLampType( bdpt, source ).toVar();
694
+ const index = sourceLampIndex( bdpt, source, type ).toVar();
695
+ If( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
696
+
697
+ throughput.mulAssign( sampleDirectionalGoboMask( DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, index ) ), hitPoint ) );
698
+
699
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_POINT ) ), () => {
700
+
701
+ const light = PointLight.wrap( getPointLight( pointLightsBuffer, index ) );
702
+ throughput.mulAssign( getDistanceAttenuation( { lightDistance: dist, cutoffDistance: light.distance, decayExponent: light.decay } ).mul( dist.mul( dist ) ) );
703
+
704
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_SPOT ) ), () => {
705
+
706
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, index ) );
707
+ throughput.mulAssign( getDistanceAttenuation( { lightDistance: dist, cutoffDistance: light.distance, decayExponent: light.decay } ).mul( dist.mul( dist ) ) );
708
+
709
+ } );
710
+ lampLanding.source.assign( source );
711
+ lampLanding.dist.assign( dist );
712
+
713
+ } );
714
+
715
+ };
716
+
717
+ // Georgiev (43) for the rect light a continuation reaches. Lamps are not geometry, so the new ray is tested
718
+ // here, after any scatter, with glass blocking as it does NEE's. `nearPick` gives NEE's pick of it here.
719
+ const bidirectionalAreaHit = ( from, dir, nearPick ) => {
720
+
721
+ const nearestT = float( 1e30 ).toVar();
722
+ const nearest = int( - 1 ).toVar();
723
+ Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
724
+
725
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
726
+ If( light.intensity.greaterThan( 0.0 ), () => {
727
+
728
+ const t = intersectAreaLight( light, hitPoint, dir ).toVar();
729
+ If( t.greaterThan( 0.0 ).and( t.lessThan( nearestT ) ), () => {
730
+
731
+ nearestT.assign( t );
732
+ nearest.assign( i );
733
+
734
+ } );
735
+
736
+ } );
737
+
738
+ } );
739
+
740
+ If( nearest.greaterThanEqual( int( 0 ) ), () => {
741
+
742
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearest ) );
743
+ const cosLight = dot( dir, light.normal ).negate().toVar();
744
+ const toLight = hitPoint.add( dir.mul( nearestT ) ).sub( from ).toVar();
745
+ const len = length( toLight ).toVar();
746
+ const visibility = traceShadowRayRefractiveOpaque(
747
+ from, toLight.div( len ), len.mul( SHADOW_END ), traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
748
+ ).toVar();
749
+
750
+ If( visibility.greaterThan( 0.0 ).and( cosLight.greaterThan( 0.0 ) ), () => {
751
+
752
+ const arrived = { dVCM: subpath.dVCM.toVar(), dVC: subpath.dVC.toVar() };
753
+ misOnHit( arrived, nearestT, cosLight );
754
+ const neePdfA = nearPick
755
+ ? nearPick( light ).mul( areaLightDirectPdfW( light, hitPoint, nearestT, cosLight ) ).mul( cosLight ).div( max( nearestT.mul( nearestT ), 1e-30 ) )
756
+ : float( 0.0 );
757
+ const emissionPdf = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
758
+ sourcePick( bdpt, lampSource( bdpt, int( LIGHT_TYPE_AREA ), nearest ) ).div( max( light.area, 1e-20 ) ).mul( cosLight ).div( Math.PI ), float( 0.0 ) );
759
+ const weight = strategyWeight( bdpt.strategyView, STRATEGY.HIT,
760
+ misWeight( float( 0.0 ), mis( neePdfA ).mul( arrived.dVCM ).add( mis( emissionPdf ).mul( arrived.dVC ) ) ) );
761
+
762
+ currentRadiance.assign( vec4(
763
+ currentRadiance.xyz.add(
764
+ regularizePathContribution(
765
+ areaLightRadiance( light ).mul( areaLightSpreadAttenuation( cosLight, light.spread ) ).mul( throughput ).mul( visibility ).mul( weight )
766
+ .mul( select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) ) ),
767
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
768
+ ),
769
+ ),
770
+ currentRadiance.w,
771
+ ) );
772
+
773
+ } );
774
+
775
+ } );
776
+
777
+ };
573
778
 
574
779
  // medium stack read once here; reused by the transparency block below
575
780
  const medStack = readMediumStack( rayBufferRW, rayID );
@@ -585,23 +790,33 @@ export function buildShadeKernel( params ) {
585
790
  If( mediumStackDepth.greaterThan( 0 ), () => {
586
791
 
587
792
  const mSigmaA = readMediumSigmaA( rayBufferRW, rayID ).toVar();
588
- const sssMed = readSSSMedium( rayBufferRW, rayID );
589
- const mSigmaS = sssMed.sigmaS.toVar();
590
- const mG = sssMed.g.toVar();
591
-
592
- If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), () => {
793
+ const absorb = () => {
593
794
 
594
795
  // glass: Beer-Lambert absorption
595
796
  const beer = exp( mSigmaA.mul( hitDist ).negate() ).toVar();
596
797
  throughput.mulAssign( beer );
597
798
  // DDFA: colored-glass volume tints the deferred aux guide by the same absorption.
598
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
799
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
599
800
 
600
801
  featCarry.mulAssign( beer );
601
802
 
602
803
  } );
603
804
 
604
- } ).Else( () => {
805
+ };
806
+
807
+ // Without subsurface materials every medium is glass (scattering 0).
808
+ if ( ! layers.subsurface ) {
809
+
810
+ absorb();
811
+ return;
812
+
813
+ }
814
+
815
+ const sssMed = readSSSMedium( rayBufferRW, rayID );
816
+ const mSigmaS = sssMed.sigmaS.toVar();
817
+ const mG = sssMed.g.toVar();
818
+
819
+ If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), absorb ).Else( () => {
605
820
 
606
821
  // subsurface: chromatic collision-distance sampling
607
822
  const mSigmaT = mSigmaA.add( mSigmaS );
@@ -628,7 +843,7 @@ export function buildShadeKernel( params ) {
628
843
 
629
844
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
630
845
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
631
- writeRngState( hitBufferRW, rayID, rngState );
846
+ persistRng();
632
847
  Return();
633
848
 
634
849
  } );
@@ -645,7 +860,7 @@ export function buildShadeKernel( params ) {
645
860
  // fires the next hit's env/emissive MIS, down-weighting SSS-then-env/emitter views.
646
861
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
647
862
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
648
- writeRngState( hitBufferRW, rayID, rngState );
863
+ persistRng();
649
864
  Return();
650
865
 
651
866
  } );
@@ -659,6 +874,33 @@ export function buildShadeKernel( params ) {
659
874
  const material = RayTracingMaterial.wrap(
660
875
  getMaterial( int( hitMatIdx ), materialBuffer )
661
876
  ).toVar();
877
+ if ( ! diffuseTransmission || bdpt ) {
878
+
879
+ material.diffuseTransmission.assign( 0.0 );
880
+ material.diffuseTransmissionMapIndex.assign( int( - 1 ) );
881
+ material.diffuseTransmissionColorMapIndex.assign( int( - 1 ) );
882
+
883
+ }
884
+
885
+ // A layer no material has is 0 in every material; saying so lets the compiler fold what still reads it.
886
+ for ( const layer of [ 'clearcoat', 'sheen', 'iridescence', 'anisotropy', 'subsurface', 'dispersion' ] ) {
887
+
888
+ if ( ! layers[ layer ] ) material[ layer ].assign( 0.0 );
889
+
890
+ }
891
+
892
+ // The segment past the last bounce is traced only for the light it hits: an opaque surface that does not glow
893
+ // ends it before any texture is read (emission is factor × map, so a zero factor is zero).
894
+ If( flags.bitAnd( uint( RAY_FLAG.EMISSION_ONLY ) ).notEqual( uint( 0 ) )
895
+ .and( material.alphaMode.equal( int( 0 ) ) ).and( material.transmission.lessThanEqual( 0.0 ) ).and( material.subsurface.lessThanEqual( 0.0 ) )
896
+ .and( max( max( material.emissive.x, material.emissive.y ), material.emissive.z ).mul( material.emissiveIntensity ).lessThanEqual( 0.0 ) ), () => {
897
+
898
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
899
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
900
+ persistRng();
901
+ Return();
902
+
903
+ } );
662
904
 
663
905
  // displacement: analytical ray-height marching refines hitPoint/UV/normal; no-op without a map
664
906
  const samplingUV = hitUV.toVar();
@@ -715,7 +957,7 @@ export function buildShadeKernel( params ) {
715
957
  const extUV = getTransformedUV( { uv: samplingUV, transform: material.albedoTransform } ).toVar();
716
958
 
717
959
  // Fold the anisotropy texture (if any) into the scalar anisotropy/rotation used by the BRDF
718
- If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
960
+ if ( layers.anisotropy ) If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
719
961
 
720
962
  const aniso = processAnisotropyMap( material, extUV ).toVar();
721
963
  material.anisotropy.assign( aniso.x );
@@ -736,6 +978,8 @@ export function buildShadeKernel( params ) {
736
978
  material.iridescenceThicknessRange.assign( vec2( material.iridescenceThicknessRange.x, extMaps.iridescenceThickness ) );
737
979
  material.specularIntensity.assign( extMaps.specularIntensity );
738
980
  material.specularColor.assign( extMaps.specularColor );
981
+ material.diffuseTransmission.assign( extMaps.diffuseTransmission );
982
+ material.diffuseTransmissionColor.assign( extMaps.diffuseTransmissionColor );
739
983
 
740
984
  // Below the floor the GGX peak is past f32, so a plain reflector reflects exactly: roughness 0
741
985
  // is the delta-lobe marker the sampler and evaluator read. The other lobes keep the floor.
@@ -769,19 +1013,19 @@ export function buildShadeKernel( params ) {
769
1013
  const metal = material.metalness.toVar();
770
1014
  const trans = material.transmission.toVar();
771
1015
  const roughFrac = smoothstep( float( 0.0 ), float( 0.15 ), rawRough ).toVar();
772
- const auxCommit = clamp(
1016
+ const auxCommit = aux ? clamp(
773
1017
  float( 1.0 ).sub( metal ).mul( float( 1.0 ).sub( trans ) )
774
1018
  .add( roughFrac.mul( metal.mul( float( 1.0 ).sub( trans ) ).add( trans ) ) ),
775
1019
  0.0, 1.0,
776
- ).greaterThanEqual( float( 0.25 ) ).toVar();
777
- const featPrefix = featCarry.toVar();
1020
+ ).greaterThanEqual( float( 0.25 ) ).toVar() : null;
1021
+ const featPrefix = aux ? featCarry.toVar() : null;
778
1022
 
779
1023
  // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
780
1024
  // surface instead of leaving a black guide OIDN would demod-amplify.
781
1025
  // Plain JS inliner, not an Fn — it closes over albedo/featPrefix/flags .toVar()s.
782
1026
  const commitDeferredAux = ( normal ) => {
783
1027
 
784
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1028
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
785
1029
 
786
1030
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
787
1031
  writeGBuffer( gBufferRW, pixelIndex, normal, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
@@ -795,14 +1039,14 @@ export function buildShadeKernel( params ) {
795
1039
 
796
1040
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
797
1041
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
798
- writeRngState( hitBufferRW, rayID, rngState );
1042
+ persistRng();
799
1043
 
800
1044
  };
801
1045
 
802
1046
  // first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
803
1047
  // The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
804
1048
  // they may defer through smooth glass/mirror and commit at the first diffuse-enough surface or the env.
805
- If( bounceIndex.equal( 0 ).and( auxOn ), () => {
1049
+ if ( aux ) If( bounceIndex.equal( 0 ).and( aux ), () => {
806
1050
 
807
1051
  const linearDepth = computeNDCDepth( {
808
1052
  worldPos: hitPoint,
@@ -847,14 +1091,14 @@ export function buildShadeKernel( params ) {
847
1091
  // ─── DDFA aux decision at a transmissive / alpha / SSS interaction. Runs for BOTH the continuing and
848
1092
  // the BLEND fall-through paths, so it sits before If(continueRay). If/ElseIf chaining so BLEND wins
849
1093
  // over the transmission branch (fixes the BLEND-glass per-frame flicker + the BLEND+transmission case). ───
850
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1094
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
851
1095
 
852
1096
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
853
1097
  // N here is the mapped normal — faceforward it (a ray exiting glass sees a back-facing N).
854
1098
  const Nff = select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ).toVar();
855
1099
  const auxAlbedo = clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
856
1100
 
857
- If( material.alphaMode.equal( int( 2 ) ), () => {
1101
+ let auxCases = If( material.alphaMode.equal( int( 2 ) ), () => {
858
1102
 
859
1103
  // BLEND: deterministic own-surface commit on BOTH stochastic skip + shade frames (no flicker).
860
1104
  writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
@@ -864,13 +1108,15 @@ export function buildShadeKernel( params ) {
864
1108
 
865
1109
  // MASK cutout passthrough: keep deferring, tint unchanged (backdrop through the hole commits later).
866
1110
 
867
- } ).ElseIf( interaction.isSubsurface, () => {
1111
+ } );
1112
+ if ( layers.subsurface ) auxCases = auxCases.ElseIf( interaction.isSubsurface, () => {
868
1113
 
869
1114
  // SSS boundary: force-commit the skin surface as diffuse.
870
1115
  writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
871
1116
  flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
872
1117
 
873
- } ).ElseIf( interaction.isTransmissive, () => {
1118
+ } );
1119
+ auxCases.ElseIf( interaction.isTransmissive, () => {
874
1120
 
875
1121
  If( auxCommit.or( material.dispersion.greaterThan( 0.0 ) ), () => {
876
1122
 
@@ -941,7 +1187,7 @@ export function buildShadeKernel( params ) {
941
1187
  } );
942
1188
 
943
1189
  // subsurface boundary: push the scattering medium on enter, pop on exit; free bounce
944
- If( interaction.isSubsurface.and( interaction.didReflect.not() ), () => {
1190
+ if ( layers.subsurface ) If( interaction.isSubsurface.and( interaction.didReflect.not() ), () => {
945
1191
 
946
1192
  If( interaction.entering, () => {
947
1193
 
@@ -994,6 +1240,15 @@ export function buildShadeKernel( params ) {
994
1240
 
995
1241
  throughput.mulAssign( interaction.throughput );
996
1242
 
1243
+ // Importance, unlike radiance, is not rescaled by refraction.
1244
+ if ( bdpt ) If( isLight.and( interaction.isTransmissive.or( interaction.isSubsurface ) ).and( interaction.didReflect.not() ), () => {
1245
+
1246
+ const n1 = select( interaction.entering, currentMediumIOR, material.ior );
1247
+ const n2 = select( interaction.entering, material.ior, previousMediumIOR );
1248
+ throughput.mulAssign( n2.mul( n2 ).div( max( n1.mul( n1 ), EPSILON ) ) );
1249
+
1250
+ } );
1251
+
997
1252
  // Off the side of the facet the new ray leaves on: reflection stays, transmission and alpha skip cross.
998
1253
  const Ng = unpackHitFacet( readHitFacet( hitBufferRW, rayID ) ).faceN;
999
1254
  const crossesFacet = dot( Ng, interaction.direction ).lessThan( 0.0 ).toVar();
@@ -1010,6 +1265,25 @@ export function buildShadeKernel( params ) {
1010
1265
  flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ) ).bitOr( uint( RAY_FLAG.REDIRECTED ) )
1011
1266
  .bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
1012
1267
 
1268
+ // Not connectible; an alpha skip is no vertex at all.
1269
+ if ( bdpt ) {
1270
+
1271
+ misOnHit( subpath, hitDist.add( readMisRayT( rayBufferRW, rayID ) ), abs( dot( exactFacetN, direction ) ) );
1272
+ misOnSpecular( subpath, abs( dot( exactFacetN, interaction.direction ) ) );
1273
+ If( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ).notEqual( uint( 0 ) ), () => {
1274
+
1275
+ landLampPath( hitDist.add( readMisRayT( rayBufferRW, rayID ) ) );
1276
+
1277
+ } );
1278
+ flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ) ) );
1279
+ If( isLight.not().and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
1280
+
1281
+ bidirectionalAreaHit( newOrigin, interaction.direction, null );
1282
+
1283
+ } );
1284
+
1285
+ }
1286
+
1013
1287
  } ).Else( () => {
1014
1288
 
1015
1289
  // Alpha passthrough: charge the guard. N is faceforwarded for the aux commit (a ray
@@ -1037,12 +1311,12 @@ export function buildShadeKernel( params ) {
1037
1311
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1038
1312
  // DDFA: persist the (possibly tinted) see-through throughput for the next bounce. MUST run after
1039
1313
  // the writeMediumSigmaA above — both RMW slot 5 (sigmaA.xyz / featTP.w). Gated AUX_LOCKED-clear.
1040
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1314
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1041
1315
 
1042
1316
  writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1043
1317
 
1044
1318
  } );
1045
- writeRngState( hitBufferRW, rayID, rngState );
1319
+ persistRng();
1046
1320
  Return();
1047
1321
 
1048
1322
  } );
@@ -1055,18 +1329,59 @@ export function buildShadeKernel( params ) {
1055
1329
  // so any later env-escape is redirected light, not the direct backdrop). Single positive bitOr.
1056
1330
  flags.assign( flags.bitOr( uint( RAY_FLAG.HAS_HIT_OPAQUE | RAY_FLAG.REDIRECTED ) ) );
1057
1331
 
1332
+ if ( bdpt ) {
1333
+
1334
+ const arrivalDist = hitDist.add( readMisRayT( rayBufferRW, rayID ) ).toVar();
1335
+
1336
+ // A light path's NEE density needs its first vertex.
1337
+ If( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ).notEqual( uint( 0 ) ), () => {
1338
+
1339
+ const start = readLightOrigin( hitBufferRW, rayID );
1340
+ const neePdfA = float( 0.0 ).toVar();
1341
+ If( start.triangle.lessThan( int( 0 ) ), () => {
1342
+
1343
+ // A light at infinity has no distance to square. The sun's NEE density is in already; a lamp's
1344
+ // depends on this vertex's material, below.
1345
+ const source = int( - 1 ).sub( start.triangle );
1346
+ neePdfA.assign( select( source.lessThan( int( SOURCE.LAMPS ) ).or( sourceLampType( bdpt, source ).equal( int( LIGHT_TYPE_DIRECTIONAL ) ) ),
1347
+ float( 1.0 ).div( max( arrivalDist.mul( arrivalDist ), 1e-30 ) ), float( 1.0 ) ) );
1348
+ landLampPath( arrivalDist );
1349
+
1350
+ } ).Else( () => {
1351
+
1352
+ // NEE draws a triangle on every side it emits from, as the light path left it.
1353
+ if ( useEmissiveNEE ) If( enableEmissiveTriangleSampling.equal( int( 1 ) )
1354
+ .and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1355
+
1356
+ neePdfA.assign( emissiveNEEPdf( start.triangle, arrivalDist, direction.negate(), hitPoint, start.instanceLeaf )
1357
+ .mul( abs( start.cosLight ) ).div( arrivalDist.mul( arrivalDist ) ) );
1358
+
1359
+ } );
1360
+
1361
+ } );
1362
+ subpath.dVCM.mulAssign( mis( neePdfA ) );
1363
+ flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ) ) );
1364
+
1365
+ } );
1366
+
1367
+ misOnHit( subpath, arrivalDist, abs( dot( exactFacetN, direction ) ) );
1368
+
1369
+ }
1370
+
1058
1371
  const emissive = matSamples.emissive.toVar();
1059
- // NEE samples only a single-sided emitter's lit side, so from behind (bounce rays) it is a dark occluder.
1060
- If( isBackdropView.not().and( length( emissive ).greaterThan( 0.0 ) ), () => {
1372
+ // NEE samples only a single-sided emitter's lit side, so from behind (bounce rays) it is a dark occluder. The side
1373
+ // is the facet's, as NEE's: the interpolated normal turns away near a coarse mesh's silhouette.
1374
+ If( cameraOnly( isBackdropView.not().and( length( emissive ).greaterThan( 0.0 ) ) ), () => {
1061
1375
 
1062
- If( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ), dot( direction, hitNormal ) ).not(), () => {
1376
+ If( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ),
1377
+ dot( direction, windingNormal( triangleBuffer, bvhBuffer, int( hitTriIdx ), hitInstance ) ) ).not(), () => {
1063
1378
 
1064
1379
  emissive.assign( vec3( 0.0 ) );
1065
1380
 
1066
1381
  } );
1067
1382
 
1068
1383
  } );
1069
- If( length( emissive ).greaterThan( 0.0 ), () => {
1384
+ If( cameraOnly( length( emissive ).greaterThan( 0.0 ) ), () => {
1070
1385
 
1071
1386
  // Key on backdrop-view (not bounceIndex>0) so an emitter seen DIRECTLY through an alpha-cutout hole
1072
1387
  // renders at full intensity (1.0) like a direct view, consistent with env-through-hole — instead of
@@ -1078,7 +1393,34 @@ export function buildShadeKernel( params ) {
1078
1393
  // two estimators. Without it emissive geometry / area lights double-count (~2x bright + noisier).
1079
1394
  // Primary hits keep weight 1.0 (the wavefront's bounce-0 stored pdf is the Generate init, not a NEE pdf).
1080
1395
  const emissiveMISWeight = float( 1.0 ).toVar();
1081
- if ( useEmissiveNEE ) {
1396
+ if ( bdpt ) {
1397
+
1398
+ // Georgiev (43): NEE from the previous vertex, and every light subpath strategy.
1399
+ If( isBackdropView.not(), () => {
1400
+
1401
+ const misT = readMisRayT( rayBufferRW, rayID ).toVar();
1402
+ const misDist = hitDist.add( misT ).toVar();
1403
+ const tri = TriangleData.wrap( fetchTriangleData( int( hitTriIdx ), triangleBuffer, bvhBuffer, hitInstance ) );
1404
+ const cosToward = dot( direction.negate(), normalize( cross( tri.v1.sub( tri.v0 ), tri.v2.sub( tri.v0 ) ) ) ).toVar();
1405
+ const areaPdf = emitterAreaPdf( material.emissive.mul( material.emissiveIntensity ), emissiveTotalPower ).toVar();
1406
+ const emissionPdf = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1407
+ areaPdf.mul( float( 1.0 ).sub( bdpt.sunPick ) ).mul( emitterSideProbability( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ), cosToward ) )
1408
+ .mul( abs( cosToward ) ).div( Math.PI ),
1409
+ float( 0.0 ) );
1410
+ const neePdfA = float( 0.0 ).toVar();
1411
+ if ( useEmissiveNEE ) If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
1412
+ .and( areaPdf.greaterThan( 0.0 ) ), () => {
1413
+
1414
+ neePdfA.assign( emissiveNEEPdf( int( hitTriIdx ), misDist, direction, origin.sub( direction.mul( misT ) ), hitInstance )
1415
+ .mul( abs( cosToward ) ).div( misDist.mul( misDist ) ) );
1416
+
1417
+ } );
1418
+ emissiveMISWeight.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
1419
+ misWeight( float( 0.0 ), mis( neePdfA ).mul( subpath.dVCM ).add( mis( emissionPdf ).mul( subpath.dVC ) ) ) ) );
1420
+
1421
+ } );
1422
+
1423
+ } else if ( useEmissiveNEE ) {
1082
1424
 
1083
1425
  If( enableEmissiveTriangleSampling.equal( int( 1 ) )
1084
1426
  .and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
@@ -1133,6 +1475,13 @@ export function buildShadeKernel( params ) {
1133
1475
 
1134
1476
  } );
1135
1477
 
1478
+ If( flags.bitAnd( uint( RAY_FLAG.EMISSION_ONLY ) ).notEqual( uint( 0 ) ), () => {
1479
+
1480
+ terminatePath();
1481
+ Return();
1482
+
1483
+ } );
1484
+
1136
1485
  // BRDF sample (needed by both direct + indirect)
1137
1486
  const V = direction.negate().toVar();
1138
1487
  // face-forwarded geometric normal: horizon guard for NEE and the bounce continuation
@@ -1146,6 +1495,13 @@ export function buildShadeKernel( params ) {
1146
1495
  hitPoint, offsetNormal: facetN, lift: terminatorLift, faceN: facetN, smoothN: NgeoFF, L,
1147
1496
  cutoff: shadowTerminatorOffset,
1148
1497
  } );
1498
+ // Light from behind a surface that passes light through diffusely: its rays leave from the other side.
1499
+ // Compiled in only with the lobe (see `diffuseTransmission`).
1500
+ const transmitting = diffuseTransmission && ! bdpt;
1501
+ const transmitsDiffusely = transmitting ? material.diffuseTransmission.greaterThan( 0.0 ) : tslBool( false );
1502
+ const throughSurface = L => ( transmitting ? transmitsDiffusely.and( dot( N, L ).lessThan( 0.0 ) ).and( dot( L, NgeoFF ).lessThan( 0.0 ) ) : tslBool( false ) );
1503
+ const backOrigin = transmitting ? offsetRayOrigin( hitPoint, facetN.negate() ) : null;
1504
+ const originFor = ( L, front ) => ( transmitting ? select( throughSurface( L ), backOrigin, front ) : front );
1149
1505
 
1150
1506
  // Two-sided shading: opaque path only (transmissive/SSS already continued). Decide the flip on the
1151
1507
  // GEOMETRIC normal — an inward-normal / double-sided mesh (GLB/PBRT) faces away as a whole — so a
@@ -1164,7 +1520,7 @@ export function buildShadeKernel( params ) {
1164
1520
  // ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
1165
1521
  // mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
1166
1522
  // viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
1167
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1523
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1168
1524
 
1169
1525
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1170
1526
  If( auxCommit, () => {
@@ -1211,8 +1567,28 @@ export function buildShadeKernel( params ) {
1211
1567
  const brdfPdf = float( 0.0 ).toVar();
1212
1568
  const brdfIsTransmission = tslBool( false ).toVar();
1213
1569
  const brdfColorWeight = vec3( 1.0 ).toVar();
1570
+ const brdfIsDiffuseTransmission = tslBool( false ).toVar();
1571
+
1572
+ const sampleLobes = () => {
1573
+
1574
+ const bs = DirectionSample.wrap( generateSampledDirection(
1575
+ V, N, material, xi, lobeXi, rngState,
1576
+ _pixelCoord, resolution, frame, dimBase,
1577
+ mc,
1578
+ false, emptyWeights,
1579
+ false, emptyCache,
1580
+ ) );
1581
+ brdfDir.assign( bs.direction );
1582
+ brdfValue.assign( bs.value );
1583
+ brdfPdf.assign( bs.pdf );
1584
+ brdfIsTransmission.assign( bs.isTransmission );
1585
+ brdfColorWeight.assign( bs.colorWeight );
1586
+ brdfIsDiffuseTransmission.assign( bs.isDiffuseTransmission );
1214
1587
 
1215
- If( material.clearcoat.greaterThan( 0.0 ), () => {
1588
+ };
1589
+
1590
+ if ( ! layers.clearcoat ) sampleLobes();
1591
+ else If( material.clearcoat.greaterThan( 0.0 ), () => {
1216
1592
 
1217
1593
  const ccRay = Ray( { origin, direction } );
1218
1594
  const ccHit = HitInfo( {
@@ -1227,25 +1603,11 @@ export function buildShadeKernel( params ) {
1227
1603
  brdfDir.assign( ccResult.L );
1228
1604
  brdfValue.assign( ccResult.brdf );
1229
1605
  brdfPdf.assign( ccResult.pdf );
1606
+ brdfIsDiffuseTransmission.assign( ccResult.isDiffuseTransmission );
1230
1607
 
1231
- } ).Else( () => {
1232
-
1233
- const bs = DirectionSample.wrap( generateSampledDirection(
1234
- V, N, material, xi, lobeXi, rngState,
1235
- _pixelCoord, resolution, frame, dimBase,
1236
- mc,
1237
- false, emptyWeights,
1238
- false, emptyCache,
1239
- ) );
1240
- brdfDir.assign( bs.direction );
1241
- brdfValue.assign( bs.value );
1242
- brdfPdf.assign( bs.pdf );
1243
- brdfIsTransmission.assign( bs.isTransmission );
1244
- brdfColorWeight.assign( bs.colorWeight );
1245
-
1246
- } );
1608
+ } ).Else( sampleLobes );
1247
1609
 
1248
- const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
1610
+ const directLighting = () => DirectLightingDual.wrap( ( transmitting ? calculateDirectLightingThroughSurfaces : calculateDirectLightingUnified )(
1249
1611
  hitPoint, N, NgeoFF, material, V,
1250
1612
  brdfDir, brdfPdf, brdfValue,
1251
1613
  bounceIndex, rngState,
@@ -1257,18 +1619,20 @@ export function buildShadeKernel( params ) {
1257
1619
  bvhBuffer, triangleBuffer, materialBuffer,
1258
1620
  envTexture, environmentIntensity, envMatrix,
1259
1621
  envCDFTexture,
1260
- envTotalSum, envCompensationDelta, envResolution,
1622
+ envTotalSum, envResolution,
1261
1623
  enableEnvironmentLight,
1262
1624
  tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
1263
1625
  terminatorLift, facetN, shadowTerminatorOffset,
1264
1626
  hasSun, sunDirection, sunRadiance, sunParams,
1265
- ) ).shadowed.toVar();
1627
+ ) ).shadowed;
1628
+ // Bidirectional mode samples every light itself, below.
1629
+ const directLight = bdpt ? null : directLighting().toVar();
1266
1630
 
1267
1631
  const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
1268
1632
  // Per-term firefly suppression (megakernel parity: PathTracerCore.js:1164) — wrap the direct-light add
1269
1633
  // like every other contribution (env/emissive-hit/emissive-NEE). This replaces the cumulative catch-all
1270
1634
  // that re-suppressed already-wrapped terms + prior-bounce radiance — suppress(a+b) ≠ suppress(a)+suppress(b) (gap #13).
1271
- currentRadiance.assign( vec4(
1635
+ if ( ! bdpt ) currentRadiance.assign( vec4(
1272
1636
  currentRadiance.xyz.add(
1273
1637
  regularizePathContribution(
1274
1638
  throughput.mul( directLight ).mul( giScale ),
@@ -1278,8 +1642,319 @@ export function buildShadeKernel( params ) {
1278
1642
  currentRadiance.w
1279
1643
  ) );
1280
1644
 
1645
+ // Georgiev (44)-(45). Glass blocks the shadow ray: light through it arrives on the light subpaths.
1646
+ const bidirectionalEmissiveNEE = () => {
1647
+
1648
+ const position = vec3( 0.0 ).toVar();
1649
+ const lightDir = vec3( 0.0 ).toVar();
1650
+ const emission = vec3( 0.0 ).toVar();
1651
+ const lightPdf = float( 0.0 ).toVar();
1652
+ const valid = tslBool( false ).toVar();
1653
+ const lightTriangle = int( 0 ).toVar();
1654
+ const windingN = vec3( 0.0 ).toVar();
1655
+ const take = ( sample ) => {
1656
+
1657
+ const e = EmissiveSampleIndexed.wrap( sample );
1658
+ position.assign( e.position );
1659
+ lightDir.assign( e.direction );
1660
+ emission.assign( e.emission );
1661
+ lightPdf.assign( e.pdf );
1662
+ valid.assign( e.valid );
1663
+ lightTriangle.assign( e.triangleIndex );
1664
+ windingN.assign( e.faceNormal );
1665
+
1666
+ };
1667
+
1668
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
1669
+
1670
+ take( sampleLightBVHTriangleIndexed(
1671
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1672
+ lightBuffer, lightBuffer, emissiveVec4Offset, triangleBuffer, bvhBuffer, tslBool( false ),
1673
+ ) );
1674
+
1675
+ } ).Else( () => {
1676
+
1677
+ take( sampleEmissiveTriangleIndexed(
1678
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1679
+ lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, triangleBuffer, bvhBuffer, tslBool( false ),
1680
+ ) );
1681
+
1682
+ } );
1683
+
1684
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1685
+ const side = triangleSide( triangleRow( triangleBuffer, lightTriangle, 4 ).z ).toVar();
1686
+ If( valid.and( lightPdf.greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) )
1687
+ .and( sideAccepts( side, dot( lightDir, windingN ) ) ), () => {
1688
+
1689
+ const rayOrigin = lightShadowOrigin( lightDir ).toVar();
1690
+ const toSample = position.sub( rayOrigin ).toVar();
1691
+ const shadowDist = length( toSample ).toVar();
1692
+ const visibility = traceShadowRayRefractiveOpaque(
1693
+ rayOrigin, toSample.div( shadowDist ), shadowDist.mul( SHADOW_END ),
1694
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1695
+ ).toVar();
1696
+
1697
+ If( visibility.greaterThan( 0.0 ), () => {
1698
+
1699
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1700
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1701
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1702
+ // Emission density over NEE's, both seen from here; the light's cosine cancels.
1703
+ const emissionOverDirect = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1704
+ emitterAreaPdf( emission, emissiveTotalPower, float( 1.0 ).sub( bdpt.sunPick ) )
1705
+ .mul( emitterSideProbability( side, dot( lightDir.negate(), windingN ) ) )
1706
+ .mul( abs( dot( lightDir, exactFacetN ) ) ).div( lightPdf.mul( Math.PI ) ),
1707
+ float( 0.0 ) );
1708
+ const wLight = mis( forward.div( lightPdf ) );
1709
+ const wCamera = misPartial( emissionOverDirect, subpath, reverse );
1710
+
1711
+ currentRadiance.assign( vec4(
1712
+ currentRadiance.xyz.add(
1713
+ regularizePathContribution(
1714
+ emission.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility ).mul( emissiveBoost )
1715
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, misWeight( wLight, wCamera ) ) ).mul( throughput ).mul( giScale ),
1716
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1717
+ ),
1718
+ ),
1719
+ currentRadiance.w,
1720
+ ) );
1721
+
1722
+ } );
1723
+
1724
+ } );
1725
+
1726
+ };
1727
+
1728
+ // Georgiev (44)-(45) for the sun; as for emitters, glass blocks the shadow ray.
1729
+ const bidirectionalSunNEE = () => {
1730
+
1731
+ const lightDir = sampleSunDisc( sunDirection, sunParams, getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 9 ) ), rngState, resolution, frame ) ).toVar();
1732
+ const radiance = sunRadianceToward( lightDir, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ).toVar();
1733
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1734
+ If( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1735
+
1736
+ const visibility = traceShadowRayRefractiveOpaque(
1737
+ lightShadowOrigin( lightDir ), lightDir, float( 1e20 ),
1738
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1739
+ ).toVar();
1740
+
1741
+ If( visibility.greaterThan( 0.0 ), () => {
1742
+
1743
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1744
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1745
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1746
+ const emissionOverDirect = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1747
+ sunEmissionPdf( bdpt, sunParams, guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).mul( sunParams.y ).mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1748
+ const weight = misWeight( mis( forward.mul( sunParams.y ) ), misPartial( emissionOverDirect, subpath, reverse ) );
1749
+
1750
+ currentRadiance.assign( vec4(
1751
+ currentRadiance.xyz.add(
1752
+ regularizePathContribution(
1753
+ radiance.mul( brdfVal ).mul( NoL ).mul( sunParams.y ).mul( visibility )
1754
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, weight ) ).mul( throughput ).mul( giScale ),
1755
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1756
+ ),
1757
+ ),
1758
+ currentRadiance.w,
1759
+ ) );
1760
+
1761
+ } );
1762
+
1763
+ } );
1764
+
1765
+ };
1766
+
1767
+ if ( bdpt ) If( isLight.not().and( hasSun.greaterThan( int( 0 ) ) ).and( enableEnvironmentLight ), () => {
1768
+
1769
+ bidirectionalSunNEE();
1770
+
1771
+ } );
1772
+
1773
+ // Georgiev (44)-(45) for the environment, drawn exactly (environmentPdfExact); glass blocks it as it does the sun's.
1774
+ const bidirectionalEnvironmentNEE = () => {
1775
+
1776
+ const drawn = sampleEnvironmentExact( envCDFTexture, envMatrix, envResolution,
1777
+ getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ) );
1778
+ const lightDir = drawn.direction.toVar();
1779
+ const lightPdf = drawn.pdf.toVar();
1780
+ const radiance = sampleEnvironment( {
1781
+ tex: envTexture, samp: sampler( envTexture ), direction: lightDir, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight: float( 1.0 ),
1782
+ } ).xyz.toVar();
1783
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1784
+ If( lightPdf.greaterThan( 0.0 ).and( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) )
1785
+ .and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1786
+
1787
+ const visibility = traceShadowRayRefractiveOpaque(
1788
+ lightShadowOrigin( lightDir ), lightDir, float( 1e20 ), traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1789
+ ).toVar();
1790
+
1791
+ If( visibility.greaterThan( 0.0 ), () => {
1792
+
1793
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1794
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1795
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1796
+ const lead = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1797
+ bdpt.envPick.mul( guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1798
+ const weight = misWeight( mis( forward.div( lightPdf ) ), misPartial( lead, subpath, reverse ) );
1799
+
1800
+ currentRadiance.assign( vec4(
1801
+ currentRadiance.xyz.add(
1802
+ regularizePathContribution(
1803
+ radiance.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility )
1804
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, weight ) ).mul( throughput ).mul( giScale ),
1805
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1806
+ ),
1807
+ ),
1808
+ currentRadiance.w,
1809
+ ) );
1810
+
1811
+ } );
1812
+
1813
+ } );
1814
+
1815
+ };
1816
+
1817
+ if ( bdpt ) If( isLight.not().and( enableEnvironmentLight ).and( bdpt.envTable.greaterThan( uint( 0 ) ) ), () => {
1818
+
1819
+ bidirectionalEnvironmentNEE();
1820
+
1821
+ } );
1822
+
1823
+ // The lamps' reservoir total here, for the BSDF-hit partner at the continuation.
1824
+ const lampTotal = bdpt ? float( 0.0 ).toVar() : null;
1825
+
1826
+ // Georgiev (44)-(45) for a lamp, picked as the path tracer picks it. A point, a spot or a directional light no
1827
+ // camera path can hit; a rect light's partner is the continuation's hit (bidirectionalAreaHit).
1828
+ const bidirectionalLampNEE = () => {
1829
+
1830
+ const lightRandom = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
1831
+ const pick = LampPick.wrap( pickLamp( lamps, hitPoint, N, material, lightRandom.x,
1832
+ ( dim ) => getRandomSample1D( _pixelCoord, int( 0 ), dim, rngState, resolution, frame ) ) ).toVar();
1833
+ lampTotal.assign( pick.total );
1834
+
1835
+ const valid = tslBool( false ).toVar();
1836
+ const lightDir = vec3( 0.0, 1.0, 0.0 ).toVar();
1837
+ const emission = vec3( 0.0 ).toVar();
1838
+ const dist = float( 0.0 ).toVar();
1839
+ const lightPdf = float( 0.0 ).toVar();
1840
+ // Emission density over NEE's, but for the cosine here.
1841
+ const emissionOverDirect = float( 0.0 ).toVar();
1842
+ const take = ( sample ) => {
1843
+
1844
+ const ls = LightSample.wrap( sample );
1845
+ valid.assign( ls.valid );
1846
+ lightDir.assign( ls.direction );
1847
+ emission.assign( ls.emission );
1848
+ dist.assign( ls.distance );
1849
+ lightPdf.assign( ls.pdf );
1850
+
1851
+ };
1852
+
1853
+ const pE = sourcePick( bdpt, lampSource( bdpt, pick.kind, pick.index ) ).toVar();
1854
+
1855
+ If( pick.kind.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
1856
+
1857
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, pick.index ) );
1858
+ take( sampleDirectionalLight( light, hitPoint, pick.pdf, lightRandom.y,
1859
+ getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 14 ) ), rngState, resolution, frame ) ) );
1860
+ emissionOverDirect.assign( pE.mul( guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).div( max( pick.pdf, 1e-30 ) ) );
1861
+
1862
+ } ).ElseIf( pick.kind.equal( int( LIGHT_TYPE_AREA ) ), () => {
1863
+
1864
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, pick.index ) );
1865
+ take( sampleRectAreaLight( light, hitPoint,
1866
+ vec2( lightRandom.y, getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ), pick.pdf ) );
1867
+ emissionOverDirect.assign( pE.div( max( light.area.mul( Math.PI ).mul( lightPdf ), 1e-30 ) ) );
1868
+
1869
+ } ).ElseIf( pick.kind.equal( int( LIGHT_TYPE_POINT ) ), () => {
1870
+
1871
+ take( samplePointLightWithAttenuation( PointLight.wrap( getPointLight( pointLightsBuffer, pick.index ) ), hitPoint, pick.pdf ) );
1872
+ emissionOverDirect.assign( pE.div( max( float( 4 * Math.PI ).mul( pick.pdf ).mul( dist ).mul( dist ), 1e-30 ) ) );
1873
+
1874
+ } ).Else( () => {
1875
+
1876
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, pick.index ) );
1877
+ take( sampleSpotLightWithRadius( light, hitPoint, pick.pdf ) );
1878
+ emissionOverDirect.assign( pE.div( max( spotConeSolidAngle( light ).mul( pick.pdf ).mul( dist ).mul( dist ), 1e-30 ) ) );
1879
+
1880
+ } );
1881
+
1882
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1883
+ If( valid.and( lightPdf.greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1884
+
1885
+ const atInfinity = pick.kind.equal( int( LIGHT_TYPE_DIRECTIONAL ) );
1886
+ const shadowOrigin = lightShadowOrigin( lightDir ).toVar();
1887
+ const toSample = hitPoint.add( lightDir.mul( dist ) ).sub( shadowOrigin ).toVar();
1888
+ const shadowDist = length( toSample ).toVar();
1889
+ const visibility = traceShadowRayRefractiveOpaque(
1890
+ shadowOrigin, select( atInfinity, lightDir, toSample.div( shadowDist ) ), select( atInfinity, float( 1e20 ), shadowDist.mul( SHADOW_END ) ),
1891
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1892
+ ).toVar();
1893
+
1894
+ If( visibility.greaterThan( 0.0 ), () => {
1895
+
1896
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1897
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1898
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1899
+ const lead = select( bdpt.lightPaths.greaterThan( uint( 0 ) ), emissionOverDirect.mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1900
+ const wLight = select( pick.kind.equal( int( LIGHT_TYPE_AREA ) ), mis( forward.div( lightPdf ) ), float( 0.0 ) );
1901
+
1902
+ currentRadiance.assign( vec4(
1903
+ currentRadiance.xyz.add(
1904
+ regularizePathContribution(
1905
+ emission.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility )
1906
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, misWeight( wLight, misPartial( lead, subpath, reverse ) ) ) ).mul( throughput ).mul( giScale ),
1907
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1908
+ ),
1909
+ ),
1910
+ currentRadiance.w,
1911
+ ) );
1912
+
1913
+ } );
1914
+
1915
+ } );
1916
+
1917
+ };
1918
+
1919
+ if ( bdpt ) If( isLight.not().and( lampCount( lamps ).greaterThan( int( 0 ) ) ), () => {
1920
+
1921
+ bidirectionalLampNEE();
1922
+
1923
+ } );
1924
+
1925
+ // NEE's density for a lamp's light path depends on this vertex's material.
1926
+ if ( bdpt ) If( isLight.and( lampLanding.source.greaterThanEqual( int( SOURCE.LAMPS ) ) ), () => {
1927
+
1928
+ const type = sourceLampType( bdpt, lampLanding.source ).toVar();
1929
+ const index = sourceLampIndex( bdpt, lampLanding.source, type ).toVar();
1930
+ const d = lampLanding.dist;
1931
+ const direct = lampPickPdf( lamps, type, index, hitPoint, N, material ).toVar();
1932
+ If( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
1933
+
1934
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, index ) );
1935
+ direct.divAssign( select( light.angle.greaterThan( 0.0 ), directionalConeSolidAngle( light ), float( 1.0 ) ) );
1936
+
1937
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_AREA ) ), () => {
1938
+
1939
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, index ) );
1940
+ const cosLight = dot( direction, light.normal ).toVar();
1941
+ direct.mulAssign( areaLightDirectPdfW( light, hitPoint, d, cosLight ).mul( cosLight ).div( max( d.mul( d ), 1e-30 ) ) );
1942
+
1943
+ } );
1944
+ subpath.dVCM.mulAssign( mis( direct ) );
1945
+
1946
+ } );
1947
+
1281
1948
  // emissive triangle NEE: light-BVH fast path when available, flat-CDF fallback otherwise
1282
- if ( useEmissiveNEE ) {
1949
+ if ( useEmissiveNEE && bdpt ) {
1950
+
1951
+ If( isLight.not().and( enableEmissiveTriangleSampling.equal( int( 1 ) ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1952
+
1953
+ bidirectionalEmissiveNEE();
1954
+
1955
+ } );
1956
+
1957
+ } else if ( useEmissiveNEE ) {
1283
1958
 
1284
1959
  If(
1285
1960
  enableEmissiveTriangleSampling.equal( int( 1 ) )
@@ -1306,18 +1981,22 @@ export function buildShadeKernel( params ) {
1306
1981
  lightBuffer,
1307
1982
  emissiveVec4Offset,
1308
1983
  triangleBuffer, bvhBuffer,
1984
+ transmitsDiffusely,
1309
1985
  ) );
1310
1986
 
1311
1987
  // No rough-diffuse secondary-bounce skip here: dropping NEE while the emissive-hit
1312
1988
  // MIS still down-weights BSDF hits deletes emitter energy from deep GI.
1313
1989
  If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1314
1990
 
1315
- const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
1991
+ const back = throughSurface( emissiveSample.direction ).toVar();
1992
+ const NoL = transmitting ? abs( dot( N, emissiveSample.direction ) ) : max( float( 0.0 ), dot( N, emissiveSample.direction ) );
1993
+ const inFront = NoL.greaterThan( 0.0 ).and( dot( N, emissiveSample.direction ).greaterThan( 0.0 ) )
1994
+ .and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) );
1316
1995
 
1317
- If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) ), () => {
1996
+ If( transmitting ? back.or( inFront ) : inFront, () => {
1318
1997
 
1319
1998
  // Aimed at the sampled point and stopped a relative hair short, as for area lights.
1320
- const rayOrigin = lightShadowOrigin( emissiveSample.direction ).toVar();
1999
+ const rayOrigin = originFor( emissiveSample.direction, lightShadowOrigin( emissiveSample.direction ) ).toVar();
1321
2000
  const toSample = emissiveSample.position.sub( rayOrigin ).toVar();
1322
2001
  const shadowDist = length( toSample ).toVar();
1323
2002
  const visibility = traceShadowRayWrapped(
@@ -1365,7 +2044,7 @@ export function buildShadeKernel( params ) {
1365
2044
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1366
2045
  triangleBuffer, bvhBuffer,
1367
2046
  traceShadowRayWrapped,
1368
- Fn( ( [ p, , L ] ) => lightShadowOrigin( L ).sub( p ) ),
2047
+ Fn( ( [ p, , L ] ) => originFor( L, lightShadowOrigin( L ) ).sub( p ) ),
1369
2048
  );
1370
2049
 
1371
2050
  currentRadiance.assign( vec4(
@@ -1389,10 +2068,46 @@ export function buildShadeKernel( params ) {
1389
2068
  // per-term (env / emissive-hit / direct-light / emissive-NEE), matching the megakernel which never
1390
2069
  // re-suppresses the running radiance.
1391
2070
 
2071
+ if ( bdpt ) {
2072
+
2073
+ // Light from below the shading normal: a camera path neither samples it nor lets NEE take it.
2074
+ If( isLight.and( dot( V, N ).lessThanEqual( 0.0 ) ), () => {
2075
+
2076
+ terminatePath();
2077
+ Return();
2078
+
2079
+ } );
2080
+
2081
+ If( isLight, () => {
2082
+
2083
+ // The path's vertex count rides in its first slot.
2084
+ const first = rayID.mul( bdpt.slotsPerPath ).toVar();
2085
+ writeVertexRecord( hitBufferRW, cachedVertex( first.add( uint( cameraDepth ) ) ), {
2086
+ position: hitPoint, tag: uint( 0 ), throughput: throughput.mul( readLightOrigin( hitBufferRW, rayID ).scale ),
2087
+ V, N, facetN: exactFacetN, materialIndex: hitMatIdx, uv: samplingUV, dVCM: subpath.dVCM, dVC: subpath.dVC,
2088
+ // 1: a camera on this side would cull the face, so light tracing must not show it.
2089
+ extra: select( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ),
2090
+ dot( direction, windingNormal( triangleBuffer, bvhBuffer, int( hitTriIdx ), hitInstance ) ) ), uint( 0 ), uint( 1 ) ),
2091
+ } );
2092
+ writeLightPathLength( hitBufferRW, cachedVertex( first ), bdpt.lightTag, uint( cameraDepth ).add( uint( 1 ) ) );
2093
+
2094
+ } ).ElseIf( bdpt.lightPaths.greaterThan( uint( 0 ) ).and( bdpt.merging ? cameraDepth.lessThanEqual( maxBounceCount ) : cameraDepth.lessThan( maxBounceCount ) ), () => {
2095
+
2096
+ // For ConnectKernel (and MergeKernel); a light vertex must still fit in the bounce budget.
2097
+ writeVertexRecord( hitBufferRW, pendingVertex( rayID ), {
2098
+ position: hitPoint, tag: bdpt.passTag, throughput,
2099
+ V, N, facetN: exactFacetN, materialIndex: hitMatIdx, uv: samplingUV, dVCM: subpath.dVCM, dVC: subpath.dVC, extra: uint( cameraDepth ),
2100
+ } );
2101
+
2102
+ } );
2103
+
2104
+ }
2105
+
1392
2106
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1393
2107
  N, material,
1394
2108
  brdfDir, brdfPdf, brdfValue,
1395
2109
  brdfIsTransmission, brdfColorWeight,
2110
+ brdfIsDiffuseTransmission,
1396
2111
  rngState,
1397
2112
  _pixelCoord, resolution, frame, dimBase,
1398
2113
  ) ).toVar();
@@ -1400,8 +2115,10 @@ export function buildShadeKernel( params ) {
1400
2115
  const bounceDir = indirectResult.direction.toVar();
1401
2116
 
1402
2117
  // Shading-normal leak guard: a normal-mapped lobe can sample below the geometric surface;
1403
- // tracing that ray tunnels through single-sided shells onto whatever sits behind them.
1404
- If( brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ), () => {
2118
+ // tracing that ray tunnels through single-sided shells onto whatever sits behind them. A diffuse
2119
+ // transmission draw is the mirror case: it must cross the surface.
2120
+ If( select( brdfIsDiffuseTransmission, dot( bounceDir, NgeoFF ).greaterThanEqual( 0.0 ),
2121
+ brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ) ), () => {
1405
2122
 
1406
2123
  commitDeferredAux( N );
1407
2124
  terminatePath();
@@ -1410,7 +2127,35 @@ export function buildShadeKernel( params ) {
1410
2127
  } );
1411
2128
  // combinedPdf is stored as next bounce's prevBouncePdf for NEE↔implicit-env MIS
1412
2129
  const bouncePdf = max( indirectResult.combinedPdf, 0.001 ).toVar();
1413
- throughput.mulAssign( indirectResult.throughput );
2130
+ const specularScatter = bdpt ? brdfIsTransmission.or( brdfPdf.greaterThanEqual( DELTA_PDF * 0.5 ) ).toVar() : null;
2131
+ if ( bdpt ) {
2132
+
2133
+ If( isLight, () => {
2134
+
2135
+ // Light flows V → bounceDir; the BSDF is not reciprocal. Shading-normal correction: Veach 5.3.2.
2136
+ const correction = lightEndCosine( V, N, exactFacetN, bounceDir ).div( max( abs( dot( bounceDir, N ) ), 1e-4 ) ).toVar();
2137
+ If( specularScatter.or( brdfPdf.greaterThan( 0.0 ).not() ), () => {
2138
+
2139
+ throughput.mulAssign( indirectResult.throughput.mul( correction ) );
2140
+
2141
+ } ).Else( () => {
2142
+
2143
+ throughput.mulAssign( evaluateMaterialResponse( bounceDir, V, N, material )
2144
+ .mul( max( dot( N, bounceDir ), 0.0 ) ).div( max( brdfPdf, MIN_PDF ) ).mul( correction ) );
2145
+
2146
+ } );
2147
+
2148
+ } ).Else( () => {
2149
+
2150
+ throughput.mulAssign( indirectResult.throughput );
2151
+
2152
+ } );
2153
+
2154
+ } else {
2155
+
2156
+ throughput.mulAssign( indirectResult.throughput );
2157
+
2158
+ }
1414
2159
 
1415
2160
  // Adaptive Russian roulette (gap #7) — material-importance + throughput + env-direction aware, replacing
1416
2161
  // the flat clamp(maxThroughput,0.05,0.95). depth = bounceIndex (path length, per gap #4); rayDirection =
@@ -1434,17 +2179,50 @@ export function buildShadeKernel( params ) {
1434
2179
  If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
1435
2180
 
1436
2181
  commitDeferredAux( N );
1437
- terminatePath();
1438
- Return();
2182
+ // One segment more, for the light it may hit: light sampled here keeps its BSDF-hit partner (and its MIS
2183
+ // weight), and bidirectional strategies all reach the longest paths.
2184
+ if ( bdpt ) If( isLight, () => {
2185
+
2186
+ terminatePath();
2187
+ Return();
2188
+
2189
+ } );
2190
+ flags.assign( flags.bitOr( uint( RAY_FLAG.EMISSION_ONLY | RAY_FLAG.AUX_LOCKED ) ) );
1439
2191
 
1440
2192
  } );
1441
2193
 
2194
+ if ( bdpt ) {
2195
+
2196
+ const cosOut = abs( dot( bounceDir, exactFacetN ) );
2197
+ If( specularScatter, () => {
2198
+
2199
+ misOnSpecular( subpath, cosOut );
2200
+
2201
+ } ).Else( () => {
2202
+
2203
+ misOnScatter( subpath, cosOut, calculateMaterialPDF( V, bounceDir, N, material ), calculateMaterialPDF( bounceDir, V, N, material ) );
2204
+
2205
+ } );
2206
+
2207
+ }
2208
+
1442
2209
  const underFacet = dot( facetN, bounceDir ).lessThan( 0.0 ).toVar();
1443
2210
  const newOrigin = offsetRayOrigin( hitPoint, select( underFacet, facetN.negate(), facetN ) );
1444
2211
 
2212
+ if ( bdpt ) If( isLight.not().and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
2213
+
2214
+ const connectible = specularScatter.not().and( dot( N, bounceDir ).greaterThan( 0.0 ) ).and( dot( bounceDir, NgeoFF ).greaterThan( 0.0 ) );
2215
+ bidirectionalAreaHit( newOrigin, bounceDir, ( light ) => select( connectible, select( lampTotal.greaterThan( 0.0 ),
2216
+ estimateLightImportance( light, hitPoint, N, material ).div( max( lampTotal, 1e-30 ) ),
2217
+ float( 1.0 ).div( max( float( lampCount( lamps ) ), 1.0 ) ) ), float( 0.0 ) ) );
2218
+
2219
+ } );
2220
+
1445
2221
  // Whether this vertex's sun NEE could have drawn bounceDir, so a sun hit at the miss knows its MIS partner.
1446
- const sunNEE = brdfIsTransmission.not().and( dot( N, bounceDir ).greaterThan( 0.0 ) );
1447
- const underSurface = brdfIsTransmission.not().and( underFacet );
2222
+ // Through a diffusely transmitting surface it samples the far side too.
2223
+ const sunNEE = brdfIsTransmission.not().and( dot( N, bounceDir ).greaterThan( 0.0 ).or( brdfIsDiffuseTransmission ) );
2224
+ // Diffuse transmission crosses the surface on purpose; the next hit is an ordinary bounce's.
2225
+ const underSurface = brdfIsTransmission.not().and( brdfIsDiffuseTransmission.not() ).and( underFacet );
1448
2226
  flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.SUN_NEE | RAY_FLAG.UNDER_SURFACE ) ) )
1449
2227
  .bitOr( select( sunNEE, uint( RAY_FLAG.SUN_NEE ), uint( 0 ) ) )
1450
2228
  .bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
@@ -1456,12 +2234,12 @@ export function buildShadeKernel( params ) {
1456
2234
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1457
2235
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1458
2236
  // DDFA: persist the (possibly mirror-tinted) see-through throughput for the next bounce.
1459
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
2237
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1460
2238
 
1461
2239
  writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1462
2240
 
1463
2241
  } );
1464
- writeRngState( hitBufferRW, rayID, rngState );
2242
+ persistRng();
1465
2243
 
1466
2244
  } );
1467
2245