rayzee 10.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -7,35 +7,48 @@
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
+ calculateDirectLightingPicked,
25
+ sampleDirectionalLight, sampleRectAreaLight, samplePointLightWithAttenuation, sampleSpotLightWithRadius,
26
+ areaLightRadiance, areaLightSpreadAttenuation,
27
+ } from './LightsSampling.js';
28
+ import { traceShadowRay, traceShadowRayRefractiveOpaque, estimateLightImportance } from './LightsDirect.js';
24
29
  import { shadowTerminatorOrigin } from './ShadowTerminator.js';
25
- import { unpackHitFacet } from './HitFacet.js';
30
+ import { hitFacet, unpackHitFacet, windingNormal } from './HitFacet.js';
26
31
  import { traverseBVHShadow, triangleSide, sideAccepts } from './BVHTraversal.js';
32
+ import { visibilityCell } from './LightVisibility.js';
27
33
  import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
28
34
  import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
35
+ import { ALL_MATERIAL_LAYERS } from './SceneResources.js';
29
36
  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';
37
+ import {
38
+ IndirectLightingResult, sampleCone, LightSample, DirectionalLight, AreaLight, PointLight, SpotLight,
39
+ getDirectionalLight, getAreaLight, getPointLight, getSpotLight, intersectAreaLight, getDistanceAttenuation, sampleDirectionalGoboMask,
40
+ LIGHT_TYPE_DIRECTIONAL, LIGHT_TYPE_AREA, LIGHT_TYPE_POINT, LIGHT_TYPE_SPOT,
41
+ } from './LightsCore.js';
42
+ import { sunRadianceToward, sampleSunDisc } from './Sun.js';
43
+ import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette, DELTA_PDF } from './PathTracerCore.js';
33
44
  import { evaluateSpecularDFG, baseFresnelParams } from './MaterialProperties.js';
34
- import { NRD_HIT_DIST_A, NRD_HIT_DIST_B } from '../EngineDefaults.js';
35
45
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
36
46
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
37
- import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
38
- import { sampleLightBVHTriangle, calculateLightBVHPdf } from './LightBVHSampling.js';
47
+ import {
48
+ calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample, EmissiveSampleIndexed,
49
+ sampleEmissiveTriangle, sampleEmissiveTriangleIndexed, fetchTriangleData, TriangleData,
50
+ } from './EmissiveSampling.js';
51
+ import { sampleLightBVHTriangle, sampleLightBVHTriangleIndexed, calculateLightBVHPdf } from './LightBVHSampling.js';
39
52
  import {
40
53
  Ray,
41
54
  HitInfo,
@@ -65,6 +78,7 @@ import {
65
78
  readRayRadiance,
66
79
  readFeatureThroughput, writeFeatureThroughput,
67
80
  readRngState, writeRngState,
81
+ readMisState, writeRngMis, pendingVertex, cachedVertex, writeVertexRecord, writeLightPathLength, readLightOrigin,
68
82
  } from '../Processor/PackedRayBuffer.js';
69
83
 
70
84
  const WG_SIZE = 256;
@@ -88,7 +102,7 @@ export function buildShadeKernel( params ) {
88
102
  enableEnvironmentLight,
89
103
  groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
90
104
  enableGroundCatcher, groundCatcherHeight,
91
- envTotalSum, envCompensationDelta, envResolution,
105
+ envTotalSum, envResolution,
92
106
  directionalLightsBuffer, numDirectionalLights,
93
107
  areaLightsBuffer, numAreaLights,
94
108
  pointLightsBuffer, numPointLights,
@@ -110,15 +124,37 @@ export function buildShadeKernel( params ) {
110
124
  emissiveBoost, enableEmissiveTriangleSampling,
111
125
  lightBVHNodeCount, reverseMapVec4Offset,
112
126
  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,
127
+ // Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Compiled in only when
128
+ // requested (PathTracer.requestOutput( 'gBuffer' )); then a live uniform (1 = denoiser on) skips the writes
129
+ // while nothing consumes them.
130
+ auxGBufferEnabled, gBuffer = true,
116
131
  hasSun, sunDirection, sunRadiance, sunParams,
132
+ // Bidirectional: light subpaths shade here too; camera vertices leave connections for ConnectKernel.
133
+ bidirectional: bdpt = null,
134
+ // The hit-distance output, when a stage asked for it (PathTracer.requestOutput).
135
+ hitDistanceEncode = null,
136
+ // The material layers some material uses (MaterialDataManager.materialLayers); the rest compile out. Bidirectional
137
+ // always leaves diffuse transmission out: its connections and light tracing do not cross surfaces yet.
138
+ materialLayers: layers = ALL_MATERIAL_LAYERS,
139
+ // Fewer shadow rays a hit, emitters included: null (one per light kind) or a lightPick variant; path tracing only.
140
+ shadowRays = null,
141
+ cameraWorldMatrix = null,
117
142
  } = params;
143
+ const diffuseTransmission = layers.diffuseTransmission;
118
144
 
119
- const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
145
+ // The bidirectional shading functions come with the integrator's uniforms (BidirectionalIntegrator), only used where bdpt is.
146
+ const {
147
+ 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,
148
+ } = bdpt?.lib ?? {};
149
+
150
+ const auxOn = gBuffer ? auxGBufferEnabled.greaterThan( uint( 0 ) ) : null;
120
151
 
121
152
  const useEmissiveNEE = lightBuffer !== undefined;
153
+ const picksLights = shadowRays && ! bdpt;
154
+ const lamps = {
155
+ directional: directionalLightsBuffer, numDirectional: numDirectionalLights, area: areaLightsBuffer, numArea: numAreaLights,
156
+ point: pointLightsBuffer, numPoint: numPointLights, spot: spotLightsBuffer, numSpot: numSpotLights,
157
+ };
122
158
 
123
159
  // Stochastic cone-jitter blur of an env backdrop lookup. Plain JS inliner (NOT a Fn — an rng Fn-param
124
160
  // would freeze; see TSL pitfalls) so it mutates the caller's rngState .toVar() directly. Shared by the
@@ -150,6 +186,29 @@ export function buildShadeKernel( params ) {
150
186
 
151
187
  };
152
188
 
189
+ // Emissive NEE's solid-angle density for a triangle's point, from whichever sampler it runs.
190
+ const emissiveNEEPdf = ( triangle, dist, toLight, from, instanceLeaf ) => {
191
+
192
+ const pdf = float( 0.0 ).toVar();
193
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
194
+
195
+ pdf.assign( calculateLightBVHPdf(
196
+ triangle, dist, toLight, from,
197
+ lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer, bvhBuffer,
198
+ ) );
199
+
200
+ } ).Else( () => {
201
+
202
+ pdf.assign( calculateEmissiveLightPdf(
203
+ triangle, dist, toLight, from,
204
+ triangleBuffer, materialBuffer, emissiveTotalPower, bvhBuffer, instanceLeaf,
205
+ ) );
206
+
207
+ } );
208
+ return pdf;
209
+
210
+ };
211
+
153
212
  const computeFn = Fn( () => {
154
213
 
155
214
  const threadIdx = instanceIndex;
@@ -186,6 +245,11 @@ export function buildShadeKernel( params ) {
186
245
 
187
246
  } );
188
247
 
248
+ // A light subpath gathers nothing; camera-only work is gated on it.
249
+ const isLight = bdpt ? flags.bitAnd( uint( RAY_FLAG.LIGHT_PATH ) ).notEqual( uint( 0 ) ).toVar() : null;
250
+ const cameraOnly = ( cond ) => ( bdpt ? cond.and( isLight.not() ) : cond );
251
+ const aux = auxOn ? cameraOnly( auxOn ) : null;
252
+
189
253
  // Backdrop-view = the ray still travels the original camera direction (only alpha/transparent passthrough
190
254
  // since the camera, REDIRECTED still clear). Captured at ARRIVAL (before the opaque/redirect bitOr below)
191
255
  // so it stays valid for both the miss branch and the emissive-hit scale. This — not bounceIndex==0 — is
@@ -200,6 +264,11 @@ export function buildShadeKernel( params ) {
200
264
  // One ray per pixel: rayID is the pixel index.
201
265
  const pixelIndex = rayID;
202
266
  const rngState = readRngState( hitBufferRW, rayID ).toVar();
267
+ const subpathState = bdpt ? readMisState( hitBufferRW, rayID ) : null;
268
+ const subpath = bdpt ? { dVCM: subpathState.dVCM.toVar(), dVC: subpathState.dVC.toVar(), vm: null } : null;
269
+ const persistRng = () => ( bdpt
270
+ ? writeRngMis( hitBufferRW, rayID, rngState, subpath.dVCM, subpath.dVC )
271
+ : writeRngState( hitBufferRW, rayID, rngState ) );
203
272
 
204
273
  // Samples key on the GLOBAL pixel. pixelIndex is the LOCAL path slot; the global pixel
205
274
  // = chunkRowBase·W + localSlot, so sequences stay aligned across row-band chunks
@@ -207,9 +276,10 @@ export function buildShadeKernel( params ) {
207
276
  // Hoisted here rather than at the BSDF draw because the ground catcher runs NEE far earlier.
208
277
  const _resX = int( resolution.x ).toVar();
209
278
  const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
279
+ const _pixelRow = float( _globalPixel.div( _resX ) ).add( 0.5 );
210
280
  const _pixelCoord = vec2(
211
281
  float( _globalPixel.mod( _resX ) ).add( 0.5 ),
212
- float( _globalPixel.div( _resX ) ).add( 0.5 ),
282
+ bdpt ? _pixelRow.add( select( isLight, float( LIGHT_PIXEL_ROW_OFFSET ), float( 0.0 ) ) ) : _pixelRow,
213
283
  );
214
284
  // This bounce's block of sampler dimensions — see the budget table in Random.js.
215
285
  const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
@@ -217,7 +287,7 @@ export function buildShadeKernel( params ) {
217
287
  // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
218
288
  // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
219
289
  // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
220
- const featCarry = readFeatureThroughput( rayBufferRW, rayID ).toVar();
290
+ const featCarry = aux ? readFeatureThroughput( rayBufferRW, rayID ).toVar() : null;
221
291
 
222
292
  const hitDist = readHitDistance( hitBufferRW, rayID ).toVar();
223
293
  const hitNormal = readHitNormal( hitBufferRW, rayID ).toVar();
@@ -234,14 +304,13 @@ export function buildShadeKernel( params ) {
234
304
  const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
235
305
  const transparentCount = readTransparentCount( rayBufferRW, rayID ).toVar();
236
306
 
237
- // NRD guide: first segment after the primary scatter, plus any alpha-skip run so a cutout hole
307
+ // Hit distance: first segment after the primary scatter, plus any alpha-skip run so a cutout hole
238
308
  // doesn't shorten it. Unconditional at depth 1, so the post-skip segment wins.
239
- If( auxOn.and( cameraDepth.equal( 1 ) ), () => {
309
+ if ( hitDistanceEncode && aux ) If( aux.and( cameraDepth.equal( 1 ) ), () => {
240
310
 
241
311
  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
312
  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 ) );
313
+ writeGBufferHitDist( gBufferRW, pixelIndex, hitDistanceEncode( total, scatterViewZ ) );
245
314
 
246
315
  } );
247
316
 
@@ -252,7 +321,7 @@ export function buildShadeKernel( params ) {
252
321
  // ratio in alpha (rgb = 0) so it composites as bg·ratio over a transparent background.
253
322
  // Shadows are caught by the EXISTING NEE shadow ray into real geometry — the plane itself
254
323
  // never enters the BVH. Secondary bounces ignore it entirely.
255
- If( enableGroundCatcher.and( bounceIndex.equal( 0 ) ), () => {
324
+ If( cameraOnly( enableGroundCatcher.and( bounceIndex.equal( 0 ) ) ), () => {
256
325
 
257
326
  const dirY = direction.y.toVar();
258
327
  If( dirY.abs().greaterThan( float( EPSILON ) ), () => {
@@ -286,7 +355,7 @@ export function buildShadeKernel( params ) {
286
355
  bvhBuffer, triangleBuffer, materialBuffer,
287
356
  envTexture, environmentIntensity, envMatrix,
288
357
  envCDFTexture,
289
- envTotalSum, envCompensationDelta, envResolution,
358
+ envTotalSum, envResolution,
290
359
  enableEnvironmentLight,
291
360
  tslBool( true ), // wantUnoccluded
292
361
  vec3( 0.0 ), planeN, float( 0.0 ),
@@ -351,7 +420,7 @@ export function buildShadeKernel( params ) {
351
420
  // The catcher is a real ground surface for the denoiser — write the plane's normal/depth
352
421
  // + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
353
422
  // valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
354
- If( auxOn, () => {
423
+ if ( aux ) If( aux, () => {
355
424
 
356
425
  const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
357
426
  writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
@@ -361,7 +430,7 @@ export function buildShadeKernel( params ) {
361
430
  writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
362
431
  // DDFA: the plane's aux (written above) is a committed surface — lock it (cosmetic; ray dies here).
363
432
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ).bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
364
- writeRngState( hitBufferRW, rayID, rngState );
433
+ persistRng();
365
434
  Return();
366
435
 
367
436
  } );
@@ -372,6 +441,13 @@ export function buildShadeKernel( params ) {
372
441
 
373
442
  If( hitDist.greaterThan( MISS_DIST ), () => {
374
443
 
444
+ if ( bdpt ) If( isLight, () => {
445
+
446
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
447
+ Return();
448
+
449
+ } );
450
+
375
451
  // Background and environment-lighting are decoupled (independent axes):
376
452
  // • Visible backdrop: a PRIMARY ray draws the env image only when showBackground — regardless
377
453
  // of enableEnvironmentLight (so you can show the HDRI without it lighting the scene).
@@ -381,12 +457,19 @@ export function buildShadeKernel( params ) {
381
457
  // is the direct backdrop", so env seen through alpha-cutout foliage holes is treated identically to
382
458
  // the open sky (blur, intensity, show/hide, color-mode, ground projection all match).
383
459
  // isBackdropView was captured at arrival (above) so it survives the REDIRECTED bitOr on opaque hits.
460
+ // A camera path out to the sky: where light from infinity gets in (LightGuide.js).
461
+ if ( bdpt ) If( isBackdropView.not().and( isLight.not() ), () => {
462
+
463
+ recordEscape( counters, bdpt, direction, origin );
464
+
465
+ } );
466
+
384
467
  const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
385
468
  const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
386
469
 
387
470
  // DDFA: env colour a redirected ray escaping to the environment sees (e.g. smooth glass over
388
471
  // sky). Committed (tinted by featCarry) into the aux G-buffer before the miss Return below.
389
- const escapedAuxAlbedo = vec3( 0.0 ).toVar();
472
+ const escapedAuxAlbedo = aux ? vec3( 0.0 ).toVar() : null;
390
473
 
391
474
  If( wantBackdrop.or( wantEnvLight ), () => {
392
475
 
@@ -428,19 +511,27 @@ export function buildShadeKernel( params ) {
428
511
 
429
512
  // DDFA: remember the (clamped) env colour this escaped ray sees, for the redirected-escape
430
513
  // aux commit below (tinted by the accumulated see-through throughput).
431
- escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
514
+ if ( aux ) escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
432
515
 
433
516
  // MIS weight for implicit env hit — prevents double-counting with NEE
434
517
  const envMisWeight = float( 1.0 ).toVar();
435
- If( isBackdropView.not(), () => {
518
+ if ( bdpt ) If( isBackdropView.not(), () => {
519
+
520
+ // Georgiev (43) for the environment at infinity, from the exact table its NEE and light paths draw from.
521
+ const pdfW = select( bdpt.envTable.greaterThan( uint( 0 ) ), environmentPdfExact( envCDFTexture, envMatrix, envResolution, direction ), float( 0.0 ) ).toVar();
522
+ const directPdfW = select( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ), pdfW, float( 0.0 ) );
523
+ const emissionPdfW = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
524
+ bdpt.envPick.mul( pdfW ).mul( guidedDiscPdf( bdpt, bdpt.guideTexture, direction, origin ) ), float( 0.0 ) );
525
+ envMisWeight.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
526
+ misWeight( float( 0.0 ), mis( directPdfW ).mul( subpath.dVCM ).add( mis( emissionPdfW ).mul( subpath.dVC ) ) ) ) );
527
+
528
+ } );
529
+ else If( isBackdropView.not(), () => {
436
530
 
437
531
  const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
438
- If( prevBouncePdf.greaterThan( 0.0 ), () => {
532
+ If( prevBouncePdf.greaterThan( 0.0 ).and( envTotalSum.greaterThan( 0.0 ) ), () => {
439
533
 
440
- const envEval = sampleEquirect(
441
- envTexture, direction, envMatrix, envTotalSum, envCompensationDelta, envResolution,
442
- );
443
- const envPdf = envEval.w;
534
+ const envPdf = environmentPdfExact( envCDFTexture, envMatrix, envResolution, direction ).toVar();
444
535
  If( envPdf.greaterThan( 0.0 ), () => {
445
536
 
446
537
  envMisWeight.assign( balanceHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) ); // megakernel parity (PathTracerCore.js:774): env NEE also uses balance
@@ -498,11 +589,29 @@ export function buildShadeKernel( params ) {
498
589
  If( sunL.x.add( sunL.y ).add( sunL.z ).greaterThan( 0.0 ), () => {
499
590
 
500
591
  const sunW = float( 1.0 ).toVar();
501
- If( isBackdropView.not().and( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ) ), () => {
592
+ if ( bdpt ) {
502
593
 
503
- sunW.assign( powerHeuristic( { pdf1: readRayPdf( rayBufferRW, rayID ), pdf2: float( 1.0 ).div( sunParams.y ) } ) );
594
+ // Georgiev (43) for a light at infinity: no distance, and NEE's density is solid angle.
595
+ If( isBackdropView.not(), () => {
596
+
597
+ const directPdfW = select( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ), float( 1.0 ).div( sunParams.y ), float( 0.0 ) );
598
+ const emissionPdfW = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
599
+ sunEmissionPdf( bdpt, sunParams, guidedDiscPdf( bdpt, bdpt.guideTexture, direction, origin ) ), float( 0.0 ) );
600
+ sunW.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
601
+ misWeight( float( 0.0 ), mis( directPdfW ).mul( subpath.dVCM ).add( mis( emissionPdfW ).mul( subpath.dVC ) ) ) ) );
602
+
603
+ } );
604
+
605
+ } else {
606
+
607
+ If( isBackdropView.not().and( flags.bitAnd( uint( RAY_FLAG.SUN_NEE ) ).notEqual( uint( 0 ) ) ), () => {
608
+
609
+ sunW.assign( powerHeuristic( { pdf1: readRayPdf( rayBufferRW, rayID ), pdf2: float( 1.0 ).div( sunParams.y ) } ) );
610
+
611
+ } );
612
+
613
+ }
504
614
 
505
- } );
506
615
  const vertex = max( bounceIndex.sub( 1 ), int( 0 ) );
507
616
  const sunScale = select( isBackdropView, backgroundIntensity,
508
617
  sunW.mul( select( vertex.greaterThan( int( 0 ) ), globalIlluminationIntensity, float( 1.0 ) ) ) );
@@ -553,7 +662,7 @@ export function buildShadeKernel( params ) {
553
662
  // per pixel → OIDN sees structure tracking the refracted view); depth kept at the primary hit.
554
663
  // AUX_LOCKED-clear alone means "never committed" — the old black-probe is redundant. A direct
555
664
  // backdrop (REDIRECTED clear) keeps Generate's black/far default (regression-safe sky guide).
556
- If( auxOn
665
+ if ( aux ) If( aux
557
666
  .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
558
667
  .and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
559
668
 
@@ -568,8 +677,110 @@ export function buildShadeKernel( params ) {
568
677
 
569
678
  } );
570
679
 
680
+ // On the triangle's plane: Extend stores the distance to it (ExtendKernel).
571
681
  const hitPoint = origin.add( direction.mul( hitDist ) ).toVar();
682
+ // Vertex merging: a merge here, weighed in every sum this vertex ends or scatters.
683
+ if ( bdpt?.merging ) subpath.vm = mergeVmAt( bdpt, hitPoint ).toVar();
572
684
  const N = normalize( hitNormal ).toVar();
685
+ // The hit record's facet is 11-bit; light tracing's and connections' grazing geometry terms need it exact.
686
+ const exactFacetN = bdpt ? hitFacet( {
687
+ triangleBuffer, bvhBuffer, triIdx: int( hitTriIdx ), instanceLeaf: hitInstance, hitPoint,
688
+ smoothNormal: N, viewDir: direction.negate(), didHit: tslBool( true ), liftEnabled: tslBool( false ),
689
+ } ).faceN : null;
690
+
691
+ // Where a lamp's light path first lands: its falloff and a directional gobo apply here.
692
+ const lampLanding = bdpt ? { source: int( - 1 ).toVar(), dist: float( 0.0 ).toVar() } : null;
693
+ const landLampPath = ( dist ) => {
694
+
695
+ const start = readLightOrigin( hitBufferRW, rayID );
696
+ const source = int( - 1 ).sub( start.triangle ).toVar();
697
+ If( start.triangle.lessThan( int( 0 ) ).and( source.greaterThanEqual( int( SOURCE.LAMPS ) ) ), () => {
698
+
699
+ const type = sourceLampType( bdpt, source ).toVar();
700
+ const index = sourceLampIndex( bdpt, source, type ).toVar();
701
+ If( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
702
+
703
+ throughput.mulAssign( sampleDirectionalGoboMask( DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, index ) ), hitPoint ) );
704
+
705
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_POINT ) ), () => {
706
+
707
+ const light = PointLight.wrap( getPointLight( pointLightsBuffer, index ) );
708
+ throughput.mulAssign( getDistanceAttenuation( { lightDistance: dist, cutoffDistance: light.distance, decayExponent: light.decay } ).mul( dist.mul( dist ) ) );
709
+
710
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_SPOT ) ), () => {
711
+
712
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, index ) );
713
+ throughput.mulAssign( getDistanceAttenuation( { lightDistance: dist, cutoffDistance: light.distance, decayExponent: light.decay } ).mul( dist.mul( dist ) ) );
714
+
715
+ } );
716
+ lampLanding.source.assign( source );
717
+ lampLanding.dist.assign( dist );
718
+
719
+ } );
720
+
721
+ };
722
+
723
+ // Georgiev (43) for the rect light a continuation reaches. Lamps are not geometry, so the new ray is tested
724
+ // here, after any scatter, with glass blocking as it does NEE's. `nearPick` gives NEE's pick of it here.
725
+ const bidirectionalAreaHit = ( from, dir, nearPick ) => {
726
+
727
+ const nearestT = float( 1e30 ).toVar();
728
+ const nearest = int( - 1 ).toVar();
729
+ Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
730
+
731
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
732
+ If( light.intensity.greaterThan( 0.0 ), () => {
733
+
734
+ const t = intersectAreaLight( light, hitPoint, dir ).toVar();
735
+ If( t.greaterThan( 0.0 ).and( t.lessThan( nearestT ) ), () => {
736
+
737
+ nearestT.assign( t );
738
+ nearest.assign( i );
739
+
740
+ } );
741
+
742
+ } );
743
+
744
+ } );
745
+
746
+ If( nearest.greaterThanEqual( int( 0 ) ), () => {
747
+
748
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearest ) );
749
+ const cosLight = dot( dir, light.normal ).negate().toVar();
750
+ const toLight = hitPoint.add( dir.mul( nearestT ) ).sub( from ).toVar();
751
+ const len = length( toLight ).toVar();
752
+ const visibility = traceShadowRayRefractiveOpaque(
753
+ from, toLight.div( len ), len.mul( SHADOW_END ), traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
754
+ ).toVar();
755
+
756
+ If( visibility.greaterThan( 0.0 ).and( cosLight.greaterThan( 0.0 ) ), () => {
757
+
758
+ const arrived = { dVCM: subpath.dVCM.toVar(), dVC: subpath.dVC.toVar() };
759
+ misOnHit( arrived, nearestT, cosLight );
760
+ const neePdfA = nearPick
761
+ ? nearPick( light ).mul( areaLightDirectPdfW( light, hitPoint, nearestT, cosLight ) ).mul( cosLight ).div( max( nearestT.mul( nearestT ), 1e-30 ) )
762
+ : float( 0.0 );
763
+ const emissionPdf = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
764
+ sourcePick( bdpt, lampSource( bdpt, int( LIGHT_TYPE_AREA ), nearest ) ).div( max( light.area, 1e-20 ) ).mul( cosLight ).div( Math.PI ), float( 0.0 ) );
765
+ const weight = strategyWeight( bdpt.strategyView, STRATEGY.HIT,
766
+ misWeight( float( 0.0 ), mis( neePdfA ).mul( arrived.dVCM ).add( mis( emissionPdf ).mul( arrived.dVC ) ) ) );
767
+
768
+ currentRadiance.assign( vec4(
769
+ currentRadiance.xyz.add(
770
+ regularizePathContribution(
771
+ areaLightRadiance( light ).mul( areaLightSpreadAttenuation( cosLight, light.spread ) ).mul( throughput ).mul( visibility ).mul( weight )
772
+ .mul( select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) ) ),
773
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
774
+ ),
775
+ ),
776
+ currentRadiance.w,
777
+ ) );
778
+
779
+ } );
780
+
781
+ } );
782
+
783
+ };
573
784
 
574
785
  // medium stack read once here; reused by the transparency block below
575
786
  const medStack = readMediumStack( rayBufferRW, rayID );
@@ -585,23 +796,33 @@ export function buildShadeKernel( params ) {
585
796
  If( mediumStackDepth.greaterThan( 0 ), () => {
586
797
 
587
798
  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 ), () => {
799
+ const absorb = () => {
593
800
 
594
801
  // glass: Beer-Lambert absorption
595
802
  const beer = exp( mSigmaA.mul( hitDist ).negate() ).toVar();
596
803
  throughput.mulAssign( beer );
597
804
  // 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 ) ) ), () => {
805
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
599
806
 
600
807
  featCarry.mulAssign( beer );
601
808
 
602
809
  } );
603
810
 
604
- } ).Else( () => {
811
+ };
812
+
813
+ // Without subsurface materials every medium is glass (scattering 0).
814
+ if ( ! layers.subsurface ) {
815
+
816
+ absorb();
817
+ return;
818
+
819
+ }
820
+
821
+ const sssMed = readSSSMedium( rayBufferRW, rayID );
822
+ const mSigmaS = sssMed.sigmaS.toVar();
823
+ const mG = sssMed.g.toVar();
824
+
825
+ If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), absorb ).Else( () => {
605
826
 
606
827
  // subsurface: chromatic collision-distance sampling
607
828
  const mSigmaT = mSigmaA.add( mSigmaS );
@@ -628,7 +849,7 @@ export function buildShadeKernel( params ) {
628
849
 
629
850
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
630
851
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
631
- writeRngState( hitBufferRW, rayID, rngState );
852
+ persistRng();
632
853
  Return();
633
854
 
634
855
  } );
@@ -645,7 +866,7 @@ export function buildShadeKernel( params ) {
645
866
  // fires the next hit's env/emissive MIS, down-weighting SSS-then-env/emitter views.
646
867
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
647
868
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
648
- writeRngState( hitBufferRW, rayID, rngState );
869
+ persistRng();
649
870
  Return();
650
871
 
651
872
  } );
@@ -659,6 +880,33 @@ export function buildShadeKernel( params ) {
659
880
  const material = RayTracingMaterial.wrap(
660
881
  getMaterial( int( hitMatIdx ), materialBuffer )
661
882
  ).toVar();
883
+ if ( ! diffuseTransmission || bdpt ) {
884
+
885
+ material.diffuseTransmission.assign( 0.0 );
886
+ material.diffuseTransmissionMapIndex.assign( int( - 1 ) );
887
+ material.diffuseTransmissionColorMapIndex.assign( int( - 1 ) );
888
+
889
+ }
890
+
891
+ // A layer no material has is 0 in every material; saying so lets the compiler fold what still reads it.
892
+ for ( const layer of [ 'clearcoat', 'sheen', 'iridescence', 'anisotropy', 'subsurface', 'dispersion' ] ) {
893
+
894
+ if ( ! layers[ layer ] ) material[ layer ].assign( 0.0 );
895
+
896
+ }
897
+
898
+ // The segment past the last bounce is traced only for the light it hits: an opaque surface that does not glow
899
+ // ends it before any texture is read (emission is factor × map, so a zero factor is zero).
900
+ If( flags.bitAnd( uint( RAY_FLAG.EMISSION_ONLY ) ).notEqual( uint( 0 ) )
901
+ .and( material.alphaMode.equal( int( 0 ) ) ).and( material.transmission.lessThanEqual( 0.0 ) ).and( material.subsurface.lessThanEqual( 0.0 ) )
902
+ .and( max( max( material.emissive.x, material.emissive.y ), material.emissive.z ).mul( material.emissiveIntensity ).lessThanEqual( 0.0 ) ), () => {
903
+
904
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
905
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
906
+ persistRng();
907
+ Return();
908
+
909
+ } );
662
910
 
663
911
  // displacement: analytical ray-height marching refines hitPoint/UV/normal; no-op without a map
664
912
  const samplingUV = hitUV.toVar();
@@ -715,7 +963,7 @@ export function buildShadeKernel( params ) {
715
963
  const extUV = getTransformedUV( { uv: samplingUV, transform: material.albedoTransform } ).toVar();
716
964
 
717
965
  // Fold the anisotropy texture (if any) into the scalar anisotropy/rotation used by the BRDF
718
- If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
966
+ if ( layers.anisotropy ) If( material.anisotropyMapIndex.greaterThanEqual( int( 0 ) ), () => {
719
967
 
720
968
  const aniso = processAnisotropyMap( material, extUV ).toVar();
721
969
  material.anisotropy.assign( aniso.x );
@@ -736,6 +984,8 @@ export function buildShadeKernel( params ) {
736
984
  material.iridescenceThicknessRange.assign( vec2( material.iridescenceThicknessRange.x, extMaps.iridescenceThickness ) );
737
985
  material.specularIntensity.assign( extMaps.specularIntensity );
738
986
  material.specularColor.assign( extMaps.specularColor );
987
+ material.diffuseTransmission.assign( extMaps.diffuseTransmission );
988
+ material.diffuseTransmissionColor.assign( extMaps.diffuseTransmissionColor );
739
989
 
740
990
  // Below the floor the GGX peak is past f32, so a plain reflector reflects exactly: roughness 0
741
991
  // is the delta-lobe marker the sampler and evaluator read. The other lobes keep the floor.
@@ -769,19 +1019,19 @@ export function buildShadeKernel( params ) {
769
1019
  const metal = material.metalness.toVar();
770
1020
  const trans = material.transmission.toVar();
771
1021
  const roughFrac = smoothstep( float( 0.0 ), float( 0.15 ), rawRough ).toVar();
772
- const auxCommit = clamp(
1022
+ const auxCommit = aux ? clamp(
773
1023
  float( 1.0 ).sub( metal ).mul( float( 1.0 ).sub( trans ) )
774
1024
  .add( roughFrac.mul( metal.mul( float( 1.0 ).sub( trans ) ).add( trans ) ) ),
775
1025
  0.0, 1.0,
776
- ).greaterThanEqual( float( 0.25 ) ).toVar();
777
- const featPrefix = featCarry.toVar();
1026
+ ).greaterThanEqual( float( 0.25 ) ).toVar() : null;
1027
+ const featPrefix = aux ? featCarry.toVar() : null;
778
1028
 
779
1029
  // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
780
1030
  // surface instead of leaving a black guide OIDN would demod-amplify.
781
1031
  // Plain JS inliner, not an Fn — it closes over albedo/featPrefix/flags .toVar()s.
782
1032
  const commitDeferredAux = ( normal ) => {
783
1033
 
784
- If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1034
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
785
1035
 
786
1036
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
787
1037
  writeGBuffer( gBufferRW, pixelIndex, normal, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
@@ -795,14 +1045,14 @@ export function buildShadeKernel( params ) {
795
1045
 
796
1046
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
797
1047
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
798
- writeRngState( hitBufferRW, rayID, rngState );
1048
+ persistRng();
799
1049
 
800
1050
  };
801
1051
 
802
1052
  // first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
803
1053
  // The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
804
1054
  // 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 ), () => {
1055
+ if ( aux ) If( bounceIndex.equal( 0 ).and( aux ), () => {
806
1056
 
807
1057
  const linearDepth = computeNDCDepth( {
808
1058
  worldPos: hitPoint,
@@ -847,14 +1097,14 @@ export function buildShadeKernel( params ) {
847
1097
  // ─── DDFA aux decision at a transmissive / alpha / SSS interaction. Runs for BOTH the continuing and
848
1098
  // the BLEND fall-through paths, so it sits before If(continueRay). If/ElseIf chaining so BLEND wins
849
1099
  // 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 ) ) ), () => {
1100
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
851
1101
 
852
1102
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
853
1103
  // N here is the mapped normal — faceforward it (a ray exiting glass sees a back-facing N).
854
1104
  const Nff = select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ).toVar();
855
1105
  const auxAlbedo = clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
856
1106
 
857
- If( material.alphaMode.equal( int( 2 ) ), () => {
1107
+ let auxCases = If( material.alphaMode.equal( int( 2 ) ), () => {
858
1108
 
859
1109
  // BLEND: deterministic own-surface commit on BOTH stochastic skip + shade frames (no flicker).
860
1110
  writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
@@ -864,13 +1114,15 @@ export function buildShadeKernel( params ) {
864
1114
 
865
1115
  // MASK cutout passthrough: keep deferring, tint unchanged (backdrop through the hole commits later).
866
1116
 
867
- } ).ElseIf( interaction.isSubsurface, () => {
1117
+ } );
1118
+ if ( layers.subsurface ) auxCases = auxCases.ElseIf( interaction.isSubsurface, () => {
868
1119
 
869
1120
  // SSS boundary: force-commit the skin surface as diffuse.
870
1121
  writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
871
1122
  flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
872
1123
 
873
- } ).ElseIf( interaction.isTransmissive, () => {
1124
+ } );
1125
+ auxCases.ElseIf( interaction.isTransmissive, () => {
874
1126
 
875
1127
  If( auxCommit.or( material.dispersion.greaterThan( 0.0 ) ), () => {
876
1128
 
@@ -941,7 +1193,7 @@ export function buildShadeKernel( params ) {
941
1193
  } );
942
1194
 
943
1195
  // subsurface boundary: push the scattering medium on enter, pop on exit; free bounce
944
- If( interaction.isSubsurface.and( interaction.didReflect.not() ), () => {
1196
+ if ( layers.subsurface ) If( interaction.isSubsurface.and( interaction.didReflect.not() ), () => {
945
1197
 
946
1198
  If( interaction.entering, () => {
947
1199
 
@@ -994,6 +1246,15 @@ export function buildShadeKernel( params ) {
994
1246
 
995
1247
  throughput.mulAssign( interaction.throughput );
996
1248
 
1249
+ // Importance, unlike radiance, is not rescaled by refraction.
1250
+ if ( bdpt ) If( isLight.and( interaction.isTransmissive.or( interaction.isSubsurface ) ).and( interaction.didReflect.not() ), () => {
1251
+
1252
+ const n1 = select( interaction.entering, currentMediumIOR, material.ior );
1253
+ const n2 = select( interaction.entering, material.ior, previousMediumIOR );
1254
+ throughput.mulAssign( n2.mul( n2 ).div( max( n1.mul( n1 ), EPSILON ) ) );
1255
+
1256
+ } );
1257
+
997
1258
  // Off the side of the facet the new ray leaves on: reflection stays, transmission and alpha skip cross.
998
1259
  const Ng = unpackHitFacet( readHitFacet( hitBufferRW, rayID ) ).faceN;
999
1260
  const crossesFacet = dot( Ng, interaction.direction ).lessThan( 0.0 ).toVar();
@@ -1010,6 +1271,25 @@ export function buildShadeKernel( params ) {
1010
1271
  flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.UNDER_SURFACE ) ) ).bitOr( uint( RAY_FLAG.REDIRECTED ) )
1011
1272
  .bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
1012
1273
 
1274
+ // Not connectible; an alpha skip is no vertex at all.
1275
+ if ( bdpt ) {
1276
+
1277
+ misOnHit( subpath, hitDist.add( readMisRayT( rayBufferRW, rayID ) ), abs( dot( exactFacetN, direction ) ) );
1278
+ misOnSpecular( subpath, abs( dot( exactFacetN, interaction.direction ) ) );
1279
+ If( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ).notEqual( uint( 0 ) ), () => {
1280
+
1281
+ landLampPath( hitDist.add( readMisRayT( rayBufferRW, rayID ) ) );
1282
+
1283
+ } );
1284
+ flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ) ) );
1285
+ If( isLight.not().and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
1286
+
1287
+ bidirectionalAreaHit( newOrigin, interaction.direction, null );
1288
+
1289
+ } );
1290
+
1291
+ }
1292
+
1013
1293
  } ).Else( () => {
1014
1294
 
1015
1295
  // Alpha passthrough: charge the guard. N is faceforwarded for the aux commit (a ray
@@ -1037,12 +1317,12 @@ export function buildShadeKernel( params ) {
1037
1317
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1038
1318
  // DDFA: persist the (possibly tinted) see-through throughput for the next bounce. MUST run after
1039
1319
  // 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 ) ) ), () => {
1320
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1041
1321
 
1042
1322
  writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1043
1323
 
1044
1324
  } );
1045
- writeRngState( hitBufferRW, rayID, rngState );
1325
+ persistRng();
1046
1326
  Return();
1047
1327
 
1048
1328
  } );
@@ -1055,18 +1335,59 @@ export function buildShadeKernel( params ) {
1055
1335
  // so any later env-escape is redirected light, not the direct backdrop). Single positive bitOr.
1056
1336
  flags.assign( flags.bitOr( uint( RAY_FLAG.HAS_HIT_OPAQUE | RAY_FLAG.REDIRECTED ) ) );
1057
1337
 
1338
+ if ( bdpt ) {
1339
+
1340
+ const arrivalDist = hitDist.add( readMisRayT( rayBufferRW, rayID ) ).toVar();
1341
+
1342
+ // A light path's NEE density needs its first vertex.
1343
+ If( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ).notEqual( uint( 0 ) ), () => {
1344
+
1345
+ const start = readLightOrigin( hitBufferRW, rayID );
1346
+ const neePdfA = float( 0.0 ).toVar();
1347
+ If( start.triangle.lessThan( int( 0 ) ), () => {
1348
+
1349
+ // A light at infinity has no distance to square. The sun's NEE density is in already; a lamp's
1350
+ // depends on this vertex's material, below.
1351
+ const source = int( - 1 ).sub( start.triangle );
1352
+ neePdfA.assign( select( source.lessThan( int( SOURCE.LAMPS ) ).or( sourceLampType( bdpt, source ).equal( int( LIGHT_TYPE_DIRECTIONAL ) ) ),
1353
+ float( 1.0 ).div( max( arrivalDist.mul( arrivalDist ), 1e-30 ) ), float( 1.0 ) ) );
1354
+ landLampPath( arrivalDist );
1355
+
1356
+ } ).Else( () => {
1357
+
1358
+ // NEE draws a triangle on every side it emits from, as the light path left it.
1359
+ if ( useEmissiveNEE ) If( enableEmissiveTriangleSampling.equal( int( 1 ) )
1360
+ .and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1361
+
1362
+ neePdfA.assign( emissiveNEEPdf( start.triangle, arrivalDist, direction.negate(), hitPoint, start.instanceLeaf )
1363
+ .mul( abs( start.cosLight ) ).div( arrivalDist.mul( arrivalDist ) ) );
1364
+
1365
+ } );
1366
+
1367
+ } );
1368
+ subpath.dVCM.mulAssign( mis( neePdfA ) );
1369
+ flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.LIGHT_EMITTED ) ) ) );
1370
+
1371
+ } );
1372
+
1373
+ misOnHit( subpath, arrivalDist, abs( dot( exactFacetN, direction ) ) );
1374
+
1375
+ }
1376
+
1058
1377
  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 ) ), () => {
1378
+ // NEE samples only a single-sided emitter's lit side, so from behind (bounce rays) it is a dark occluder. The side
1379
+ // is the facet's, as NEE's: the interpolated normal turns away near a coarse mesh's silhouette.
1380
+ If( cameraOnly( isBackdropView.not().and( length( emissive ).greaterThan( 0.0 ) ) ), () => {
1061
1381
 
1062
- If( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ), dot( direction, hitNormal ) ).not(), () => {
1382
+ If( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ),
1383
+ dot( direction, windingNormal( triangleBuffer, bvhBuffer, int( hitTriIdx ), hitInstance ) ) ).not(), () => {
1063
1384
 
1064
1385
  emissive.assign( vec3( 0.0 ) );
1065
1386
 
1066
1387
  } );
1067
1388
 
1068
1389
  } );
1069
- If( length( emissive ).greaterThan( 0.0 ), () => {
1390
+ If( cameraOnly( length( emissive ).greaterThan( 0.0 ) ), () => {
1070
1391
 
1071
1392
  // Key on backdrop-view (not bounceIndex>0) so an emitter seen DIRECTLY through an alpha-cutout hole
1072
1393
  // renders at full intensity (1.0) like a direct view, consistent with env-through-hole — instead of
@@ -1078,7 +1399,34 @@ export function buildShadeKernel( params ) {
1078
1399
  // two estimators. Without it emissive geometry / area lights double-count (~2x bright + noisier).
1079
1400
  // Primary hits keep weight 1.0 (the wavefront's bounce-0 stored pdf is the Generate init, not a NEE pdf).
1080
1401
  const emissiveMISWeight = float( 1.0 ).toVar();
1081
- if ( useEmissiveNEE ) {
1402
+ if ( bdpt ) {
1403
+
1404
+ // Georgiev (43): NEE from the previous vertex, and every light subpath strategy.
1405
+ If( isBackdropView.not(), () => {
1406
+
1407
+ const misT = readMisRayT( rayBufferRW, rayID ).toVar();
1408
+ const misDist = hitDist.add( misT ).toVar();
1409
+ const tri = TriangleData.wrap( fetchTriangleData( int( hitTriIdx ), triangleBuffer, bvhBuffer, hitInstance ) );
1410
+ const cosToward = dot( direction.negate(), normalize( cross( tri.v1.sub( tri.v0 ), tri.v2.sub( tri.v0 ) ) ) ).toVar();
1411
+ const areaPdf = emitterAreaPdf( material.emissive.mul( material.emissiveIntensity ), emissiveTotalPower ).toVar();
1412
+ const emissionPdf = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1413
+ areaPdf.mul( float( 1.0 ).sub( bdpt.sunPick ) ).mul( emitterSideProbability( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ), cosToward ) )
1414
+ .mul( abs( cosToward ) ).div( Math.PI ),
1415
+ float( 0.0 ) );
1416
+ const neePdfA = float( 0.0 ).toVar();
1417
+ if ( useEmissiveNEE ) If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
1418
+ .and( areaPdf.greaterThan( 0.0 ) ), () => {
1419
+
1420
+ neePdfA.assign( emissiveNEEPdf( int( hitTriIdx ), misDist, direction, origin.sub( direction.mul( misT ) ), hitInstance )
1421
+ .mul( abs( cosToward ) ).div( misDist.mul( misDist ) ) );
1422
+
1423
+ } );
1424
+ emissiveMISWeight.assign( strategyWeight( bdpt.strategyView, STRATEGY.HIT,
1425
+ misWeight( float( 0.0 ), mis( neePdfA ).mul( subpath.dVCM ).add( mis( emissionPdf ).mul( subpath.dVC ) ) ) ) );
1426
+
1427
+ } );
1428
+
1429
+ } else if ( useEmissiveNEE ) {
1082
1430
 
1083
1431
  If( enableEmissiveTriangleSampling.equal( int( 1 ) )
1084
1432
  .and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
@@ -1133,6 +1481,13 @@ export function buildShadeKernel( params ) {
1133
1481
 
1134
1482
  } );
1135
1483
 
1484
+ If( flags.bitAnd( uint( RAY_FLAG.EMISSION_ONLY ) ).notEqual( uint( 0 ) ), () => {
1485
+
1486
+ terminatePath();
1487
+ Return();
1488
+
1489
+ } );
1490
+
1136
1491
  // BRDF sample (needed by both direct + indirect)
1137
1492
  const V = direction.negate().toVar();
1138
1493
  // face-forwarded geometric normal: horizon guard for NEE and the bounce continuation
@@ -1146,6 +1501,13 @@ export function buildShadeKernel( params ) {
1146
1501
  hitPoint, offsetNormal: facetN, lift: terminatorLift, faceN: facetN, smoothN: NgeoFF, L,
1147
1502
  cutoff: shadowTerminatorOffset,
1148
1503
  } );
1504
+ // Light from behind a surface that passes light through diffusely: its rays leave from the other side.
1505
+ // Compiled in only with the lobe (see `diffuseTransmission`).
1506
+ const transmitting = diffuseTransmission && ! bdpt;
1507
+ const transmitsDiffusely = transmitting ? material.diffuseTransmission.greaterThan( 0.0 ) : tslBool( false );
1508
+ const throughSurface = L => ( transmitting ? transmitsDiffusely.and( dot( N, L ).lessThan( 0.0 ) ).and( dot( L, NgeoFF ).lessThan( 0.0 ) ) : tslBool( false ) );
1509
+ const backOrigin = transmitting ? offsetRayOrigin( hitPoint, facetN.negate() ) : null;
1510
+ const originFor = ( L, front ) => ( transmitting ? select( throughSurface( L ), backOrigin, front ) : front );
1149
1511
 
1150
1512
  // Two-sided shading: opaque path only (transmissive/SSS already continued). Decide the flip on the
1151
1513
  // GEOMETRIC normal — an inward-normal / double-sided mesh (GLB/PBRT) faces away as a whole — so a
@@ -1164,7 +1526,7 @@ export function buildShadeKernel( params ) {
1164
1526
  // ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
1165
1527
  // mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
1166
1528
  // 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 ) ) ), () => {
1529
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1168
1530
 
1169
1531
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1170
1532
  If( auxCommit, () => {
@@ -1211,8 +1573,28 @@ export function buildShadeKernel( params ) {
1211
1573
  const brdfPdf = float( 0.0 ).toVar();
1212
1574
  const brdfIsTransmission = tslBool( false ).toVar();
1213
1575
  const brdfColorWeight = vec3( 1.0 ).toVar();
1576
+ const brdfIsDiffuseTransmission = tslBool( false ).toVar();
1577
+
1578
+ const sampleLobes = () => {
1579
+
1580
+ const bs = DirectionSample.wrap( generateSampledDirection(
1581
+ V, N, material, xi, lobeXi, rngState,
1582
+ _pixelCoord, resolution, frame, dimBase,
1583
+ mc,
1584
+ false, emptyWeights,
1585
+ false, emptyCache,
1586
+ ) );
1587
+ brdfDir.assign( bs.direction );
1588
+ brdfValue.assign( bs.value );
1589
+ brdfPdf.assign( bs.pdf );
1590
+ brdfIsTransmission.assign( bs.isTransmission );
1591
+ brdfColorWeight.assign( bs.colorWeight );
1592
+ brdfIsDiffuseTransmission.assign( bs.isDiffuseTransmission );
1593
+
1594
+ };
1214
1595
 
1215
- If( material.clearcoat.greaterThan( 0.0 ), () => {
1596
+ if ( ! layers.clearcoat ) sampleLobes();
1597
+ else If( material.clearcoat.greaterThan( 0.0 ), () => {
1216
1598
 
1217
1599
  const ccRay = Ray( { origin, direction } );
1218
1600
  const ccHit = HitInfo( {
@@ -1227,25 +1609,89 @@ export function buildShadeKernel( params ) {
1227
1609
  brdfDir.assign( ccResult.L );
1228
1610
  brdfValue.assign( ccResult.brdf );
1229
1611
  brdfPdf.assign( ccResult.pdf );
1612
+ brdfIsDiffuseTransmission.assign( ccResult.isDiffuseTransmission );
1230
1613
 
1231
- } ).Else( () => {
1614
+ } ).Else( sampleLobes );
1232
1615
 
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 );
1616
+ const emitterCandidate = () => {
1245
1617
 
1246
- } );
1618
+ const value = vec3( 0.0 ).toVar();
1619
+ const from = vec3( 0.0 ).toVar();
1620
+ const dir = vec3( 0.0, 1.0, 0.0 ).toVar();
1621
+ const dist = float( 0.0 ).toVar();
1622
+ if ( ! useEmissiveNEE ) return [ value, from, dir, dist ];
1623
+
1624
+ If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1625
+
1626
+ const position = vec3( 0.0 ).toVar();
1627
+ const direction = vec3( 0.0, 1.0, 0.0 ).toVar();
1628
+ const emission = vec3( 0.0 ).toVar();
1629
+ const pdf = float( 0.0 ).toVar();
1630
+ const valid = tslBool( false ).toVar();
1631
+ const take = ( sample ) => {
1632
+
1633
+ const e = EmissiveSample.wrap( sample );
1634
+ position.assign( e.position );
1635
+ direction.assign( e.direction );
1636
+ emission.assign( e.emission );
1637
+ pdf.assign( e.pdf );
1638
+ valid.assign( e.valid );
1639
+
1640
+ };
1641
+
1642
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
1643
+
1644
+ take( sampleLightBVHTriangle(
1645
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1646
+ lightBuffer, lightBuffer, emissiveVec4Offset, triangleBuffer, bvhBuffer, transmitsDiffusely,
1647
+ ) );
1648
+
1649
+ } ).Else( () => {
1650
+
1651
+ take( sampleEmissiveTriangle(
1652
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1653
+ lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, triangleBuffer, bvhBuffer, transmitsDiffusely,
1654
+ ) );
1655
+
1656
+ } );
1657
+
1658
+ If( valid.and( pdf.greaterThan( 0.0 ) ), () => {
1659
+
1660
+ const back = throughSurface( direction ).toVar();
1661
+ const NoL = transmitting ? abs( dot( N, direction ) ) : max( float( 0.0 ), dot( N, direction ) );
1662
+ const inFront = NoL.greaterThan( 0.0 ).and( dot( N, direction ).greaterThan( 0.0 ) )
1663
+ .and( dot( direction, NgeoFF ).greaterThan( 0.0 ) );
1664
+
1665
+ If( transmitting ? back.or( inFront ) : inFront, () => {
1247
1666
 
1248
- const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
1667
+ from.assign( originFor( direction, lightShadowOrigin( direction ) ) );
1668
+ const toSample = position.sub( from ).toVar();
1669
+ const span = length( toSample ).toVar();
1670
+ dir.assign( toSample.div( span ) );
1671
+ dist.assign( span.mul( SHADOW_END ) );
1672
+
1673
+ const bPdf = calculateMaterialPDF( V, direction, N, material );
1674
+ const misW = select( bPdf.greaterThan( 0.0 ), powerHeuristic( { pdf1: pdf, pdf2: bPdf } ), float( 1.0 ) );
1675
+ value.assign( emission.mul( evaluateMaterialResponse( V, direction, N, material ) ).mul( NoL )
1676
+ .div( pdf ).mul( emissiveBoost ).mul( misW ) );
1677
+
1678
+ } );
1679
+
1680
+ } );
1681
+
1682
+ } );
1683
+
1684
+ return [ value, from, dir, dist ];
1685
+
1686
+ };
1687
+
1688
+ const pickArgs = picksLights
1689
+ ? [ ...emitterCandidate(), counters, visibilityCell( hitPoint, NgeoFF, vec3( cameraWorldMatrix[ 3 ] ) ) ]
1690
+ : [];
1691
+ const directLightingFn = picksLights
1692
+ ? calculateDirectLightingPicked( transmitting, shadowRays )
1693
+ : ( transmitting ? calculateDirectLightingThroughSurfaces : calculateDirectLightingUnified );
1694
+ const directLighting = () => DirectLightingDual.wrap( directLightingFn(
1249
1695
  hitPoint, N, NgeoFF, material, V,
1250
1696
  brdfDir, brdfPdf, brdfValue,
1251
1697
  bounceIndex, rngState,
@@ -1257,18 +1703,21 @@ export function buildShadeKernel( params ) {
1257
1703
  bvhBuffer, triangleBuffer, materialBuffer,
1258
1704
  envTexture, environmentIntensity, envMatrix,
1259
1705
  envCDFTexture,
1260
- envTotalSum, envCompensationDelta, envResolution,
1706
+ envTotalSum, envResolution,
1261
1707
  enableEnvironmentLight,
1262
1708
  tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
1263
1709
  terminatorLift, facetN, shadowTerminatorOffset,
1264
1710
  hasSun, sunDirection, sunRadiance, sunParams,
1265
- ) ).shadowed.toVar();
1711
+ ...pickArgs,
1712
+ ) ).shadowed;
1713
+ // Bidirectional mode samples every light itself, below.
1714
+ const directLight = bdpt ? null : directLighting().toVar();
1266
1715
 
1267
1716
  const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
1268
1717
  // Per-term firefly suppression (megakernel parity: PathTracerCore.js:1164) — wrap the direct-light add
1269
1718
  // like every other contribution (env/emissive-hit/emissive-NEE). This replaces the cumulative catch-all
1270
1719
  // that re-suppressed already-wrapped terms + prior-bounce radiance — suppress(a+b) ≠ suppress(a)+suppress(b) (gap #13).
1271
- currentRadiance.assign( vec4(
1720
+ if ( ! bdpt ) currentRadiance.assign( vec4(
1272
1721
  currentRadiance.xyz.add(
1273
1722
  regularizePathContribution(
1274
1723
  throughput.mul( directLight ).mul( giScale ),
@@ -1278,8 +1727,319 @@ export function buildShadeKernel( params ) {
1278
1727
  currentRadiance.w
1279
1728
  ) );
1280
1729
 
1730
+ // Georgiev (44)-(45). Glass blocks the shadow ray: light through it arrives on the light subpaths.
1731
+ const bidirectionalEmissiveNEE = () => {
1732
+
1733
+ const position = vec3( 0.0 ).toVar();
1734
+ const lightDir = vec3( 0.0 ).toVar();
1735
+ const emission = vec3( 0.0 ).toVar();
1736
+ const lightPdf = float( 0.0 ).toVar();
1737
+ const valid = tslBool( false ).toVar();
1738
+ const lightTriangle = int( 0 ).toVar();
1739
+ const windingN = vec3( 0.0 ).toVar();
1740
+ const take = ( sample ) => {
1741
+
1742
+ const e = EmissiveSampleIndexed.wrap( sample );
1743
+ position.assign( e.position );
1744
+ lightDir.assign( e.direction );
1745
+ emission.assign( e.emission );
1746
+ lightPdf.assign( e.pdf );
1747
+ valid.assign( e.valid );
1748
+ lightTriangle.assign( e.triangleIndex );
1749
+ windingN.assign( e.faceNormal );
1750
+
1751
+ };
1752
+
1753
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
1754
+
1755
+ take( sampleLightBVHTriangleIndexed(
1756
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1757
+ lightBuffer, lightBuffer, emissiveVec4Offset, triangleBuffer, bvhBuffer, tslBool( false ),
1758
+ ) );
1759
+
1760
+ } ).Else( () => {
1761
+
1762
+ take( sampleEmissiveTriangleIndexed(
1763
+ hitPoint, N, rngState, _pixelCoord, resolution, frame, dimBase,
1764
+ lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, triangleBuffer, bvhBuffer, tslBool( false ),
1765
+ ) );
1766
+
1767
+ } );
1768
+
1769
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1770
+ const side = triangleSide( triangleRow( triangleBuffer, lightTriangle, 4 ).z ).toVar();
1771
+ If( valid.and( lightPdf.greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) )
1772
+ .and( sideAccepts( side, dot( lightDir, windingN ) ) ), () => {
1773
+
1774
+ const rayOrigin = lightShadowOrigin( lightDir ).toVar();
1775
+ const toSample = position.sub( rayOrigin ).toVar();
1776
+ const shadowDist = length( toSample ).toVar();
1777
+ const visibility = traceShadowRayRefractiveOpaque(
1778
+ rayOrigin, toSample.div( shadowDist ), shadowDist.mul( SHADOW_END ),
1779
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1780
+ ).toVar();
1781
+
1782
+ If( visibility.greaterThan( 0.0 ), () => {
1783
+
1784
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1785
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1786
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1787
+ // Emission density over NEE's, both seen from here; the light's cosine cancels.
1788
+ const emissionOverDirect = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1789
+ emitterAreaPdf( emission, emissiveTotalPower, float( 1.0 ).sub( bdpt.sunPick ) )
1790
+ .mul( emitterSideProbability( side, dot( lightDir.negate(), windingN ) ) )
1791
+ .mul( abs( dot( lightDir, exactFacetN ) ) ).div( lightPdf.mul( Math.PI ) ),
1792
+ float( 0.0 ) );
1793
+ const wLight = mis( forward.div( lightPdf ) );
1794
+ const wCamera = misPartial( emissionOverDirect, subpath, reverse );
1795
+
1796
+ currentRadiance.assign( vec4(
1797
+ currentRadiance.xyz.add(
1798
+ regularizePathContribution(
1799
+ emission.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility ).mul( emissiveBoost )
1800
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, misWeight( wLight, wCamera ) ) ).mul( throughput ).mul( giScale ),
1801
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1802
+ ),
1803
+ ),
1804
+ currentRadiance.w,
1805
+ ) );
1806
+
1807
+ } );
1808
+
1809
+ } );
1810
+
1811
+ };
1812
+
1813
+ // Georgiev (44)-(45) for the sun; as for emitters, glass blocks the shadow ray.
1814
+ const bidirectionalSunNEE = () => {
1815
+
1816
+ const lightDir = sampleSunDisc( sunDirection, sunParams, getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 9 ) ), rngState, resolution, frame ) ).toVar();
1817
+ const radiance = sunRadianceToward( lightDir, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ).toVar();
1818
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1819
+ If( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1820
+
1821
+ const visibility = traceShadowRayRefractiveOpaque(
1822
+ lightShadowOrigin( lightDir ), lightDir, float( 1e20 ),
1823
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1824
+ ).toVar();
1825
+
1826
+ If( visibility.greaterThan( 0.0 ), () => {
1827
+
1828
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1829
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1830
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1831
+ const emissionOverDirect = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1832
+ sunEmissionPdf( bdpt, sunParams, guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).mul( sunParams.y ).mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1833
+ const weight = misWeight( mis( forward.mul( sunParams.y ) ), misPartial( emissionOverDirect, subpath, reverse ) );
1834
+
1835
+ currentRadiance.assign( vec4(
1836
+ currentRadiance.xyz.add(
1837
+ regularizePathContribution(
1838
+ radiance.mul( brdfVal ).mul( NoL ).mul( sunParams.y ).mul( visibility )
1839
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, weight ) ).mul( throughput ).mul( giScale ),
1840
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1841
+ ),
1842
+ ),
1843
+ currentRadiance.w,
1844
+ ) );
1845
+
1846
+ } );
1847
+
1848
+ } );
1849
+
1850
+ };
1851
+
1852
+ if ( bdpt ) If( isLight.not().and( hasSun.greaterThan( int( 0 ) ) ).and( enableEnvironmentLight ), () => {
1853
+
1854
+ bidirectionalSunNEE();
1855
+
1856
+ } );
1857
+
1858
+ // Georgiev (44)-(45) for the environment, drawn exactly (environmentPdfExact); glass blocks it as it does the sun's.
1859
+ const bidirectionalEnvironmentNEE = () => {
1860
+
1861
+ const drawn = sampleEnvironmentExact( envCDFTexture, envMatrix, envResolution,
1862
+ getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ) );
1863
+ const lightDir = drawn.direction.toVar();
1864
+ const lightPdf = drawn.pdf.toVar();
1865
+ const radiance = sampleEnvironment( {
1866
+ tex: envTexture, samp: sampler( envTexture ), direction: lightDir, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight: float( 1.0 ),
1867
+ } ).xyz.toVar();
1868
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1869
+ If( lightPdf.greaterThan( 0.0 ).and( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) )
1870
+ .and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1871
+
1872
+ const visibility = traceShadowRayRefractiveOpaque(
1873
+ lightShadowOrigin( lightDir ), lightDir, float( 1e20 ), traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1874
+ ).toVar();
1875
+
1876
+ If( visibility.greaterThan( 0.0 ), () => {
1877
+
1878
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1879
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1880
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1881
+ const lead = select( bdpt.lightPaths.greaterThan( uint( 0 ) ),
1882
+ bdpt.envPick.mul( guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1883
+ const weight = misWeight( mis( forward.div( lightPdf ) ), misPartial( lead, subpath, reverse ) );
1884
+
1885
+ currentRadiance.assign( vec4(
1886
+ currentRadiance.xyz.add(
1887
+ regularizePathContribution(
1888
+ radiance.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility )
1889
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, weight ) ).mul( throughput ).mul( giScale ),
1890
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1891
+ ),
1892
+ ),
1893
+ currentRadiance.w,
1894
+ ) );
1895
+
1896
+ } );
1897
+
1898
+ } );
1899
+
1900
+ };
1901
+
1902
+ if ( bdpt ) If( isLight.not().and( enableEnvironmentLight ).and( bdpt.envTable.greaterThan( uint( 0 ) ) ), () => {
1903
+
1904
+ bidirectionalEnvironmentNEE();
1905
+
1906
+ } );
1907
+
1908
+ // The lamps' reservoir total here, for the BSDF-hit partner at the continuation.
1909
+ const lampTotal = bdpt ? float( 0.0 ).toVar() : null;
1910
+
1911
+ // Georgiev (44)-(45) for a lamp, picked as the path tracer picks it. A point, a spot or a directional light no
1912
+ // camera path can hit; a rect light's partner is the continuation's hit (bidirectionalAreaHit).
1913
+ const bidirectionalLampNEE = () => {
1914
+
1915
+ const lightRandom = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
1916
+ const pick = LampPick.wrap( pickLamp( lamps, hitPoint, N, material, lightRandom.x,
1917
+ ( dim ) => getRandomSample1D( _pixelCoord, int( 0 ), dim, rngState, resolution, frame ) ) ).toVar();
1918
+ lampTotal.assign( pick.total );
1919
+
1920
+ const valid = tslBool( false ).toVar();
1921
+ const lightDir = vec3( 0.0, 1.0, 0.0 ).toVar();
1922
+ const emission = vec3( 0.0 ).toVar();
1923
+ const dist = float( 0.0 ).toVar();
1924
+ const lightPdf = float( 0.0 ).toVar();
1925
+ // Emission density over NEE's, but for the cosine here.
1926
+ const emissionOverDirect = float( 0.0 ).toVar();
1927
+ const take = ( sample ) => {
1928
+
1929
+ const ls = LightSample.wrap( sample );
1930
+ valid.assign( ls.valid );
1931
+ lightDir.assign( ls.direction );
1932
+ emission.assign( ls.emission );
1933
+ dist.assign( ls.distance );
1934
+ lightPdf.assign( ls.pdf );
1935
+
1936
+ };
1937
+
1938
+ const pE = sourcePick( bdpt, lampSource( bdpt, pick.kind, pick.index ) ).toVar();
1939
+
1940
+ If( pick.kind.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
1941
+
1942
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, pick.index ) );
1943
+ take( sampleDirectionalLight( light, hitPoint, pick.pdf, lightRandom.y,
1944
+ getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 14 ) ), rngState, resolution, frame ) ) );
1945
+ emissionOverDirect.assign( pE.mul( guidedDiscPdf( bdpt, bdpt.guideTexture, lightDir, hitPoint ) ).div( max( pick.pdf, 1e-30 ) ) );
1946
+
1947
+ } ).ElseIf( pick.kind.equal( int( LIGHT_TYPE_AREA ) ), () => {
1948
+
1949
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, pick.index ) );
1950
+ take( sampleRectAreaLight( light, hitPoint,
1951
+ vec2( lightRandom.y, getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 15 ) ), rngState, resolution, frame ) ), pick.pdf ) );
1952
+ emissionOverDirect.assign( pE.div( max( light.area.mul( Math.PI ).mul( lightPdf ), 1e-30 ) ) );
1953
+
1954
+ } ).ElseIf( pick.kind.equal( int( LIGHT_TYPE_POINT ) ), () => {
1955
+
1956
+ take( samplePointLightWithAttenuation( PointLight.wrap( getPointLight( pointLightsBuffer, pick.index ) ), hitPoint, pick.pdf ) );
1957
+ emissionOverDirect.assign( pE.div( max( float( 4 * Math.PI ).mul( pick.pdf ).mul( dist ).mul( dist ), 1e-30 ) ) );
1958
+
1959
+ } ).Else( () => {
1960
+
1961
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, pick.index ) );
1962
+ take( sampleSpotLightWithRadius( light, hitPoint, pick.pdf ) );
1963
+ emissionOverDirect.assign( pE.div( max( spotConeSolidAngle( light ).mul( pick.pdf ).mul( dist ).mul( dist ), 1e-30 ) ) );
1964
+
1965
+ } );
1966
+
1967
+ const NoL = max( float( 0.0 ), dot( N, lightDir ) ).toVar();
1968
+ If( valid.and( lightPdf.greaterThan( 0.0 ) ).and( NoL.greaterThan( 0.0 ) ).and( dot( lightDir, NgeoFF ).greaterThan( 0.0 ) ), () => {
1969
+
1970
+ const atInfinity = pick.kind.equal( int( LIGHT_TYPE_DIRECTIONAL ) );
1971
+ const shadowOrigin = lightShadowOrigin( lightDir ).toVar();
1972
+ const toSample = hitPoint.add( lightDir.mul( dist ) ).sub( shadowOrigin ).toVar();
1973
+ const shadowDist = length( toSample ).toVar();
1974
+ const visibility = traceShadowRayRefractiveOpaque(
1975
+ shadowOrigin, select( atInfinity, lightDir, toSample.div( shadowDist ) ), select( atInfinity, float( 1e20 ), shadowDist.mul( SHADOW_END ) ),
1976
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
1977
+ ).toVar();
1978
+
1979
+ If( visibility.greaterThan( 0.0 ), () => {
1980
+
1981
+ const brdfVal = evaluateMaterialResponse( V, lightDir, N, material );
1982
+ const forward = calculateMaterialPDF( V, lightDir, N, material );
1983
+ const reverse = calculateMaterialPDF( lightDir, V, N, material );
1984
+ const lead = select( bdpt.lightPaths.greaterThan( uint( 0 ) ), emissionOverDirect.mul( abs( dot( lightDir, exactFacetN ) ) ), float( 0.0 ) );
1985
+ const wLight = select( pick.kind.equal( int( LIGHT_TYPE_AREA ) ), mis( forward.div( lightPdf ) ), float( 0.0 ) );
1986
+
1987
+ currentRadiance.assign( vec4(
1988
+ currentRadiance.xyz.add(
1989
+ regularizePathContribution(
1990
+ emission.mul( brdfVal ).mul( NoL ).div( lightPdf ).mul( visibility )
1991
+ .mul( strategyWeight( bdpt.strategyView, STRATEGY.NEE, misWeight( wLight, misPartial( lead, subpath, reverse ) ) ) ).mul( throughput ).mul( giScale ),
1992
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1993
+ ),
1994
+ ),
1995
+ currentRadiance.w,
1996
+ ) );
1997
+
1998
+ } );
1999
+
2000
+ } );
2001
+
2002
+ };
2003
+
2004
+ if ( bdpt ) If( isLight.not().and( lampCount( lamps ).greaterThan( int( 0 ) ) ), () => {
2005
+
2006
+ bidirectionalLampNEE();
2007
+
2008
+ } );
2009
+
2010
+ // NEE's density for a lamp's light path depends on this vertex's material.
2011
+ if ( bdpt ) If( isLight.and( lampLanding.source.greaterThanEqual( int( SOURCE.LAMPS ) ) ), () => {
2012
+
2013
+ const type = sourceLampType( bdpt, lampLanding.source ).toVar();
2014
+ const index = sourceLampIndex( bdpt, lampLanding.source, type ).toVar();
2015
+ const d = lampLanding.dist;
2016
+ const direct = lampPickPdf( lamps, type, index, hitPoint, N, material ).toVar();
2017
+ If( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
2018
+
2019
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, index ) );
2020
+ direct.divAssign( select( light.angle.greaterThan( 0.0 ), directionalConeSolidAngle( light ), float( 1.0 ) ) );
2021
+
2022
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_AREA ) ), () => {
2023
+
2024
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, index ) );
2025
+ const cosLight = dot( direction, light.normal ).toVar();
2026
+ direct.mulAssign( areaLightDirectPdfW( light, hitPoint, d, cosLight ).mul( cosLight ).div( max( d.mul( d ), 1e-30 ) ) );
2027
+
2028
+ } );
2029
+ subpath.dVCM.mulAssign( mis( direct ) );
2030
+
2031
+ } );
2032
+
1281
2033
  // emissive triangle NEE: light-BVH fast path when available, flat-CDF fallback otherwise
1282
- if ( useEmissiveNEE ) {
2034
+ if ( useEmissiveNEE && bdpt ) {
2035
+
2036
+ If( isLight.not().and( enableEmissiveTriangleSampling.equal( int( 1 ) ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
2037
+
2038
+ bidirectionalEmissiveNEE();
2039
+
2040
+ } );
2041
+
2042
+ } else if ( useEmissiveNEE && ! picksLights ) {
1283
2043
 
1284
2044
  If(
1285
2045
  enableEmissiveTriangleSampling.equal( int( 1 ) )
@@ -1306,18 +2066,22 @@ export function buildShadeKernel( params ) {
1306
2066
  lightBuffer,
1307
2067
  emissiveVec4Offset,
1308
2068
  triangleBuffer, bvhBuffer,
2069
+ transmitsDiffusely,
1309
2070
  ) );
1310
2071
 
1311
2072
  // No rough-diffuse secondary-bounce skip here: dropping NEE while the emissive-hit
1312
2073
  // MIS still down-weights BSDF hits deletes emitter energy from deep GI.
1313
2074
  If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1314
2075
 
1315
- const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
2076
+ const back = throughSurface( emissiveSample.direction ).toVar();
2077
+ const NoL = transmitting ? abs( dot( N, emissiveSample.direction ) ) : max( float( 0.0 ), dot( N, emissiveSample.direction ) );
2078
+ const inFront = NoL.greaterThan( 0.0 ).and( dot( N, emissiveSample.direction ).greaterThan( 0.0 ) )
2079
+ .and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) );
1316
2080
 
1317
- If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) ), () => {
2081
+ If( transmitting ? back.or( inFront ) : inFront, () => {
1318
2082
 
1319
2083
  // Aimed at the sampled point and stopped a relative hair short, as for area lights.
1320
- const rayOrigin = lightShadowOrigin( emissiveSample.direction ).toVar();
2084
+ const rayOrigin = originFor( emissiveSample.direction, lightShadowOrigin( emissiveSample.direction ) ).toVar();
1321
2085
  const toSample = emissiveSample.position.sub( rayOrigin ).toVar();
1322
2086
  const shadowDist = length( toSample ).toVar();
1323
2087
  const visibility = traceShadowRayWrapped(
@@ -1365,7 +2129,7 @@ export function buildShadeKernel( params ) {
1365
2129
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1366
2130
  triangleBuffer, bvhBuffer,
1367
2131
  traceShadowRayWrapped,
1368
- Fn( ( [ p, , L ] ) => lightShadowOrigin( L ).sub( p ) ),
2132
+ Fn( ( [ p, , L ] ) => originFor( L, lightShadowOrigin( L ) ).sub( p ) ),
1369
2133
  );
1370
2134
 
1371
2135
  currentRadiance.assign( vec4(
@@ -1389,10 +2153,46 @@ export function buildShadeKernel( params ) {
1389
2153
  // per-term (env / emissive-hit / direct-light / emissive-NEE), matching the megakernel which never
1390
2154
  // re-suppresses the running radiance.
1391
2155
 
2156
+ if ( bdpt ) {
2157
+
2158
+ // Light from below the shading normal: a camera path neither samples it nor lets NEE take it.
2159
+ If( isLight.and( dot( V, N ).lessThanEqual( 0.0 ) ), () => {
2160
+
2161
+ terminatePath();
2162
+ Return();
2163
+
2164
+ } );
2165
+
2166
+ If( isLight, () => {
2167
+
2168
+ // The path's vertex count rides in its first slot.
2169
+ const first = rayID.mul( bdpt.slotsPerPath ).toVar();
2170
+ writeVertexRecord( hitBufferRW, cachedVertex( first.add( uint( cameraDepth ) ) ), {
2171
+ position: hitPoint, tag: uint( 0 ), throughput: throughput.mul( readLightOrigin( hitBufferRW, rayID ).scale ),
2172
+ V, N, facetN: exactFacetN, materialIndex: hitMatIdx, uv: samplingUV, dVCM: subpath.dVCM, dVC: subpath.dVC,
2173
+ // 1: a camera on this side would cull the face, so light tracing must not show it.
2174
+ extra: select( sideAccepts( triangleSide( triangleRow( triangleBuffer, int( hitTriIdx ), 4 ).z ),
2175
+ dot( direction, windingNormal( triangleBuffer, bvhBuffer, int( hitTriIdx ), hitInstance ) ) ), uint( 0 ), uint( 1 ) ),
2176
+ } );
2177
+ writeLightPathLength( hitBufferRW, cachedVertex( first ), bdpt.lightTag, uint( cameraDepth ).add( uint( 1 ) ) );
2178
+
2179
+ } ).ElseIf( bdpt.lightPaths.greaterThan( uint( 0 ) ).and( bdpt.merging ? cameraDepth.lessThanEqual( maxBounceCount ) : cameraDepth.lessThan( maxBounceCount ) ), () => {
2180
+
2181
+ // For ConnectKernel (and MergeKernel); a light vertex must still fit in the bounce budget.
2182
+ writeVertexRecord( hitBufferRW, pendingVertex( rayID ), {
2183
+ position: hitPoint, tag: bdpt.passTag, throughput,
2184
+ V, N, facetN: exactFacetN, materialIndex: hitMatIdx, uv: samplingUV, dVCM: subpath.dVCM, dVC: subpath.dVC, extra: uint( cameraDepth ),
2185
+ } );
2186
+
2187
+ } );
2188
+
2189
+ }
2190
+
1392
2191
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1393
2192
  N, material,
1394
2193
  brdfDir, brdfPdf, brdfValue,
1395
2194
  brdfIsTransmission, brdfColorWeight,
2195
+ brdfIsDiffuseTransmission,
1396
2196
  rngState,
1397
2197
  _pixelCoord, resolution, frame, dimBase,
1398
2198
  ) ).toVar();
@@ -1400,8 +2200,10 @@ export function buildShadeKernel( params ) {
1400
2200
  const bounceDir = indirectResult.direction.toVar();
1401
2201
 
1402
2202
  // 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 ) ), () => {
2203
+ // tracing that ray tunnels through single-sided shells onto whatever sits behind them. A diffuse
2204
+ // transmission draw is the mirror case: it must cross the surface.
2205
+ If( select( brdfIsDiffuseTransmission, dot( bounceDir, NgeoFF ).greaterThanEqual( 0.0 ),
2206
+ brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ) ), () => {
1405
2207
 
1406
2208
  commitDeferredAux( N );
1407
2209
  terminatePath();
@@ -1410,7 +2212,35 @@ export function buildShadeKernel( params ) {
1410
2212
  } );
1411
2213
  // combinedPdf is stored as next bounce's prevBouncePdf for NEE↔implicit-env MIS
1412
2214
  const bouncePdf = max( indirectResult.combinedPdf, 0.001 ).toVar();
1413
- throughput.mulAssign( indirectResult.throughput );
2215
+ const specularScatter = bdpt ? brdfIsTransmission.or( brdfPdf.greaterThanEqual( DELTA_PDF * 0.5 ) ).toVar() : null;
2216
+ if ( bdpt ) {
2217
+
2218
+ If( isLight, () => {
2219
+
2220
+ // Light flows V → bounceDir; the BSDF is not reciprocal. Shading-normal correction: Veach 5.3.2.
2221
+ const correction = lightEndCosine( V, N, exactFacetN, bounceDir ).div( max( abs( dot( bounceDir, N ) ), 1e-4 ) ).toVar();
2222
+ If( specularScatter.or( brdfPdf.greaterThan( 0.0 ).not() ), () => {
2223
+
2224
+ throughput.mulAssign( indirectResult.throughput.mul( correction ) );
2225
+
2226
+ } ).Else( () => {
2227
+
2228
+ throughput.mulAssign( evaluateMaterialResponse( bounceDir, V, N, material )
2229
+ .mul( max( dot( N, bounceDir ), 0.0 ) ).div( max( brdfPdf, MIN_PDF ) ).mul( correction ) );
2230
+
2231
+ } );
2232
+
2233
+ } ).Else( () => {
2234
+
2235
+ throughput.mulAssign( indirectResult.throughput );
2236
+
2237
+ } );
2238
+
2239
+ } else {
2240
+
2241
+ throughput.mulAssign( indirectResult.throughput );
2242
+
2243
+ }
1414
2244
 
1415
2245
  // Adaptive Russian roulette (gap #7) — material-importance + throughput + env-direction aware, replacing
1416
2246
  // the flat clamp(maxThroughput,0.05,0.95). depth = bounceIndex (path length, per gap #4); rayDirection =
@@ -1434,17 +2264,50 @@ export function buildShadeKernel( params ) {
1434
2264
  If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
1435
2265
 
1436
2266
  commitDeferredAux( N );
1437
- terminatePath();
1438
- Return();
2267
+ // One segment more, for the light it may hit: light sampled here keeps its BSDF-hit partner (and its MIS
2268
+ // weight), and bidirectional strategies all reach the longest paths.
2269
+ if ( bdpt ) If( isLight, () => {
2270
+
2271
+ terminatePath();
2272
+ Return();
2273
+
2274
+ } );
2275
+ flags.assign( flags.bitOr( uint( RAY_FLAG.EMISSION_ONLY | RAY_FLAG.AUX_LOCKED ) ) );
1439
2276
 
1440
2277
  } );
1441
2278
 
2279
+ if ( bdpt ) {
2280
+
2281
+ const cosOut = abs( dot( bounceDir, exactFacetN ) );
2282
+ If( specularScatter, () => {
2283
+
2284
+ misOnSpecular( subpath, cosOut );
2285
+
2286
+ } ).Else( () => {
2287
+
2288
+ misOnScatter( subpath, cosOut, calculateMaterialPDF( V, bounceDir, N, material ), calculateMaterialPDF( bounceDir, V, N, material ) );
2289
+
2290
+ } );
2291
+
2292
+ }
2293
+
1442
2294
  const underFacet = dot( facetN, bounceDir ).lessThan( 0.0 ).toVar();
1443
2295
  const newOrigin = offsetRayOrigin( hitPoint, select( underFacet, facetN.negate(), facetN ) );
1444
2296
 
2297
+ if ( bdpt ) If( isLight.not().and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
2298
+
2299
+ const connectible = specularScatter.not().and( dot( N, bounceDir ).greaterThan( 0.0 ) ).and( dot( bounceDir, NgeoFF ).greaterThan( 0.0 ) );
2300
+ bidirectionalAreaHit( newOrigin, bounceDir, ( light ) => select( connectible, select( lampTotal.greaterThan( 0.0 ),
2301
+ estimateLightImportance( light, hitPoint, N, material ).div( max( lampTotal, 1e-30 ) ),
2302
+ float( 1.0 ).div( max( float( lampCount( lamps ) ), 1.0 ) ) ), float( 0.0 ) ) );
2303
+
2304
+ } );
2305
+
1445
2306
  // 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 );
2307
+ // Through a diffusely transmitting surface it samples the far side too.
2308
+ const sunNEE = brdfIsTransmission.not().and( dot( N, bounceDir ).greaterThan( 0.0 ).or( brdfIsDiffuseTransmission ) );
2309
+ // Diffuse transmission crosses the surface on purpose; the next hit is an ordinary bounce's.
2310
+ const underSurface = brdfIsTransmission.not().and( brdfIsDiffuseTransmission.not() ).and( underFacet );
1448
2311
  flags.assign( flags.sub( flags.bitAnd( uint( RAY_FLAG.SUN_NEE | RAY_FLAG.UNDER_SURFACE ) ) )
1449
2312
  .bitOr( select( sunNEE, uint( RAY_FLAG.SUN_NEE ), uint( 0 ) ) )
1450
2313
  .bitOr( select( underSurface, uint( RAY_FLAG.UNDER_SURFACE ), uint( 0 ) ) ) );
@@ -1456,12 +2319,12 @@ export function buildShadeKernel( params ) {
1456
2319
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1457
2320
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1458
2321
  // 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 ) ) ), () => {
2322
+ if ( aux ) If( aux.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1460
2323
 
1461
2324
  writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1462
2325
 
1463
2326
  } );
1464
- writeRngState( hitBufferRW, rayID, rngState );
2327
+ persistRng();
1465
2328
 
1466
2329
  } );
1467
2330