rayzee 9.4.2 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +223 -21
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-xuUJiklP.js +4301 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-C5PKjJoT.js +934 -0
  6. package/dist/ColorManagement-CheBCPaW.js +1488 -0
  7. package/dist/EngineDefaults-DH3bI5t-.js +844 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-BnHghprG.js +97 -0
  12. package/dist/{NeuralRetouch-vkdXXyK2.js → NeuralRetouch-CW11yLZ_.js} +8 -6
  13. package/dist/{NeuralSuperRes-FVqRJKQV.js → NeuralSuperRes-BekaecYg.js} +55 -120
  14. package/dist/PathTracer-Ccl9KOuF.js +29729 -0
  15. package/dist/PhysicalSky-Dv2PMFGW.js +1219 -0
  16. package/dist/RayzeeRenderer-BQxnzCTD.js +3542 -0
  17. package/dist/SolarLimb-ken-JbDK.js +721 -0
  18. package/dist/ToneMapGPU-BHi5JzIq.js +177 -0
  19. package/dist/ViewTransforms-D0rTLBsX.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2266 -44079
  33. package/dist/rayzee.umd.js +485 -473
  34. package/dist/sceneCachePolicy-BmwYOYDu.js +183 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/EngineDefaults.js +22 -8
  41. package/src/EngineEvents.js +2 -0
  42. package/src/EngineIssues.js +1 -0
  43. package/src/PathTracerApp.js +617 -4016
  44. package/src/Pipeline/PipelineContext.js +0 -4
  45. package/src/Platform.js +36 -0
  46. package/src/Processor/ArchiveCache.js +1 -1
  47. package/src/Processor/ArchiveImporter.js +1259 -0
  48. package/src/Processor/AssetLoader.js +322 -1746
  49. package/src/Processor/AtmosphereModel.js +1 -2
  50. package/src/Processor/BVHBuilder.js +4 -95
  51. package/src/Processor/EnvironmentExactTable.js +187 -0
  52. package/src/Processor/EquirectHDRInfo.js +30 -223
  53. package/src/Processor/FileFormats.js +116 -0
  54. package/src/Processor/GLTFDecoders.js +85 -0
  55. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  56. package/src/Processor/GeometryExtractor.js +12 -1
  57. package/src/Processor/IESParser.js +47 -35
  58. package/src/Processor/KernelManager.js +35 -2
  59. package/src/Processor/MaterialPacking.js +8 -7
  60. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  61. package/src/Processor/PBRT/PBRTParser.js +30 -17
  62. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  63. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  64. package/src/Processor/PBRT/PFM.js +59 -0
  65. package/src/Processor/PackedRayBuffer.js +116 -9
  66. package/src/Processor/ParallelBVHBuilder.js +3 -6
  67. package/src/Processor/PhysicalSky.js +5 -4
  68. package/src/Processor/PlatformImageLoader.js +50 -6
  69. package/src/Processor/QueueManager.js +12 -3
  70. package/src/Processor/ReinsertionOptimizer.js +2 -2
  71. package/src/Processor/SceneProcessor.js +15 -55
  72. package/src/Processor/ShaderBuilder.js +6 -16
  73. package/src/Processor/SolarLimb.js +3 -0
  74. package/src/Processor/TextureCreator.js +1 -1
  75. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  76. package/src/Processor/Workers/BVHWorker.js +5 -18
  77. package/src/Processor/Workers/CDFWorker.js +6 -183
  78. package/src/Processor/archiveFormats.js +7 -0
  79. package/src/Processor/utils.js +2 -1
  80. package/src/RayzeeRenderer.js +3902 -0
  81. package/src/RenderSettings.js +49 -41
  82. package/src/Stages/ASVGF.js +5 -7
  83. package/src/Stages/AutoExposure.js +3 -12
  84. package/src/Stages/Compositor.js +8 -23
  85. package/src/Stages/NRD.js +9 -2
  86. package/src/Stages/NormalDepth.js +9 -6
  87. package/src/Stages/PathTracer.js +254 -38
  88. package/src/Stages/PathTracerStage.js +11 -16
  89. package/src/Storage/BLASCache.js +1 -1
  90. package/src/Storage/CDFCache.js +18 -17
  91. package/src/Storage/DownloadCache.js +1 -1
  92. package/src/Storage/SceneGraphCodec.js +46 -3
  93. package/src/Storage/SpillStore.js +1 -1
  94. package/src/Storage/StorageManager.js +2 -4
  95. package/src/Storage/areas.js +4 -0
  96. package/src/TSL/BVHTraversal.js +62 -24
  97. package/src/TSL/Bidirectional.js +204 -0
  98. package/src/TSL/BidirectionalLamps.js +132 -0
  99. package/src/TSL/Clearcoat.js +15 -6
  100. package/src/TSL/Common.js +19 -14
  101. package/src/TSL/ConnectKernel.js +147 -0
  102. package/src/TSL/Debugger.js +1 -1
  103. package/src/TSL/Displacement.js +4 -3
  104. package/src/TSL/EmissiveSampling.js +64 -17
  105. package/src/TSL/Environment.js +77 -106
  106. package/src/TSL/EnvironmentCDF.js +85 -39
  107. package/src/TSL/ExtendKernel.js +8 -3
  108. package/src/TSL/FinalWriteKernel.js +4 -4
  109. package/src/TSL/GenerateKernel.js +18 -4
  110. package/src/TSL/HitFacet.js +35 -5
  111. package/src/TSL/LightBVHSampling.js +40 -9
  112. package/src/TSL/LightGenerateKernel.js +293 -0
  113. package/src/TSL/LightGuide.js +198 -0
  114. package/src/TSL/LightSplatKernel.js +165 -0
  115. package/src/TSL/LightsCore.js +13 -38
  116. package/src/TSL/LightsDirect.js +28 -29
  117. package/src/TSL/LightsIndirect.js +3 -1
  118. package/src/TSL/LightsSampling.js +90 -38
  119. package/src/TSL/MaterialEvaluation.js +113 -31
  120. package/src/TSL/MaterialProperties.js +44 -17
  121. package/src/TSL/MaterialTransmission.js +52 -47
  122. package/src/TSL/MergeKernel.js +218 -0
  123. package/src/TSL/PathTracerCore.js +65 -29
  124. package/src/TSL/Random.js +4 -0
  125. package/src/TSL/SceneResources.js +51 -0
  126. package/src/TSL/ShadeKernel.js +881 -103
  127. package/src/TSL/Struct.js +10 -0
  128. package/src/TSL/Sun.js +1 -1
  129. package/src/TSL/TextureSampling.js +65 -60
  130. package/src/addons/archives.js +14 -0
  131. package/src/addons/bidirectional.js +13 -0
  132. package/src/addons/color.js +32 -0
  133. package/src/addons/formats.js +16 -0
  134. package/src/addons/physicalSky.js +14 -0
  135. package/src/addons/storage.js +19 -0
  136. package/src/core.js +35 -0
  137. package/src/index.js +3 -1
  138. package/src/integrators/BidirectionalIntegrator.js +832 -0
  139. package/src/managers/EnvironmentManager.js +85 -33
  140. package/src/managers/MaterialDataManager.js +42 -0
  141. package/src/managers/UniformManager.js +0 -1
  142. package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
  143. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  144. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -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,218 @@
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,
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, 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
+ } = params;
109
+ const { passTag, slotsPerPath, strategyView, hashMask } = bdpt;
110
+
111
+ return Fn( () => {
112
+
113
+ const tid = instanceIndex;
114
+ If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
115
+
116
+ Return();
117
+
118
+ } );
119
+
120
+ const rayID = activeIndicesRO.element( tid ).toVar();
121
+ const pending = pendingVertex( rayID );
122
+ If( readVertexTag( hitBufferRO, pending ).notEqual( passTag ), () => {
123
+
124
+ Return();
125
+
126
+ } );
127
+
128
+ const cam = readVertexRecord( hitBufferRO, pending );
129
+ const cameraDepth = int( cam.extra ).toVar();
130
+ // A merged path of k scattering vertices: the camera's cameraDepth + 1 and the light's l share one.
131
+ const lightBudget = int( maxBounceCount ).add( int( 1 ) ).sub( cameraDepth ).toVar();
132
+ const material = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
133
+ const gathered = vec3( 0.0 ).toVar();
134
+
135
+ const radius = mergeRadiusAt( bdpt, cam.position ).toVar();
136
+ const radius2 = radius.mul( radius ).toVar();
137
+ const eta = mergeEta( bdpt, radius ).toVar();
138
+ const trusted = eta.mul( bdpt.mergeTrust );
139
+ const vc = float( 1.0 ).div( max( trusted.mul( trusted ), 1e-30 ) ).toVar();
140
+ // The shells the sphere reaches: its radius at its nearest and farthest points from the camera.
141
+ const distance = length( cam.position.sub( bdpt.cameraPosition ) ).toVar();
142
+ const nearShell = shellOf( bdpt, max( bdpt.mergeConst.add( bdpt.mergeSlope.mul( max( distance.sub( radius ), 0.0 ) ) ), bdpt.mergeMin ) ).toVar();
143
+ const farShell = min( shellOf( bdpt, bdpt.mergeConst.add( bdpt.mergeSlope.mul( distance.add( radius ) ) ) ), nearShell.add( int( 1 ) ) ).toVar();
144
+
145
+ // Per shell, the 2 × 2 × 2 cells the sphere can reach, the cells being at least two radii wide.
146
+ Loop( { start: int( 0 ), end: int( 16 ), type: 'int', condition: '<' }, ( { i } ) => {
147
+
148
+ const shell = nearShell.add( i.shiftRight( int( 3 ) ) ).toVar();
149
+ If( shell.greaterThan( farShell ), () => {
150
+
151
+ Break();
152
+
153
+ } );
154
+ const cellSize = shellCell( bdpt, shell ).toVar();
155
+ const corner = i.bitAnd( int( 7 ) );
156
+ const cell = ivec3( floor( cam.position.div( cellSize ).sub( 0.5 ) ) )
157
+ .add( ivec3( corner.bitAnd( int( 1 ) ), corner.shiftRight( int( 1 ) ).bitAnd( int( 1 ) ), corner.shiftRight( int( 2 ) ) ) ).toVar();
158
+ const slot = atomicLoad( head.element( cellHash( shell, cell, hashMask ) ) ).toVar();
159
+ const steps = int( 0 ).toVar();
160
+
161
+ Loop( slot.notEqual( uint( MERGE_EMPTY ) ).and( steps.lessThan( int( MAX_CELL_STEPS ) ) ), () => {
162
+
163
+ const at = cachedVertex( slot );
164
+ const position = readVertexPosition( hitBufferRO, at ).toVar();
165
+ const offset = position.sub( cam.position );
166
+ const l = int( slot.mod( slotsPerPath ) ).add( int( 1 ) ).toVar();
167
+ If( dot( offset, offset ).lessThan( radius2 ).and( l.lessThanEqual( lightBudget ) ), () => {
168
+
169
+ // Its own shell and cell only: two lists sharing a bucket (a hash collision) must not count it twice.
170
+ const own = filing( bdpt, position );
171
+ 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 ) ), () => {
172
+
173
+ const light = readVertexRecord( hitBufferRO, at );
174
+ const cosShading = dot( light.V, cam.N ).toVar();
175
+ const cosFacet = dot( light.V, cam.facetN ).toVar();
176
+ // Light arriving on this side. A corner's other wall stays in: its light vertices stand in for the
177
+ // part of the sphere past the corner, which the density divides by too.
178
+ If( cosShading.greaterThan( 0.0 ).and( cosFacet.greaterThan( 0.0 ) ), () => {
179
+
180
+ const f = evaluateMaterialResponse( cam.V, light.V, cam.N, material );
181
+ const wLight = misMergePartial( light, calculateMaterialPDF( cam.V, light.V, cam.N, material ), vc );
182
+ const wCamera = misMergePartial( cam, calculateMaterialPDF( light.V, cam.V, cam.N, material ), vc );
183
+ const scattering = cameraDepth.add( l );
184
+ // Alone, a path of k scattering vertices is reached by k merges.
185
+ const weight = strategyWeight( strategyView, STRATEGY.MERGE, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering ) ) );
186
+ const gi = select( scattering.greaterThan( int( 1 ) ), globalIlluminationIntensity, float( 1.0 ) );
187
+ // The photon's flux is per unit area; the BSDF wants it per projected solid angle (Veach 5.3.2).
188
+ const correction = cosShading.div( max( cosFacet, 1e-4 ) );
189
+ gathered.addAssign( regularizePathContribution(
190
+ cam.throughput.mul( f ).mul( light.throughput ).mul( weight.mul( correction ).div( eta ).mul( gi ) ),
191
+ float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),
192
+ ) );
193
+
194
+ } );
195
+
196
+ } );
197
+
198
+ } );
199
+
200
+ slot.assign( readVertexNext( hitBufferRO, at ) );
201
+ steps.addAssign( int( 1 ) );
202
+
203
+ } );
204
+
205
+ } );
206
+
207
+ If( luminance( gathered ).greaterThan( 0.0 ), () => {
208
+
209
+ const radiance = readRayRadiance( rayBufferRW, rayID ).toVar();
210
+ writeRayRadiance( rayBufferRW, rayID, vec4( radiance.xyz.add( gathered ), radiance.w ) );
211
+
212
+ } );
213
+
214
+ } );
215
+
216
+ }
217
+
218
+ export { WG_SIZE as MERGE_WG_SIZE };
@@ -45,8 +45,10 @@ import {
45
45
  computeAnisoAlphas,
46
46
  calculateBRDFWeights,
47
47
  sheenSamplingRoughness,
48
+ diffuseTransmissionPdf,
48
49
  } from './MaterialProperties.js';
49
- import { evaluateMaterialResponseFromDots, evaluateSpecularDeltaFromDots } from './MaterialEvaluation.js';
50
+ import { evaluateMaterialResponseFromDots, evaluateSpecularDeltaFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
51
+ import { materialLayers } from './SceneResources.js';
50
52
 
51
53
  import {
52
54
  ImportanceSampleCosine,
@@ -71,14 +73,16 @@ export const generateSampledDirection = Fn( ( [
71
73
  mc,
72
74
  weightsComputed, cachedBrdfWeights,
73
75
  materialCacheCached, cachedMaterialCache,
74
- ] ) => {
76
+ ], builder ) => {
75
77
 
78
+ const layers = materialLayers( builder );
76
79
  const resultDirection = vec3( 0.0 ).toVar();
77
80
  const resultValue = vec3( 0.0 ).toVar();
78
81
  const resultPdf = float( 0.0 ).toVar();
79
82
  const resultIsTransmission = tslBool( false ).toVar();
80
83
  const resultColorWeight = vec3( 1.0 ).toVar();
81
84
  const resultIsDelta = tslBool( false ).toVar();
85
+ const resultIsDiffuseTransmission = tslBool( false ).toVar();
82
86
 
83
87
  // Compute BRDF weights
84
88
  const weights = cachedBrdfWeights.toVar();
@@ -114,17 +118,42 @@ export const generateSampledDirection = Fn( ( [
114
118
  const cumulativeDiffuse = weights.diffuse.toVar();
115
119
  const cumulativeSpecular = cumulativeDiffuse.add( weights.specular ).toVar();
116
120
  const cumulativeSheen = cumulativeSpecular.add( weights.sheen ).toVar();
117
- const cumulativeClearcoat = cumulativeSheen.add( weights.clearcoat );
121
+ const cumulativeClearcoat = cumulativeSheen.add( weights.clearcoat ).toVar();
122
+ const cumulativeDiffuseTransmission = cumulativeClearcoat.add( weights.diffuseTransmission );
118
123
 
119
124
  // Chained If/ElseIf so emitted WGSL becomes a single mutually-exclusive branch
120
- // (replaces five separate If blocks gated on a `sampled` flag — divergence hotspot)
121
- If( rand.lessThan( cumulativeDiffuse ), () => {
125
+ // (replaces five separate If blocks gated on a `sampled` flag — divergence hotspot). A layer no material uses
126
+ // has weight 0, so its link could never be taken and is left out.
127
+ const isotropicSpecular = () => {
128
+
129
+ If( material.roughness.greaterThan( 0.0 ), () => {
130
+
131
+ const TBN = constructTBN( { N } );
132
+ const localV = TBN.transpose().mul( V );
133
+
134
+ // VNDF sampling
135
+ const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
136
+ H.assign( TBN.mul( localH ) );
137
+
138
+ resultDirection.assign( reflect( V.negate(), H ) );
139
+
140
+ } ).Else( () => {
141
+
142
+ resultDirection.assign( reflect( V.negate(), N ) );
143
+ resultIsDelta.assign( true );
144
+
145
+ } );
146
+
147
+ };
148
+
149
+ let lobes = If( rand.lessThan( cumulativeDiffuse ), () => {
122
150
 
123
151
  resultDirection.assign( ImportanceSampleCosine( { N, xi } ) );
124
152
 
125
153
  } ).ElseIf( rand.lessThan( cumulativeSpecular ), () => {
126
154
 
127
- If( material.anisotropy.greaterThan( 0.0 ), () => {
155
+ if ( ! layers.anisotropy ) isotropicSpecular();
156
+ else If( material.anisotropy.greaterThan( 0.0 ), () => {
128
157
 
129
158
  // Shared frame → sampler and eval/PDF stay bit-identical (MIS consistency)
130
159
  const f = AnisoFrame.wrap( anisoTangentFrame( N, material.anisotropyRotation ) );
@@ -138,25 +167,11 @@ export const generateSampledDirection = Fn( ( [
138
167
 
139
168
  resultDirection.assign( reflect( V.negate(), H ) );
140
169
 
141
- } ).ElseIf( material.roughness.greaterThan( 0.0 ), () => {
142
-
143
- const TBN = constructTBN( { N } );
144
- const localV = TBN.transpose().mul( V );
145
-
146
- // VNDF sampling
147
- const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
148
- H.assign( TBN.mul( localH ) );
149
-
150
- resultDirection.assign( reflect( V.negate(), H ) );
151
-
152
- } ).Else( () => {
153
-
154
- resultDirection.assign( reflect( V.negate(), N ) );
155
- resultIsDelta.assign( true );
170
+ } ).Else( isotropicSpecular );
156
171
 
157
- } );
172
+ } );
158
173
 
159
- } ).ElseIf( rand.lessThan( cumulativeSheen ), () => {
174
+ if ( layers.sheen ) lobes = lobes.ElseIf( rand.lessThan( cumulativeSheen ), () => {
160
175
 
161
176
  H.assign( ImportanceSampleGGX( { N, roughness: sheenSamplingRoughness( material.sheenRoughness ), Xi: xi } ) );
162
177
  resultDirection.assign( reflect( V.negate(), H ) );
@@ -165,7 +180,9 @@ export const generateSampledDirection = Fn( ( [
165
180
  // would add a P(reject)·cos term to the true density that calculateBSDFSamplingPDF cannot
166
181
  // model, and dividing by the smaller modelled density inflated the lobe 23 %.
167
182
 
168
- } ).ElseIf( rand.lessThan( cumulativeClearcoat ), () => {
183
+ } );
184
+
185
+ if ( layers.clearcoat ) lobes = lobes.ElseIf( rand.lessThan( cumulativeClearcoat ), () => {
169
186
 
170
187
  // VNDF, not ImportanceSampleGGX: the mixture density reports this lobe with
171
188
  // calculateVNDFPDF, and sampling the half-vector GGX distribution instead would make the
@@ -175,7 +192,16 @@ export const generateSampledDirection = Fn( ( [
175
192
  H.assign( ccTBN.mul( sampleGGXVNDF( { V: ccTBN.transpose().mul( V ), roughness: clearcoatRoughness, Xi: xi } ) ) );
176
193
  resultDirection.assign( reflect( V.negate(), H ) );
177
194
 
178
- } ).Else( () => {
195
+ } );
196
+
197
+ if ( layers.diffuseTransmission ) lobes = lobes.ElseIf( rand.lessThan( cumulativeDiffuseTransmission ), () => {
198
+
199
+ resultDirection.assign( ImportanceSampleCosine( { N: N.negate(), xi } ) );
200
+ resultIsDiffuseTransmission.assign( true );
201
+
202
+ } );
203
+
204
+ lobes.Else( () => {
179
205
 
180
206
  const entering = dot( V, N ).greaterThan( 0.0 );
181
207
  // pathWavelength=0 — the spectral lock happens downstream; colorWeight is carried out on
@@ -193,10 +219,11 @@ export const generateSampledDirection = Fn( ( [
193
219
 
194
220
  } );
195
221
 
196
- // One mixture density for every reflection lobe: any of them could have produced this
197
- // direction, so the chosen lobe's own pdf is not the density we sampled from.
198
- If( resultIsTransmission.not(), () => {
222
+ // Below the surface only the diffuse transmission lobe draws, so its own density is the mixture's.
223
+ const reflected = () => {
199
224
 
225
+ // One mixture density for every reflection lobe: any of them could have produced this
226
+ // direction, so the chosen lobe's own pdf is not the density we sampled from.
200
227
  const dotsOut = DotProducts.wrap( computeDotProductsAniso( N, V, resultDirection, material ) );
201
228
  If( resultIsDelta, () => {
202
229
 
@@ -213,7 +240,15 @@ export const generateSampledDirection = Fn( ( [
213
240
 
214
241
  } );
215
242
 
216
- } );
243
+ };
244
+
245
+ if ( ! layers.diffuseTransmission ) If( resultIsTransmission.not(), reflected );
246
+ else If( resultIsDiffuseTransmission, () => {
247
+
248
+ resultValue.assign( evaluateDiffuseTransmission( material, max( dot( N, V ), 0.001 ) ) );
249
+ resultPdf.assign( diffuseTransmissionPdf( weights.diffuseTransmission, dot( N, resultDirection ).negate() ) );
250
+
251
+ } ).ElseIf( resultIsTransmission.not(), reflected );
217
252
 
218
253
  resultPdf.assign( max( resultPdf, MIN_PDF ) );
219
254
 
@@ -223,6 +258,7 @@ export const generateSampledDirection = Fn( ( [
223
258
  pdf: resultPdf,
224
259
  isTransmission: resultIsTransmission,
225
260
  colorWeight: resultColorWeight,
261
+ isDiffuseTransmission: resultIsDiffuseTransmission,
226
262
  } );
227
263
 
228
264
  } );
package/src/TSL/Random.js CHANGED
@@ -123,6 +123,8 @@ export const RandomPointInCircle = ( rngState ) => {
123
123
  // +13 clearcoat lobe (Clearcoat)
124
124
  // +14 area/spot light phi (LightsSampling)
125
125
  // +15 light uv second component (LightsSampling)
126
+ // +16 light path to connect to (ConnectKernel, bidirectional)
127
+ // +17 light vertex on that path (ConnectKernel, bidirectional)
126
128
  //
127
129
  // Stride is 32 rather than the ~16 in use so a new call site does not force a renumbering.
128
130
  // The index only keys a scramble seed, so spare slots cost nothing.
@@ -131,6 +133,8 @@ export const RandomPointInCircle = ( rngState ) => {
131
133
  // the next bounce's dimensions, so they live above AUX_BASE:
132
134
  // AUX_BASE + 0..63 env-backdrop blur taps (ShadeKernel)
133
135
  // AUX_BASE + 64.. light reservoir, one dimension per scene light (LightsSampling)
136
+ // AUX_BASE + 2048.. light subpath emission (LightGenerateKernel); light paths draw at pixels
137
+ // offset by LIGHT_PIXEL_ROW_OFFSET rows, so no camera path shares a sequence
134
138
  export const SAMPLER_DIMS_PER_BOUNCE = 32;
135
139
  export const SAMPLER_DIM_AUX_BASE = 4096;
136
140
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A renderer's own scene resources for its shaders: material texture buckets, the albedo maps alpha-cutout
3
+ * shadow rays read, gobo and IES textures, and the alpha-shadow switch. They ride in the build context of each
4
+ * kernel that reads them: a TSL function's body runs when its kernel compiles, often at the first dispatch, so
5
+ * module-level state was read from whichever renderer — or pipeline stage — had set it last.
6
+ *
7
+ * @typedef {Object} SceneResources
8
+ * @property {Array<Node>} srgbBuckets - sRGB material bucket texture nodes (albedo, emissive, sheen, specular colour)
9
+ * @property {Array<Node>} linearBuckets - linear material bucket texture nodes
10
+ * @property {?Array<Node>} shadowAlbedoMaps - sRGB buckets alpha-cutout shadow rays sample, or null
11
+ * @property {?Node} goboMaps - spot / directional gobo texture array, or null
12
+ * @property {?Node} iesProfiles - IES profile texture array, or null
13
+ * @property {?Node} alphaShadows - int uniform, 1 = shadow rays test texture alpha; null = never
14
+ * @property {?Object<string, boolean>} materialLayers - the MATERIAL_LAYERS some material uses; null = all of them
15
+ */
16
+
17
+ import { context } from 'three/tsl';
18
+
19
+ const KEY = 'rayzeeScene';
20
+
21
+ /** Builds `node` (a kernel's root call) with these resources in its context. */
22
+ export const withSceneResources = ( node, resources ) => context( node, { [ KEY ]: resources } );
23
+
24
+ /**
25
+ * The resources of the kernel being built; call from a TSL function body with its builder.
26
+ * @returns {SceneResources}
27
+ */
28
+ export function sceneResources( builder ) {
29
+
30
+ const resources = builder.context[ KEY ];
31
+ if ( ! resources ) throw new Error( 'this kernel samples scene resources: build its root with withSceneResources()' );
32
+ return resources;
33
+
34
+ }
35
+
36
+ /** The material layers a kernel compiles only while some material in its scene has them. */
37
+ export const MATERIAL_LAYERS = Object.freeze( [ 'clearcoat', 'sheen', 'iridescence', 'anisotropy', 'subsurface', 'dispersion', 'diffuseTransmission' ] );
38
+
39
+ /** Every layer compiled in. */
40
+ export const ALL_MATERIAL_LAYERS = Object.freeze( Object.fromEntries( MATERIAL_LAYERS.map( ( layer ) => [ layer, true ] ) ) );
41
+
42
+ /**
43
+ * The material layers the kernel being built compiles; call from a TSL function body with its builder. A layer no
44
+ * material uses is left out of every function that reads it. Outside a scene kernel (a test), every layer is in.
45
+ * @returns {Object<string, boolean>}
46
+ */
47
+ export function materialLayers( builder ) {
48
+
49
+ return builder.context[ KEY ]?.materialLayers ?? ALL_MATERIAL_LAYERS;
50
+
51
+ }