rayzee 10.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-Hn35o6Z5.js +5145 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BzNpiVHZ.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-Bk7bHg2B.js} +55 -120
  14. package/dist/PathTracer-72XufKAr.js +29976 -0
  15. package/dist/PhysicalSky-BgjdV88k.js +1219 -0
  16. package/dist/RayzeeRenderer-1QRO97eq.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-tnAoUL1j.js +94 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-C67jbci-.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +20 -7
  52. package/src/Processor/ArchiveImporter.js +1265 -0
  53. package/src/Processor/ArchiveReader.js +22 -4
  54. package/src/Processor/AssetLoader.js +327 -1763
  55. package/src/Processor/AtmosphereModel.js +1 -2
  56. package/src/Processor/BVHBuilder.js +14 -126
  57. package/src/Processor/BVHLeafFold.js +4 -6
  58. package/src/Processor/BVHRefitter.js +67 -46
  59. package/src/Processor/BufferLayout.js +291 -0
  60. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  61. package/src/Processor/EnvironmentExactTable.js +187 -0
  62. package/src/Processor/EquirectHDRInfo.js +30 -223
  63. package/src/Processor/FileFormats.js +116 -0
  64. package/src/Processor/GLTFDecoders.js +85 -0
  65. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  66. package/src/Processor/GeometryExtractor.js +15 -5
  67. package/src/Processor/HostMemory.js +7 -1
  68. package/src/Processor/IESParser.js +47 -35
  69. package/src/Processor/InstanceTable.js +1 -1
  70. package/src/Processor/KernelManager.js +35 -2
  71. package/src/Processor/MaterialPacking.js +10 -8
  72. package/src/Processor/PBRT/PBRTMaterials.js +254 -42
  73. package/src/Processor/PBRT/PBRTParser.js +49 -19
  74. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  75. package/src/Processor/PBRT/PBRTSceneBuilder.js +1050 -75
  76. package/src/Processor/PBRT/PBRTTextureBake.js +179 -0
  77. package/src/Processor/PBRT/PFM.js +59 -0
  78. package/src/Processor/PackedRayBuffer.js +116 -9
  79. package/src/Processor/ParallelBVHBuilder.js +3 -6
  80. package/src/Processor/PhysicalSky.js +5 -4
  81. package/src/Processor/PlatformImageLoader.js +50 -6
  82. package/src/Processor/QueueManager.js +39 -5
  83. package/src/Processor/ReinsertionOptimizer.js +2 -2
  84. package/src/Processor/SceneMetadata.js +30 -5
  85. package/src/Processor/SceneProcessor.js +53 -62
  86. package/src/Processor/ShaderBuilder.js +6 -16
  87. package/src/Processor/SimpleSky.js +1 -62
  88. package/src/Processor/SolarLimb.js +3 -0
  89. package/src/Processor/SplitBLAS.js +1 -1
  90. package/src/Processor/StorageTexturePool.js +22 -1
  91. package/src/Processor/TLASBuilder.js +1 -3
  92. package/src/Processor/TextureBuckets.js +237 -0
  93. package/src/Processor/TextureCreator.js +181 -8
  94. package/src/Processor/ToneMapGPU.js +64 -15
  95. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  96. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  97. package/src/Processor/Workers/BVHWorker.js +5 -18
  98. package/src/Processor/Workers/CDFWorker.js +6 -183
  99. package/src/Processor/Workers/TexturesWorker.js +103 -18
  100. package/src/Processor/archiveFormats.js +7 -0
  101. package/src/Processor/utils.js +2 -1
  102. package/src/RayzeeRenderer.js +3900 -0
  103. package/src/RenderSettings.js +68 -62
  104. package/src/Stages/ASVGF.js +13 -14
  105. package/src/Stages/AutoExposure.js +17 -17
  106. package/src/Stages/BilateralFilter.js +2 -1
  107. package/src/Stages/Compositor.js +8 -23
  108. package/src/Stages/DenoiserSettings.js +182 -0
  109. package/src/Stages/EdgeFilter.js +2 -1
  110. package/src/Stages/MotionVector.js +1 -1
  111. package/src/Stages/NRD.js +11 -5
  112. package/src/Stages/NormalDepth.js +12 -8
  113. package/src/Stages/PathTracer.js +336 -43
  114. package/src/Stages/PathTracerStage.js +69 -66
  115. package/src/Stages/Variance.js +1 -1
  116. package/src/Storage/BLASCache.js +1 -1
  117. package/src/Storage/CDFCache.js +18 -17
  118. package/src/Storage/DownloadCache.js +1 -1
  119. package/src/Storage/SceneGraphCodec.js +46 -3
  120. package/src/Storage/SpillStore.js +1 -1
  121. package/src/Storage/StorageManager.js +45 -9
  122. package/src/Storage/areas.js +4 -0
  123. package/src/TSL/BVHTraversal.js +50 -25
  124. package/src/TSL/Bidirectional.js +208 -0
  125. package/src/TSL/BidirectionalLamps.js +132 -0
  126. package/src/TSL/Clearcoat.js +15 -6
  127. package/src/TSL/Common.js +20 -15
  128. package/src/TSL/ConnectKernel.js +167 -0
  129. package/src/TSL/Debugger.js +1 -1
  130. package/src/TSL/Displacement.js +4 -3
  131. package/src/TSL/EmissiveSampling.js +65 -18
  132. package/src/TSL/Environment.js +77 -106
  133. package/src/TSL/EnvironmentCDF.js +85 -39
  134. package/src/TSL/ExtendKernel.js +8 -3
  135. package/src/TSL/FinalWriteKernel.js +4 -4
  136. package/src/TSL/GenerateKernel.js +18 -4
  137. package/src/TSL/HitFacet.js +35 -5
  138. package/src/TSL/LightBVHSampling.js +40 -9
  139. package/src/TSL/LightGenerateKernel.js +293 -0
  140. package/src/TSL/LightGuide.js +198 -0
  141. package/src/TSL/LightSplatKernel.js +180 -0
  142. package/src/TSL/LightVisibility.js +111 -0
  143. package/src/TSL/LightsCore.js +13 -38
  144. package/src/TSL/LightsDirect.js +28 -29
  145. package/src/TSL/LightsIndirect.js +3 -1
  146. package/src/TSL/LightsSampling.js +239 -51
  147. package/src/TSL/MaterialEvaluation.js +113 -31
  148. package/src/TSL/MaterialProperties.js +44 -17
  149. package/src/TSL/MaterialTransmission.js +52 -47
  150. package/src/TSL/MergeKernel.js +224 -0
  151. package/src/TSL/PathTracerCore.js +65 -29
  152. package/src/TSL/Random.js +5 -1
  153. package/src/TSL/SceneResources.js +51 -0
  154. package/src/TSL/ShadeKernel.js +985 -101
  155. package/src/TSL/Struct.js +10 -0
  156. package/src/TSL/Sun.js +1 -1
  157. package/src/TSL/TextureSampling.js +66 -61
  158. package/src/addons/archives.js +14 -0
  159. package/src/addons/bidirectional.js +13 -0
  160. package/src/addons/color.js +32 -0
  161. package/src/addons/formats.js +16 -0
  162. package/src/addons/physicalSky.js +14 -0
  163. package/src/addons/storage.js +19 -0
  164. package/src/core.js +31 -0
  165. package/src/index.js +13 -23
  166. package/src/integrators/BidirectionalIntegrator.js +838 -0
  167. package/src/managers/AnimationManager.js +88 -6
  168. package/src/managers/CameraManager.js +103 -46
  169. package/src/managers/DenoisingManager.js +14 -10
  170. package/src/managers/EnvironmentManager.js +125 -103
  171. package/src/managers/GoboManager.js +9 -0
  172. package/src/managers/MaterialDataManager.js +48 -37
  173. package/src/managers/UniformManager.js +32 -10
  174. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  175. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  176. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -6,6 +6,7 @@ import { BRDFWeights, MaterialCache, MaterialClassification, DFGResult, BaseFres
6
6
  import { PI, PI_INV, TWO_PI, EPSILON, MIN_ROUGHNESS, MIN_CLEARCOAT_ROUGHNESS, MAX_ROUGHNESS, XYZ_TO_REC709, square, classifyMaterial } from './Common.js';
7
7
 
8
8
  import { fresnelSchlickFloat, fresnel0ToIor, iorToFresnel0Vec3, iorToFresnel0, dielectricF0 } from './Fresnel.js';
9
+ import { materialLayers } from './SceneResources.js';
9
10
 
10
11
  // -----------------------------------------------------------------------------
11
12
  // Microfacet Distribution Functions
@@ -407,8 +408,9 @@ export const evalIridescence = Fn( ( [ outsideIOR, eta2, cosTheta1, thinFilmThic
407
408
  // BRDF Weight Calculation
408
409
  // -----------------------------------------------------------------------------
409
410
 
410
- export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
411
+ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ], builder ) => {
411
412
 
413
+ const layers = materialLayers( builder );
412
414
  // Use precomputed values from cache
413
415
  const invRoughness = cache.invRoughness;
414
416
  const metalFactor = cache.metalFactor;
@@ -438,10 +440,10 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
438
440
  const diffuse = float( 1.0 ).sub( baseSpecularWeight )
439
441
  .mul( float( 1.0 ).sub( material.metalness ) )
440
442
  .mul( float( 1.0 ).sub( material.transmission ) ).toVar();
441
- const sheen = material.sheen.mul( cache.maxSheenColor ).toVar();
443
+ const sheen = ( layers.sheen ? material.sheen.mul( cache.maxSheenColor ) : float( 0.0 ) ).toVar();
442
444
 
443
445
  const clearcoat = float( 0.0 ).toVar();
444
- If( mc.hasClearcoat, () => {
446
+ if ( layers.clearcoat ) If( mc.hasClearcoat, () => {
445
447
 
446
448
  clearcoat.assign( material.clearcoat.mul( invRoughness ).mul( 0.4 ) );
447
449
 
@@ -471,17 +473,22 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
471
473
  // Iridescence: shifts energy from diffuse to specular since it modifies specular F0
472
474
  // This preserves the total weight (no inflation) while increasing specular importance
473
475
  const iridescenceBase = invRoughness.mul( mc.isSmooth.select( float( 0.6 ), float( 0.5 ) ) );
474
- const iridescenceWeight = material.iridescence.mul( iridescenceBase )
476
+ // Off, the weight is 0 but the shift is kept: min( 0, diffuse ) still moves a negative diffuse weight to specular.
477
+ const iridescenceWeight = layers.iridescence ? material.iridescence.mul( iridescenceBase )
475
478
  .mul( float( 0.5 ).add( float( 0.5 ).mul(
476
479
  material.iridescenceThicknessRange.y.sub( material.iridescenceThicknessRange.x ).div( 1000.0 )
477
480
  ) ) )
478
- .mul( float( 0.5 ).add( float( 0.5 ).mul( material.iridescenceIOR.div( 2.0 ) ) ) );
481
+ .mul( float( 0.5 ).add( float( 0.5 ).mul( material.iridescenceIOR.div( 2.0 ) ) ) ) : float( 0.0 );
479
482
  const iridescenceShift = min( iridescenceWeight, diffuse );
480
483
  specular.addAssign( iridescenceShift );
481
484
  diffuse.subAssign( iridescenceShift );
482
485
 
486
+ // The diffuse lobe's transmitted share is drawn on the other side, in proportion.
487
+ const diffuseTransmission = ( layers.diffuseTransmission ? diffuse.mul( material.diffuseTransmission ) : float( 0.0 ) ).toVar();
488
+ diffuse.subAssign( diffuseTransmission );
489
+
483
490
  // Single normalization pass
484
- const total = specular.add( diffuse ).add( sheen ).add( clearcoat ).add( transmission );
491
+ const total = specular.add( diffuse ).add( sheen ).add( clearcoat ).add( transmission ).add( diffuseTransmission );
485
492
  const invTotal = float( 1.0 ).div( max( total, 0.001 ) );
486
493
 
487
494
  return BRDFWeights( {
@@ -491,6 +498,7 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
491
498
  clearcoat: clearcoat.mul( invTotal ),
492
499
  transmission: transmission.mul( invTotal ),
493
500
  iridescence: float( 0.0 ),
501
+ diffuseTransmission: diffuseTransmission.mul( invTotal ),
494
502
  } );
495
503
 
496
504
  } );
@@ -508,8 +516,9 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
508
516
  //
509
517
  // Transmission is absent because it only produces NoL < 0. `weights` is normalised over all
510
518
  // lobes including transmission, so omitting its term is exact, not an approximation.
511
- export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
519
+ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ], builder ) => {
512
520
 
521
+ const layers = materialLayers( builder );
513
522
  const pdf = float( 0.0 ).toVar();
514
523
 
515
524
  If( dots.NoL.greaterThan( 0.0 ), () => {
@@ -517,21 +526,24 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
517
526
  pdf.addAssign( weights.diffuse.mul( dots.NoL ).mul( PI_INV ) );
518
527
 
519
528
  const specPdf = float( 0.0 ).toVar();
520
- If( material.anisotropy.greaterThan( 0.0 ), () => {
529
+ // Raw roughness, not clamped: sampleGGXVNDF is called with material.roughness, and a
530
+ // clamp here would report a density the sampler never drew from.
531
+ const isotropic = () => {
521
532
 
522
- const a = computeAnisoAlphas( material.roughness, material.anisotropy );
523
- specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
533
+ specPdf.assign( calculateVNDFPDF( dots.NoH, dots.NoV, material.roughness ) );
524
534
 
525
- } ).Else( () => {
535
+ };
526
536
 
527
- // Raw roughness, not clamped: sampleGGXVNDF is called with material.roughness, and a
528
- // clamp here would report a density the sampler never drew from.
529
- specPdf.assign( calculateVNDFPDF( dots.NoH, dots.NoV, material.roughness ) );
537
+ if ( ! layers.anisotropy ) isotropic();
538
+ else If( material.anisotropy.greaterThan( 0.0 ), () => {
530
539
 
531
- } );
540
+ const a = computeAnisoAlphas( material.roughness, material.anisotropy );
541
+ specPdf.assign( calculateVNDFPDFAniso( a.x, a.y, dots.NoH, dots.ToH, dots.BoH, dots.NoV, dots.ToV, dots.BoV ) );
542
+
543
+ } ).Else( isotropic );
532
544
  pdf.addAssign( weights.specular.mul( specPdf ) );
533
545
 
534
- If( weights.sheen.greaterThan( 0.0 ), () => {
546
+ if ( layers.sheen ) If( weights.sheen.greaterThan( 0.0 ), () => {
535
547
 
536
548
  const sheenPdf = SheenDistribution( dots.NoH, material.sheenRoughness )
537
549
  .mul( dots.NoH ).div( max( float( 4.0 ).mul( dots.VoH ), EPSILON ) );
@@ -539,7 +551,7 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
539
551
 
540
552
  } );
541
553
 
542
- If( weights.clearcoat.greaterThan( 0.0 ), () => {
554
+ if ( layers.clearcoat ) If( weights.clearcoat.greaterThan( 0.0 ), () => {
543
555
 
544
556
  const ccRoughness = clamp( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS, MAX_ROUGHNESS );
545
557
  pdf.addAssign( weights.clearcoat.mul( calculateVNDFPDF( dots.NoH, dots.NoV, ccRoughness ) ) );
@@ -552,6 +564,21 @@ export const calculateBSDFSamplingPDF = Fn( ( [ material, weights, dots ] ) => {
552
564
 
553
565
  } );
554
566
 
567
+ // The sampling density below the surface (cosBelow = −N·L > 0): only the diffuse transmission lobe draws there,
568
+ // chosen with probability `weight` (BRDFWeights.diffuseTransmission).
569
+ export const diffuseTransmissionPdf = Fn( ( [ weight, cosBelow ] ) => {
570
+
571
+ return weight.mul( cosBelow ).mul( PI_INV );
572
+
573
+ } );
574
+
575
+ // The diffuse transmission lobe's selection probability, for samplers outside the classify-once path.
576
+ export const diffuseTransmissionWeight = Fn( ( [ material ] ) => {
577
+
578
+ return BRDFWeights.wrap( calculateBRDFWeightsFromMaterial( material ) ).diffuseTransmission;
579
+
580
+ } );
581
+
555
582
  // Selection probabilities from `material` alone, for samplers outside the wavefront's
556
583
  // classify-once path. Must be the same weights whose density calculateBSDFSamplingPDF reports.
557
584
  export const calculateBRDFWeightsFromMaterial = Fn( ( [ material ] ) => {
@@ -10,6 +10,7 @@ import { DistributionGGX } from './MaterialProperties.js';
10
10
  import { ImportanceSampleGGX } from './MaterialSampling.js';
11
11
  import { getRandomSample1D, getRandomSample2D, pcgHash } from './Random.js';
12
12
  import { handleSubsurfaceEntry, SubsurfaceEntryResult } from './Subsurface.js';
13
+ import { materialLayers } from './SceneResources.js';
13
14
 
14
15
  // ================================================================================
15
16
  // STRUCTS (local to transmission)
@@ -158,8 +159,9 @@ export const calculateBeerLawAbsorption = /*@__PURE__*/ wgslFn( `
158
159
  export const sampleMicrofacetTransmission = Fn( ( [
159
160
  V, N, ior, roughness, entering, dispersion, xi, rngState, pathWavelength,
160
161
  pixelCoord, resolution, frame, dimBase,
161
- ] ) => {
162
+ ], builder ) => {
162
163
 
164
+ const layers = materialLayers( builder );
163
165
  const result = MicrofacetTransmissionResult( {
164
166
  direction: vec3( 0.0 ),
165
167
  halfVector: vec3( 0.0 ),
@@ -169,48 +171,7 @@ export const sampleMicrofacetTransmission = Fn( ( [
169
171
  pathWavelength: pathWavelength,
170
172
  } ).toVar();
171
173
 
172
- // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
173
- If( roughness.lessThanEqual( 0.05 ).and( dispersion.greaterThan( 0.0 ) ), () => {
174
-
175
- result.halfVector.assign( N );
176
- result.didReflect.assign( false );
177
-
178
- const eta = ior;
179
- const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
180
-
181
- // Reuse the path's locked wavelength if any; else sample a new one and tint once.
182
- If( pathWavelength.greaterThan( 0.0 ), () => {
183
-
184
- const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
185
- etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
186
-
187
- } ).Else( () => {
188
-
189
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 5 ) ), rngState, resolution, frame ) ) );
190
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
191
- result.colorWeight.assign( spectralSample.colorWeight );
192
- result.pathWavelength.assign( spectralSample.wavelength );
193
-
194
- } );
195
-
196
- // Perfect refraction using surface normal
197
- const refractDir = refract( V.negate(), N, etaRatio ).toVar();
198
-
199
- // Check for total internal reflection
200
- If( dot( refractDir, refractDir ).lessThan( 0.001 ), () => {
201
-
202
- result.direction.assign( reflect( V.negate(), N ) );
203
- result.didReflect.assign( true );
204
- result.pdf.assign( 1.0 );
205
-
206
- } ).Else( () => {
207
-
208
- result.direction.assign( refractDir );
209
- result.pdf.assign( 1.0 );
210
-
211
- } );
212
-
213
- } ).Else( () => {
174
+ const microfacet = () => {
214
175
 
215
176
  // Use minimum roughness to avoid numerical issues for rough surfaces
216
177
  const transmissionRoughness = max( MIN_ROUGHNESS, roughness );
@@ -223,7 +184,7 @@ export const sampleMicrofacetTransmission = Fn( ( [
223
184
  const etaRatio = select( entering, float( 1.0 ).div( ior ), ior ).toVar();
224
185
 
225
186
  // Reuse the path's locked wavelength if any; else sample a new one and tint once.
226
- If( dispersion.greaterThan( 0.0 ), () => {
187
+ if ( layers.dispersion ) If( dispersion.greaterThan( 0.0 ), () => {
227
188
 
228
189
  If( pathWavelength.greaterThan( 0.0 ), () => {
229
190
 
@@ -278,7 +239,51 @@ export const sampleMicrofacetTransmission = Fn( ( [
278
239
 
279
240
  } );
280
241
 
281
- } );
242
+ };
243
+
244
+ // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
245
+ if ( ! layers.dispersion ) microfacet();
246
+ else If( roughness.lessThanEqual( 0.05 ).and( dispersion.greaterThan( 0.0 ) ), () => {
247
+
248
+ result.halfVector.assign( N );
249
+ result.didReflect.assign( false );
250
+
251
+ const eta = ior;
252
+ const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
253
+
254
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
255
+ If( pathWavelength.greaterThan( 0.0 ), () => {
256
+
257
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
258
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
259
+
260
+ } ).Else( () => {
261
+
262
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 5 ) ), rngState, resolution, frame ) ) );
263
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
264
+ result.colorWeight.assign( spectralSample.colorWeight );
265
+ result.pathWavelength.assign( spectralSample.wavelength );
266
+
267
+ } );
268
+
269
+ // Perfect refraction using surface normal
270
+ const refractDir = refract( V.negate(), N, etaRatio ).toVar();
271
+
272
+ // Check for total internal reflection
273
+ If( dot( refractDir, refractDir ).lessThan( 0.001 ), () => {
274
+
275
+ result.direction.assign( reflect( V.negate(), N ) );
276
+ result.didReflect.assign( true );
277
+ result.pdf.assign( 1.0 );
278
+
279
+ } ).Else( () => {
280
+
281
+ result.direction.assign( refractDir );
282
+ result.pdf.assign( 1.0 );
283
+
284
+ } );
285
+
286
+ } ).Else( microfacet );
282
287
 
283
288
  return result;
284
289
 
@@ -430,7 +435,7 @@ export const handleMaterialTransparency = Fn( ( [
430
435
  transmissiveTraversals,
431
436
  currentMediumIOR, previousMediumIOR,
432
437
  pathWavelength,
433
- ] ) => {
438
+ ], builder ) => {
434
439
 
435
440
  const result = MaterialInteractionResult( {
436
441
  continueRay: false,
@@ -534,7 +539,7 @@ export const handleMaterialTransparency = Fn( ( [
534
539
 
535
540
  // Subsurface (independent of transmission; works at transmission==0). Entry is a lottery
536
541
  // (prob = weight) so 1-weight falls through to the opaque BRDF; exit is deterministic.
537
- If( handled.not().and( material.subsurface.greaterThan( 0.0 ) ), () => {
542
+ if ( materialLayers( builder ).subsurface ) If( handled.not().and( material.subsurface.greaterThan( 0.0 ) ), () => {
538
543
 
539
544
  const entering = dot( ray.direction, normal ).lessThan( 0.0 );
540
545
  const doEnter = entering.not().or( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 8 ) ), rngState, resolution, frame ).lessThan( material.subsurface ) );
@@ -0,0 +1,224 @@
1
+ /**
2
+ * MergeKernel.js — vertex merging (Georgiev et al. 2012, "Light Transport Simulation with Vertex Connection and
3
+ * Merging"): each camera vertex Shade left pending gathers the light vertices within the merge radius, as photon
4
+ * mapping does, MIS-weighted against every other strategy. It is the one strategy for light no path can connect:
5
+ * a lamp behind glass seen in a mirror, a caustic seen through water.
6
+ *
7
+ * The radius is a few pixels' footprint where the camera vertex is (Bidirectional.js mergeRadiusAt), so it grows
8
+ * with the distance from the camera. The light vertex cache is filed by that radius in shells, each a hash grid of
9
+ * cells two of its largest radii wide: one list a cell, threaded through the cached records' spare lane (`next`),
10
+ * built after the light pass by mergeInsert. A gather searches the one or two shells its sphere reaches, 2³ cells each.
11
+ */
12
+
13
+ import {
14
+ Fn, float, vec3, vec4, int, uint, ivec3, If, Loop, instanceIndex, atomicLoad, atomicStore, atomicFunc, Return, dot, floor, max, min, select,
15
+ length, log2, exp2, Break, abs,
16
+ } from 'three/tsl';
17
+
18
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
19
+ import { calculateMaterialPDF } from './LightsSampling.js';
20
+ import { luminance } from './Common.js';
21
+ import { regularizePathContribution } from './PathTracerCore.js';
22
+ import { resolveSurfaceMaterial, misMergePartial, misWeight, strategyWeight, mergeRadiusAt, mergeEta, facingSide, STRATEGY } from './Bidirectional.js';
23
+ import { COUNTER } from '../Processor/QueueManager.js';
24
+ import {
25
+ readRayRadiance, writeRayRadiance, pendingVertex, cachedVertex, readVertexTag, readVertexRecord, readVertexPosition,
26
+ readVertexNext, writeVertexNext, readLightPathLength,
27
+ } from '../Processor/PackedRayBuffer.js';
28
+
29
+ const WG_SIZE = 256;
30
+ export const MERGE_EMPTY = 0xFFFFFFFF;
31
+ // A cell's list is walked at most this far: a bound on the work, never reached at the radius the radius scale gives.
32
+ const MAX_CELL_STEPS = 1024;
33
+
34
+ // Shells of radius ratio 1.25: a sphere whose radius changes by (1 ± slope) across it spans at most two.
35
+ export const MERGE_SHELL_RATIO = 1.25;
36
+ const LOG2_RATIO = Math.log2( MERGE_SHELL_RATIO );
37
+
38
+ const shellOf = ( bdpt, radius ) => int( max( floor( log2( radius.div( bdpt.mergeMin ) ).div( LOG2_RATIO ) ), 0.0 ) );
39
+
40
+ // Twice the largest radius whose sphere can reach the shell (its own largest, over 1 − slope).
41
+ const shellCell = ( bdpt, shell ) => bdpt.mergeMin.mul( exp2( float( shell.add( int( 1 ) ) ).mul( LOG2_RATIO ) ) ).mul( 2.0 )
42
+ .div( max( float( 1.0 ).sub( bdpt.mergeSlope ), 0.5 ) );
43
+
44
+ const cellHash = ( shell, cell, hashMask ) => uint( cell.x ).mul( uint( 73856093 ) )
45
+ .bitXor( uint( cell.y ).mul( uint( 19349663 ) ) )
46
+ .bitXor( uint( cell.z ).mul( uint( 83492791 ) ) )
47
+ .bitXor( uint( shell ).mul( uint( 2654435761 ) ) )
48
+ .bitAnd( hashMask );
49
+
50
+ // A light vertex's shell and cell.
51
+ const filing = ( bdpt, p ) => {
52
+
53
+ const shell = shellOf( bdpt, mergeRadiusAt( bdpt, p ) ).toVar();
54
+ return { shell, cell: ivec3( floor( p.div( shellCell( bdpt, shell ) ) ) ).toVar() };
55
+
56
+ };
57
+
58
+ /** Empties every list head. */
59
+ export function buildMergeClearKernel( { head, hashSize } ) {
60
+
61
+ return Fn( () => {
62
+
63
+ If( instanceIndex.lessThan( uint( hashSize ) ), () => {
64
+
65
+ atomicStore( head.element( instanceIndex ), uint( MERGE_EMPTY ) );
66
+
67
+ } );
68
+
69
+ } )().compute( Math.ceil( hashSize / WG_SIZE ) * WG_SIZE, [ WG_SIZE ] );
70
+
71
+ }
72
+
73
+ /** Files each light vertex of this frame at the head of its cell's list. */
74
+ export function buildMergeInsertKernel( { hitBufferRW, head, bidirectional: bdpt } ) {
75
+
76
+ const { lightPaths, slotsPerPath, lightTag, hashMask } = bdpt;
77
+
78
+ return Fn( () => {
79
+
80
+ const slot = instanceIndex.toVar();
81
+ If( slot.greaterThanEqual( lightPaths.mul( slotsPerPath ) ), () => {
82
+
83
+ Return();
84
+
85
+ } );
86
+
87
+ const depth = slot.mod( slotsPerPath ).toVar();
88
+ If( depth.greaterThanEqual( readLightPathLength( hitBufferRW, cachedVertex( slot.sub( depth ) ), lightTag ) ), () => {
89
+
90
+ Return();
91
+
92
+ } );
93
+
94
+ const at = cachedVertex( slot );
95
+ const { shell, cell } = filing( bdpt, readVertexPosition( hitBufferRW, at ) );
96
+ const bucket = cellHash( shell, cell, hashMask );
97
+ writeVertexNext( hitBufferRW, at, atomicFunc( 'atomicExchange', head.element( bucket ), slot ) );
98
+
99
+ } );
100
+
101
+ }
102
+
103
+ export function buildMergeKernel( params ) {
104
+
105
+ const {
106
+ rayBufferRW, hitBufferRO, activeIndicesRO, counters, head, materialBuffer, bidirectional: bdpt,
107
+ maxBounceCount, globalIlluminationIntensity, fireflyThreshold, accumFrame,
108
+ // Some material passes light through diffusely: light arriving from behind its surface merges too.
109
+ diffuseTransmission = true,
110
+ } = params;
111
+ const { passTag, slotsPerPath, strategyView, hashMask } = bdpt;
112
+
113
+ return Fn( () => {
114
+
115
+ const tid = instanceIndex;
116
+ If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
117
+
118
+ Return();
119
+
120
+ } );
121
+
122
+ const rayID = activeIndicesRO.element( tid ).toVar();
123
+ const pending = pendingVertex( rayID );
124
+ If( readVertexTag( hitBufferRO, pending ).notEqual( passTag ), () => {
125
+
126
+ Return();
127
+
128
+ } );
129
+
130
+ const cam = readVertexRecord( hitBufferRO, pending );
131
+ const cameraDepth = int( cam.extra ).toVar();
132
+ // A merged path of k scattering vertices: the camera's cameraDepth + 1 and the light's l share one.
133
+ const lightBudget = int( maxBounceCount ).add( int( 1 ) ).sub( cameraDepth ).toVar();
134
+ const material = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
135
+ const gathered = vec3( 0.0 ).toVar();
136
+
137
+ const radius = mergeRadiusAt( bdpt, cam.position ).toVar();
138
+ const radius2 = radius.mul( radius ).toVar();
139
+ const eta = mergeEta( bdpt, radius ).toVar();
140
+ const trusted = eta.mul( bdpt.mergeTrust );
141
+ const vc = float( 1.0 ).div( max( trusted.mul( trusted ), 1e-30 ) ).toVar();
142
+ // The shells the sphere reaches: its radius at its nearest and farthest points from the camera.
143
+ const distance = length( cam.position.sub( bdpt.cameraPosition ) ).toVar();
144
+ const nearShell = shellOf( bdpt, max( bdpt.mergeConst.add( bdpt.mergeSlope.mul( max( distance.sub( radius ), 0.0 ) ) ), bdpt.mergeMin ) ).toVar();
145
+ const farShell = min( shellOf( bdpt, bdpt.mergeConst.add( bdpt.mergeSlope.mul( distance.add( radius ) ) ) ), nearShell.add( int( 1 ) ) ).toVar();
146
+
147
+ // Per shell, the 2 × 2 × 2 cells the sphere can reach, the cells being at least two radii wide.
148
+ Loop( { start: int( 0 ), end: int( 16 ), type: 'int', condition: '<' }, ( { i } ) => {
149
+
150
+ const shell = nearShell.add( i.shiftRight( int( 3 ) ) ).toVar();
151
+ If( shell.greaterThan( farShell ), () => {
152
+
153
+ Break();
154
+
155
+ } );
156
+ const cellSize = shellCell( bdpt, shell ).toVar();
157
+ const corner = i.bitAnd( int( 7 ) );
158
+ const cell = ivec3( floor( cam.position.div( cellSize ).sub( 0.5 ) ) )
159
+ .add( ivec3( corner.bitAnd( int( 1 ) ), corner.shiftRight( int( 1 ) ).bitAnd( int( 1 ) ), corner.shiftRight( int( 2 ) ) ) ).toVar();
160
+ const slot = atomicLoad( head.element( cellHash( shell, cell, hashMask ) ) ).toVar();
161
+ const steps = int( 0 ).toVar();
162
+
163
+ Loop( slot.notEqual( uint( MERGE_EMPTY ) ).and( steps.lessThan( int( MAX_CELL_STEPS ) ) ), () => {
164
+
165
+ const at = cachedVertex( slot );
166
+ const position = readVertexPosition( hitBufferRO, at ).toVar();
167
+ const offset = position.sub( cam.position );
168
+ const l = int( slot.mod( slotsPerPath ) ).add( int( 1 ) ).toVar();
169
+ If( dot( offset, offset ).lessThan( radius2 ).and( l.lessThanEqual( lightBudget ) ), () => {
170
+
171
+ // Its own shell and cell only: two lists sharing a bucket (a hash collision) must not count it twice.
172
+ const own = filing( bdpt, position );
173
+ If( own.shell.equal( shell ).and( own.cell.x.equal( cell.x ) ).and( own.cell.y.equal( cell.y ) ).and( own.cell.z.equal( cell.z ) ), () => {
174
+
175
+ const light = readVertexRecord( hitBufferRO, at );
176
+ const cosShading = dot( light.V, cam.N ).toVar();
177
+ const cosFacet = dot( light.V, cam.facetN ).toVar();
178
+ // Light arriving on this side, or from behind a surface that passes it through diffusely. A corner's
179
+ // other wall stays in: its light vertices stand in for the part of the sphere past the corner, which
180
+ // the density divides by too.
181
+ const onThisSide = cosShading.greaterThan( 0.0 ).and( cosFacet.greaterThan( 0.0 ) );
182
+ const throughSurface = material.diffuseTransmission.greaterThan( 0.0 ).and( cosShading.lessThan( 0.0 ) ).and( cosFacet.lessThan( 0.0 ) );
183
+ If( diffuseTransmission ? onThisSide.or( throughSurface ) : onThisSide, () => {
184
+
185
+ const f = evaluateMaterialResponse( cam.V, light.V, cam.N, material );
186
+ const wLight = misMergePartial( light, calculateMaterialPDF( cam.V, light.V, cam.N, material ), vc );
187
+ const reverseN = diffuseTransmission ? facingSide( cam.N, light.V, material ) : cam.N;
188
+ const wCamera = misMergePartial( cam, calculateMaterialPDF( light.V, cam.V, reverseN, material ), vc );
189
+ const scattering = cameraDepth.add( l );
190
+ // Alone, a path of k scattering vertices is reached by k merges.
191
+ const weight = strategyWeight( strategyView, STRATEGY.MERGE, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering ) ) );
192
+ const gi = select( scattering.greaterThan( int( 1 ) ), globalIlluminationIntensity, float( 1.0 ) );
193
+ // The photon's flux is per unit area; the BSDF wants it per projected solid angle (Veach 5.3.2).
194
+ const correction = diffuseTransmission ? abs( cosShading ).div( max( abs( cosFacet ), 1e-4 ) ) : cosShading.div( max( cosFacet, 1e-4 ) );
195
+ gathered.addAssign( regularizePathContribution(
196
+ cam.throughput.mul( f ).mul( light.throughput ).mul( weight.mul( correction ).div( eta ).mul( gi ) ),
197
+ float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),
198
+ ) );
199
+
200
+ } );
201
+
202
+ } );
203
+
204
+ } );
205
+
206
+ slot.assign( readVertexNext( hitBufferRO, at ) );
207
+ steps.addAssign( int( 1 ) );
208
+
209
+ } );
210
+
211
+ } );
212
+
213
+ If( luminance( gathered ).greaterThan( 0.0 ), () => {
214
+
215
+ const radiance = readRayRadiance( rayBufferRW, rayID ).toVar();
216
+ writeRayRadiance( rayBufferRW, rayID, vec4( radiance.xyz.add( gathered ), radiance.w ) );
217
+
218
+ } );
219
+
220
+ } );
221
+
222
+ }
223
+
224
+ export { WG_SIZE as MERGE_WG_SIZE };