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
@@ -18,11 +18,14 @@ import {
18
18
  normalize,
19
19
  cross,
20
20
  length,
21
+ abs,
21
22
  } from 'three/tsl';
22
23
  import { MIN_PDF } from './Common.js';
24
+ import { sideAccepts } from './BVHTraversal.js';
23
25
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
24
26
  import {
25
27
  EmissiveSample,
28
+ EmissiveSampleIndexed,
26
29
  sampleTriangle,
27
30
  interpolateNormal,
28
31
  fetchTriangleData,
@@ -103,7 +106,7 @@ export const lbvhNodeImportance = Fn( ( [ nMin, power, nMax, coneAxis, cosThetaO
103
106
  *
104
107
  * Returns an EmissiveSample struct.
105
108
  */
106
- export const sampleLightBVHTriangle = Fn( ( [
109
+ const makeSampleLightBVHTriangle = ( indexed ) => Fn( ( [
107
110
  hitPoint, surfaceNormal,
108
111
  rngState,
109
112
  pixelCoord, resolution, frame, dimBase,
@@ -111,9 +114,11 @@ export const sampleLightBVHTriangle = Fn( ( [
111
114
  emissiveTriangleBuffer,
112
115
  emissiveVec4Offset,
113
116
  triangleBuffer, bvhBuffer,
117
+ // The surface passes light through diffusely: a point behind it is as good as one in front.
118
+ throughSurface,
114
119
  ] ) => {
115
120
 
116
- const result = EmissiveSample( {
121
+ const fields = {
117
122
  position: vec3( 0.0 ),
118
123
  normal: vec3( 0.0 ),
119
124
  emission: vec3( 0.0 ),
@@ -123,7 +128,10 @@ export const sampleLightBVHTriangle = Fn( ( [
123
128
  area: float( 0.0 ),
124
129
  cosThetaLight: float( 0.0 ),
125
130
  valid: false,
126
- } ).toVar();
131
+ };
132
+ const result = ( indexed
133
+ ? EmissiveSampleIndexed( { ...fields, triangleIndex: int( - 1 ), faceNormal: vec3( 0.0 ) } )
134
+ : EmissiveSample( fields ) ).toVar();
127
135
 
128
136
  // Accumulated selection PDF (product of per-level choice probabilities)
129
137
  const selectionPdf = float( 1.0 ).toVar();
@@ -279,9 +287,9 @@ export const sampleLightBVHTriangle = Fn( ( [
279
287
  const samplePos = sphResult.position;
280
288
 
281
289
  const surfaceFacing = dot( dir, surfaceNormal );
282
- const emissiveFacing = dot( dir, geoNormal.negate() );
290
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
283
291
 
284
- If( surfaceFacing.greaterThan( float( 0.0 ) ).and( emissiveFacing.greaterThan( float( 0.0 ) ) ), () => {
292
+ If( surfaceFacing.greaterThan( float( 0.0 ) ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ), () => {
285
293
 
286
294
  // Interpolate normal at sampled point via barycentric coords
287
295
  const barycentricCoords = barycentricFromPoint( samplePos, triData.v0, triData.v1, triData.v2 );
@@ -305,6 +313,12 @@ export const sampleLightBVHTriangle = Fn( ( [
305
313
  result.area.assign( area );
306
314
  result.cosThetaLight.assign( emissiveFacing );
307
315
  result.valid.assign( true );
316
+ if ( indexed ) {
317
+
318
+ result.triangleIndex.assign( triangleIndex );
319
+ result.faceNormal.assign( geoNormal );
320
+
321
+ }
308
322
 
309
323
  } );
310
324
 
@@ -322,9 +336,11 @@ export const sampleLightBVHTriangle = Fn( ( [
322
336
  const dir = toEmissive.div( dist );
323
337
 
324
338
  const surfaceFacing = dot( dir, surfaceNormal );
325
- const emissiveFacing = dot( dir, sampleNormal.negate() );
339
+ // The facet's cosine, not the shading normal's: it is what turns an area density into a solid-angle one.
340
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
326
341
 
327
- If( surfaceFacing.greaterThan( float( 0.0 ) ).and( emissiveFacing.greaterThan( float( 0.0 ) ) ), () => {
342
+ // Edge-on (a two-sided emitter passes its side test there) the density is infinite and the light zero.
343
+ If( surfaceFacing.greaterThan( float( 0.0 ) ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ).and( emissiveFacing.greaterThan( float( 0.0 ) ) ), () => {
328
344
 
329
345
  // PDF: selectionPdf / area, converted to solid angle: pdfArea * distSq / cosLight
330
346
  const pdfArea = selectionPdf.div( max( area, float( 1e-10 ) ) );
@@ -339,6 +355,12 @@ export const sampleLightBVHTriangle = Fn( ( [
339
355
  result.area.assign( area );
340
356
  result.cosThetaLight.assign( emissiveFacing );
341
357
  result.valid.assign( true );
358
+ if ( indexed ) {
359
+
360
+ result.triangleIndex.assign( triangleIndex );
361
+ result.faceNormal.assign( geoNormal );
362
+
363
+ }
342
364
 
343
365
  } );
344
366
 
@@ -350,6 +372,9 @@ export const sampleLightBVHTriangle = Fn( ( [
350
372
 
351
373
  } );
352
374
 
375
+ export const sampleLightBVHTriangle = /*@__PURE__*/ makeSampleLightBVHTriangle( false );
376
+ export const sampleLightBVHTriangleIndexed = /*@__PURE__*/ makeSampleLightBVHTriangle( true );
377
+
353
378
  // ================================================================================
354
379
  // LIGHT-BVH MIS PDF (re-walk)
355
380
  // ================================================================================
@@ -482,6 +507,12 @@ export const calculateLightBVHPdf = Fn( ( [
482
507
 
483
508
  // Convert selection pdf → solid-angle measure using the SAME heuristic as the sampler.
484
509
  const triData = TriangleData.wrap( fetchTriangleData( triIdx, triangleBuffer, bvhBuffer, targetInstance ) );
510
+ // The sampler draws only the sides the triangle emits from (a unit normal: sideAccepts has a threshold).
511
+ If( sideAccepts( triData.side, dot( rayDir, normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) ) ) ).not(), () => {
512
+
513
+ selectionPdf.assign( float( 0.0 ) );
514
+
515
+ } );
485
516
  If( useSphericalSampling( triData.v0, triData.v1, triData.v2, shadingPoint ), () => {
486
517
 
487
518
  const solidAngle = sphericalTriangleSolidAngle( triData.v0, triData.v1, triData.v2, shadingPoint );
@@ -490,7 +521,7 @@ export const calculateLightBVHPdf = Fn( ( [
490
521
  } ).Else( () => {
491
522
 
492
523
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
493
- const cosLight = max( dot( rayDir.negate(), geoNormal ), float( 0.001 ) );
524
+ const cosLight = max( abs( dot( rayDir, geoNormal ) ), float( 0.001 ) );
494
525
  const area = triangleArea( triData.v0, triData.v1, triData.v2 );
495
526
  const distSq = hitDistance.mul( hitDistance );
496
527
  const pdfArea = selectionPdf.div( max( area, float( 1e-10 ) ) );
@@ -502,6 +533,6 @@ export const calculateLightBVHPdf = Fn( ( [
502
533
 
503
534
  } );
504
535
 
505
- return max( result, MIN_PDF );
536
+ return select( result.greaterThan( 0.0 ), max( result, MIN_PDF ), float( 0.0 ) );
506
537
 
507
538
  } );
@@ -0,0 +1,293 @@
1
+ /**
2
+ * LightGenerateKernel.js — starts the bidirectional light subpaths, one per pool slot, on a source picked from
3
+ * the source table: an emissive triangle, the physical sky's sun, the environment, or a lamp. Throughput is kept at a max channel
4
+ * of 1, for Russian roulette; its scale rides with the emission data.
5
+ */
6
+
7
+ import {
8
+ Fn, float, vec3, vec4, int, uint, If, instanceIndex, atomicStore, normalize, cross, dot, max, select, Return,
9
+ sqrt, cos, sin, Loop, sampler, bool as tslBool,
10
+ } from 'three/tsl';
11
+
12
+ import { getRandomSample1D, getRandomSample2D, getDecorrelatedSeed, pcgHash } from './Random.js';
13
+ import { sampleTriangle, fetchTriangleData, binarySearchCDF, TriangleData } from './EmissiveSampling.js';
14
+ import { cosineWeightedSample } from './MaterialSampling.js';
15
+ import { offsetRayOrigin, triangleRow } from './Common.js';
16
+ import { triangleSide } from './BVHTraversal.js';
17
+ import { sampleSunDisc, sunRadianceToward } from './Sun.js';
18
+ import { sampleEnvironment, sampleEnvironmentExact } from './Environment.js';
19
+ import {
20
+ DirectionalLight, AreaLight, PointLight, SpotLight, getDirectionalLight, getAreaLight, getPointLight, getSpotLight,
21
+ getSpotAttenuation, sampleSpotGoboMask, sampleIESProfile, sampleCone, LIGHT_TYPE_DIRECTIONAL, LIGHT_TYPE_AREA, LIGHT_TYPE_POINT,
22
+ } from './LightsCore.js';
23
+ import { areaLightRadiance, areaLightSpreadAttenuation } from './LightsSampling.js';
24
+ import { spotConeSolidAngle, directionalConeSolidAngle } from './BidirectionalLamps.js';
25
+ import {
26
+ lightPathPixel, mis, sunEmissionPdf, sourcePick, sourceLampType, sourceLampIndex, SOURCE, DIM_EMIT,
27
+ } from './Bidirectional.js';
28
+ import { sampleGuidedDisc, guidedDiscPdf } from './LightGuide.js';
29
+ import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
30
+ import {
31
+ writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance, writeMediumStack,
32
+ writeRngMis, writeLightOrigin,
33
+ } from '../Processor/PackedRayBuffer.js';
34
+
35
+ const WG_SIZE = 256;
36
+
37
+ export function buildLightGenerateKernel( params ) {
38
+
39
+ const {
40
+ rayBufferRW, hitBufferRW, activeIndicesRW, counters,
41
+ lightBuffer, triangleBuffer, bvhBuffer,
42
+ emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower, emissiveBoost,
43
+ sunDirection, sunRadiance, sunParams, environmentIntensity,
44
+ envTexture, envCDFTexture, envMatrix, envResolution,
45
+ directionalLightsBuffer, areaLightsBuffer, pointLightsBuffer, spotLightsBuffer,
46
+ bidirectional, resolution, frame, transmissiveBounces,
47
+ } = params;
48
+ const { lightPaths } = bidirectional;
49
+
50
+ return Fn( () => {
51
+
52
+ const tid = instanceIndex;
53
+
54
+ If( tid.equal( uint( 0 ) ), () => {
55
+
56
+ atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), lightPaths );
57
+ atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), lightPaths );
58
+
59
+ } );
60
+
61
+ If( tid.greaterThanEqual( lightPaths ), () => {
62
+
63
+ Return();
64
+
65
+ } );
66
+
67
+ activeIndicesRW.element( tid ).assign( tid );
68
+
69
+ const pixel = lightPathPixel( tid, resolution ).toVar();
70
+ const rng = pcgHash( { state: getDecorrelatedSeed( { pixelCoord: pixel, rayIndex: int( 3 ), frame } ) } ).toVar();
71
+
72
+ const origin = vec3( 0.0 ).toVar();
73
+ const direction = vec3( 0.0, 1.0, 0.0 ).toVar();
74
+ const throughput = vec3( 0.0 ).toVar();
75
+ const dVCM = float( 0.0 ).toVar();
76
+ const dVC = float( 0.0 ).toVar();
77
+ const triangle = int( - 1 ).toVar();
78
+ const instanceLeaf = int( 0 ).toVar();
79
+ const startCos = float( 1.0 ).toVar();
80
+ const valid = tslBool( false ).toVar();
81
+
82
+ const xiSource = getRandomSample1D( pixel, int( 0 ), int( DIM_EMIT + 4 ), rng, resolution, frame ).toVar();
83
+ const source = int( 0 ).toVar();
84
+ const hi = int( bidirectional.sourceCount - 1 ).toVar();
85
+ Loop( source.lessThan( hi ), () => {
86
+
87
+ const mid = source.add( hi ).div( 2 ).toVar();
88
+ If( bidirectional.sourceCdf.element( mid ).lessThanEqual( xiSource ), () => {
89
+
90
+ source.assign( mid.add( 1 ) );
91
+
92
+ } ).Else( () => {
93
+
94
+ hi.assign( mid );
95
+
96
+ } );
97
+
98
+ } );
99
+
100
+ // From the scene's bounding disc facing `toLight`, for a light at infinity, where the guide learned light gets in;
101
+ // returns the start and its area density.
102
+ const guide = bidirectional.guideTexture;
103
+ const discOrigin = ( toLight ) => {
104
+
105
+ const start = sampleGuidedDisc( bidirectional, guide, toLight,
106
+ getRandomSample1D( pixel, int( 0 ), int( DIM_EMIT + 7 ), rng, resolution, frame ),
107
+ getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 6 ), rng, resolution, frame ) ).toVar();
108
+ return { start, pdf: guidedDiscPdf( bidirectional, guide, toLight, start ) };
109
+
110
+ };
111
+
112
+ If( source.equal( int( SOURCE.SUN ) ), () => {
113
+
114
+ const toSun = sampleSunDisc( sunDirection, sunParams, getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 5 ), rng, resolution, frame ) ).toVar();
115
+ const radiance = sunRadianceToward( toSun, sunDirection, sunRadiance, sunParams ).mul( environmentIntensity ).toVar();
116
+ const disc = discOrigin( toSun );
117
+ const emissionPdf = sunEmissionPdf( bidirectional, sunParams, disc.pdf ).toVar();
118
+
119
+ If( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ).and( emissionPdf.greaterThan( 0.0 ) ), () => {
120
+
121
+ origin.assign( disc.start );
122
+ direction.assign( toSun.negate() );
123
+ throughput.assign( radiance.div( emissionPdf ) );
124
+ // NEE draws this direction at 1 / solid angle, from any point; the distance is undone at the first hit.
125
+ dVCM.assign( mis( float( 1.0 ).div( sunParams.y.mul( emissionPdf ) ) ) );
126
+ dVC.assign( mis( float( 1.0 ).div( emissionPdf ) ) );
127
+ valid.assign( true );
128
+
129
+ } );
130
+
131
+ } ).ElseIf( source.equal( int( SOURCE.EMITTERS ) ), () => {
132
+
133
+ const pick = binarySearchCDF( lightBuffer, emissiveVec4Offset, emissiveTriangleCount,
134
+ getRandomSample1D( pixel, int( 0 ), int( DIM_EMIT ), rng, resolution, frame ) ).toVar();
135
+ const base = emissiveVec4Offset.add( pick.mul( int( 2 ) ) );
136
+ const entry = lightBuffer.element( base ).toVar(); // triangle, power, cdf, instance leaf
137
+ const emission = lightBuffer.element( base.add( int( 1 ) ) ).toVar(); // rgb, area
138
+ triangle.assign( int( entry.x ) );
139
+ instanceLeaf.assign( int( entry.w ) );
140
+
141
+ const tri = TriangleData.wrap( fetchTriangleData( triangle, triangleBuffer, bvhBuffer, instanceLeaf ) ).toVar();
142
+ const point = sampleTriangle( tri.v0, tri.v1, tri.v2, getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 1 ), rng, resolution, frame ) ).toVar();
143
+ const windingN = normalize( cross( tri.v1.sub( tri.v0 ), tri.v2.sub( tri.v0 ) ) ).toVar();
144
+
145
+ const side = triangleSide( triangleRow( triangleBuffer, triangle, 4 ).z ).toVar();
146
+ const back = side.equal( int( 1 ) ).or( side.equal( int( 2 ) )
147
+ .and( getRandomSample1D( pixel, int( 0 ), int( DIM_EMIT + 3 ), rng, resolution, frame ).lessThan( 0.5 ) ) ).toVar();
148
+ const n = select( back, windingN.negate(), windingN ).toVar();
149
+ direction.assign( cosineWeightedSample( n, getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 2 ), rng, resolution, frame ) ) );
150
+ const cosLight = dot( direction, n ).toVar();
151
+
152
+ const areaPdf = max( entry.y, 0.0 ).div( max( emissiveTotalPower, 1e-10 ) ).div( max( emission.w, 1e-20 ) ).mul(
153
+ select( side.equal( int( 2 ) ), float( 0.5 ), float( 1.0 ) ) ).mul( bidirectional.emitterPick ).toVar();
154
+ const emissionPdf = areaPdf.mul( cosLight ).div( Math.PI ).toVar();
155
+
156
+ If( cosLight.greaterThan( 1e-6 ).and( emissionPdf.greaterThan( 0.0 ) ), () => {
157
+
158
+ origin.assign( offsetRayOrigin( point, n ) );
159
+ // Le·cos / (pdfA·pdfW) = Le·π / pdfA.
160
+ throughput.assign( emission.xyz.mul( emissiveBoost ).mul( Math.PI ).div( areaPdf ) );
161
+ // NEE's density for this point depends on the first hit; Shade multiplies it into dVCM there.
162
+ dVCM.assign( mis( float( 1.0 ).div( emissionPdf ) ) );
163
+ dVC.assign( mis( cosLight.div( emissionPdf ) ) );
164
+ startCos.assign( cosLight );
165
+ valid.assign( true );
166
+
167
+ } );
168
+
169
+ } ).ElseIf( source.equal( int( SOURCE.ENVIRONMENT ) ), () => {
170
+
171
+ const drawn = sampleEnvironmentExact( envCDFTexture, envMatrix, envResolution,
172
+ getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 5 ), rng, resolution, frame ) );
173
+ const toEnvironment = drawn.direction.toVar();
174
+ const directionPdf = drawn.pdf.toVar();
175
+ const radiance = sampleEnvironment( {
176
+ tex: envTexture, samp: sampler( envTexture ), direction: toEnvironment, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight: float( 1.0 ),
177
+ } ).xyz.toVar();
178
+ const disc = discOrigin( toEnvironment );
179
+ const emissionPdf = bidirectional.envPick.mul( directionPdf ).mul( disc.pdf ).toVar();
180
+
181
+ If( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ).and( emissionPdf.greaterThan( 0.0 ) ), () => {
182
+
183
+ origin.assign( disc.start );
184
+ direction.assign( toEnvironment.negate() );
185
+ throughput.assign( radiance.div( emissionPdf ) );
186
+ // NEE draws the direction from the same table, from any point.
187
+ dVCM.assign( mis( directionPdf.div( emissionPdf ) ) );
188
+ dVC.assign( mis( float( 1.0 ).div( emissionPdf ) ) );
189
+ triangle.assign( int( - 1 - SOURCE.ENVIRONMENT ) );
190
+ valid.assign( true );
191
+
192
+ } );
193
+
194
+ } ).ElseIf( source.greaterThanEqual( int( SOURCE.LAMPS ) ), () => {
195
+
196
+ // NEE's density depends on where the light lands; Shade multiplies it into dVCM there.
197
+ const pick = sourcePick( bidirectional, source ).toVar();
198
+ const type = sourceLampType( bidirectional, source ).toVar();
199
+ const index = sourceLampIndex( bidirectional, source, type ).toVar();
200
+ triangle.assign( int( - 1 ).sub( source ) );
201
+ const emissionPdf = float( 0.0 ).toVar();
202
+ const radiance = vec3( 0.0 ).toVar();
203
+ const xi = getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 2 ), rng, resolution, frame ).toVar();
204
+
205
+ If( type.equal( int( LIGHT_TYPE_DIRECTIONAL ) ), () => {
206
+
207
+ const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, index ) );
208
+ const toLight = normalize( light.direction ).toVar();
209
+ radiance.assign( light.color.mul( light.intensity ) );
210
+ emissionPdf.assign( pick );
211
+ If( light.angle.greaterThan( 0.0 ), () => {
212
+
213
+ const solidAngle = directionalConeSolidAngle( light ).toVar();
214
+ toLight.assign( sampleCone( toLight, light.angle.mul( 0.5 ), getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 5 ), rng, resolution, frame ) ) );
215
+ radiance.divAssign( solidAngle );
216
+ emissionPdf.divAssign( solidAngle );
217
+
218
+ } );
219
+ const disc = discOrigin( toLight );
220
+ emissionPdf.mulAssign( disc.pdf );
221
+ origin.assign( disc.start );
222
+ direction.assign( toLight.negate() );
223
+
224
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_AREA ) ), () => {
225
+
226
+ const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, index ) );
227
+ const disk = light.shape.greaterThan( 0.5 );
228
+ const p = getRandomSample2D( pixel, int( 0 ), int( DIM_EMIT + 1 ), rng, resolution, frame ).toVar();
229
+ const r = sqrt( p.x );
230
+ const phi = p.y.mul( 2 * Math.PI );
231
+ origin.assign( light.position.add( light.u.mul( select( disk, r.mul( cos( phi ) ), p.x.mul( 2.0 ).sub( 1.0 ) ) ) )
232
+ .add( light.v.mul( select( disk, r.mul( sin( phi ) ), p.y.mul( 2.0 ).sub( 1.0 ) ) ) ) );
233
+ direction.assign( cosineWeightedSample( light.normal, xi ) );
234
+ const cosLight = dot( direction, light.normal ).toVar();
235
+ radiance.assign( select( light.area.greaterThan( 0.0 ).and( cosLight.greaterThan( 1e-6 ) ),
236
+ areaLightRadiance( light ).mul( areaLightSpreadAttenuation( cosLight, light.spread ) ).mul( cosLight ), vec3( 0.0 ) ) );
237
+ emissionPdf.assign( pick.div( max( light.area, 1e-20 ) ).mul( cosLight ).div( Math.PI ) );
238
+ dVC.assign( mis( cosLight.div( max( emissionPdf, 1e-30 ) ) ) );
239
+
240
+ } ).ElseIf( type.equal( int( LIGHT_TYPE_POINT ) ), () => {
241
+
242
+ const light = PointLight.wrap( getPointLight( pointLightsBuffer, index ) );
243
+ origin.assign( light.position );
244
+ direction.assign( sampleCone( vec3( 0.0, 1.0, 0.0 ), float( Math.PI ), xi ) );
245
+ radiance.assign( light.color.mul( light.intensity ) );
246
+ emissionPdf.assign( pick.div( 4 * Math.PI ) );
247
+
248
+ } ).Else( () => {
249
+
250
+ const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, index ) );
251
+ origin.assign( light.position );
252
+ direction.assign( sampleCone( light.direction, light.angle, xi ) );
253
+ const toLight = direction.negate();
254
+ radiance.assign( light.color.mul( light.intensity )
255
+ .mul( getSpotAttenuation( { coneCosine: cos( light.angle ), blend: light.penumbra, angleCosine: dot( direction, light.direction ) } ) )
256
+ .mul( sampleSpotGoboMask( light, toLight ) ).mul( sampleIESProfile( light, toLight ) ) );
257
+ emissionPdf.assign( pick.div( spotConeSolidAngle( light ) ) );
258
+
259
+ } );
260
+
261
+ If( radiance.x.add( radiance.y ).add( radiance.z ).greaterThan( 0.0 ).and( emissionPdf.greaterThan( 0.0 ) ), () => {
262
+
263
+ throughput.assign( radiance.div( emissionPdf ) );
264
+ dVCM.assign( mis( float( 1.0 ).div( emissionPdf ) ) );
265
+ valid.assign( true );
266
+
267
+ } );
268
+
269
+ } );
270
+
271
+ If( valid.not(), () => {
272
+
273
+ writeRayDirFlags( rayBufferRW, tid, direction, uint( 0 ) );
274
+ Return();
275
+
276
+ } );
277
+
278
+ const scale = max( max( max( throughput.x, throughput.y ), throughput.z ), 1e-20 ).toVar();
279
+
280
+ writeRayOriginMeta( rayBufferRW, tid, origin, int( 0 ), int( 0 ) );
281
+ writeRayDirFlags( rayBufferRW, tid, direction,
282
+ uint( RAY_FLAG.ACTIVE | RAY_FLAG.REDIRECTED | RAY_FLAG.LIGHT_PATH | RAY_FLAG.LIGHT_EMITTED ) );
283
+ writeRayThroughputPdf( rayBufferRW, tid, throughput.div( scale ), float( 0.0 ) );
284
+ writeRayRadiance( rayBufferRW, tid, vec4( vec3( 0.0 ), float( 1.0 ) ) );
285
+ writeMediumStack( rayBufferRW, tid, uint( 0 ), uint( transmissiveBounces ), float( 1.0 ), float( 1.0 ), float( 1.0 ) );
286
+ writeRngMis( hitBufferRW, tid, rng, dVCM, dVC );
287
+ writeLightOrigin( hitBufferRW, tid, triangle, instanceLeaf, startCos, scale );
288
+
289
+ } );
290
+
291
+ }
292
+
293
+ export { WG_SIZE as LIGHT_GENERATE_WG_SIZE };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Where light from infinity gets into the scene, learned from camera paths. A camera path escaping at p in
3
+ * direction ω is where a light path along −ω would have done some good — through a window, past a door — so each
4
+ * escape counts one at p's place on the scene's bounding disc facing ω, per coarse direction. Lights at infinity
5
+ * (the sky, the sun, directional lamps) start their light paths from those counts, a share still drawn uniformly
6
+ * over the disc; every weight reads the same table, so the estimate stays unbiased whatever was learned.
7
+ *
8
+ * The counts sit in the counter buffer past COUNTER.GUIDE (Shade has no binding to spare); a kernel folds them
9
+ * into running sums every so often, which PathTracer copies into the guide texture: GUIDE_BINS rows of
10
+ * GUIDE_CELLS running sums ending at 1, then the row's count of escapes (0 = nothing learned: uniform).
11
+ */
12
+
13
+ import {
14
+ Fn, float, int, uint, vec2, vec3, ivec2, If, Loop, Return, instanceIndex, abs, floor, max, mix, select, normalize, cross,
15
+ dot, sqrt, cos, sin, atomicAdd, atomicLoad, atomicStore,
16
+ } from 'three/tsl';
17
+
18
+ import { COUNTER, GUIDE_COUNTER_WORDS } from '../Processor/QueueManager.js';
19
+
20
+ export const GUIDE_BIN_SIDE = 4;
21
+ export const GUIDE_BINS = GUIDE_BIN_SIDE * GUIDE_BIN_SIDE;
22
+ export const GUIDE_CELL_SIDE = 64;
23
+ export const GUIDE_CELLS = GUIDE_CELL_SIDE * GUIDE_CELL_SIDE;
24
+ export const GUIDE_TEXTURE_WIDTH = GUIDE_CELLS + 1;
25
+ if ( GUIDE_BINS * GUIDE_CELLS !== GUIDE_COUNTER_WORDS ) throw new Error( 'LightGuide: the counter buffer is sized for another guide' );
26
+ /** Floats per row of the build buffer: the texture's row, padded to the 256 bytes a buffer→texture copy needs. */
27
+ export const GUIDE_ROW_STRIDE = Math.ceil( GUIDE_TEXTURE_WIDTH * 4 / 256 ) * 64;
28
+ /** Share of light paths still drawn uniformly over the disc, so no start is ever ruled out. */
29
+ export const GUIDE_UNIFORM_SHARE = 0.2;
30
+
31
+ // The disc facing `toLight`: its plane's axes, as the light path's start is built on them.
32
+ const discFrame = ( toLight ) => {
33
+
34
+ const u = normalize( cross( select( abs( toLight.x ).greaterThan( 0.9 ), vec3( 0, 1, 0 ), vec3( 1, 0, 0 ) ), toLight ) ).toVar();
35
+ return { u, v: cross( toLight, u ).toVar() };
36
+
37
+ };
38
+
39
+ // Coarse direction: an octahedral GUIDE_BIN_SIDE² grid.
40
+ const guideBin = ( dir ) => {
41
+
42
+ const d = dir.div( max( abs( dir.x ).add( abs( dir.y ) ).add( abs( dir.z ) ), 1e-20 ) ).toVar();
43
+ const p = vec2( d.x, d.z ).toVar();
44
+ If( d.y.lessThan( 0.0 ), () => {
45
+
46
+ p.assign( vec2(
47
+ float( 1.0 ).sub( abs( d.z ) ).mul( select( d.x.greaterThanEqual( 0.0 ), 1.0, - 1.0 ) ),
48
+ float( 1.0 ).sub( abs( d.x ) ).mul( select( d.z.greaterThanEqual( 0.0 ), 1.0, - 1.0 ) ),
49
+ ) );
50
+
51
+ } );
52
+ const q = floor( p.mul( 0.5 ).add( 0.5 ).mul( GUIDE_BIN_SIDE ) ).clamp( 0.0, GUIDE_BIN_SIDE - 1 );
53
+ return int( q.y ).mul( GUIDE_BIN_SIDE ).add( int( q.x ) );
54
+
55
+ };
56
+
57
+ // p's place on the disc facing `toLight`, in units of its radius: where the light path through p along −toLight starts.
58
+ const discCoords = ( bdpt, toLight, p ) => {
59
+
60
+ const { u, v } = discFrame( toLight );
61
+ const rel = p.sub( bdpt.sceneCenter );
62
+ return vec2( dot( rel, u ), dot( rel, v ) ).div( bdpt.sceneRadius );
63
+
64
+ };
65
+
66
+ // The guide's cell at disc coordinates `st`, or −1 outside the square it covers.
67
+ const guideCell = ( st ) => {
68
+
69
+ const g = st.mul( 0.5 ).add( 0.5 ).mul( GUIDE_CELL_SIDE ).toVar();
70
+ const inside = g.x.greaterThanEqual( 0.0 ).and( g.y.greaterThanEqual( 0.0 ) ).and( g.x.lessThan( GUIDE_CELL_SIDE ) ).and( g.y.lessThan( GUIDE_CELL_SIDE ) );
71
+ return select( inside, int( g.y ).mul( GUIDE_CELL_SIDE ).add( int( g.x ) ), int( - 1 ) );
72
+
73
+ };
74
+
75
+ const learned = ( bdpt, guide, bin ) => bdpt.guide.greaterThan( uint( 0 ) ).and( guide.load( ivec2( int( GUIDE_CELLS ), bin ) ).x.greaterThan( 0.0 ) );
76
+
77
+ /**
78
+ * Area density, over the disc facing `toLight`, of the light path whose start projects to `p` along −toLight —
79
+ * what `sceneDiscPdf` was before the guide.
80
+ */
81
+ export const guidedDiscPdf = ( bdpt, guide, toLight, p ) => {
82
+
83
+ const st = discCoords( bdpt, toLight, p ).toVar();
84
+ const radius2 = bdpt.sceneRadius.mul( bdpt.sceneRadius ).toVar();
85
+ const uniformPdf = select( dot( st, st ).lessThanEqual( 1.0 ), float( 1.0 ).div( max( radius2.mul( Math.PI ), 1e-30 ) ), float( 0.0 ) ).toVar();
86
+ const bin = guideBin( toLight ).toVar();
87
+ const cell = guideCell( st ).toVar();
88
+ const at = ( c ) => guide.load( ivec2( max( c, int( 0 ) ), bin ) ).x;
89
+ const share = select( cell.greaterThanEqual( int( 0 ) ), at( cell ).sub( select( cell.greaterThan( int( 0 ) ), at( cell.sub( int( 1 ) ) ), float( 0.0 ) ) ), float( 0.0 ) );
90
+ // A cell is ( 2R / GUIDE_CELL_SIDE )².
91
+ const cellPdf = share.mul( GUIDE_CELLS / 4 ).div( max( radius2, 1e-30 ) );
92
+ return select( learned( bdpt, guide, bin ), mix( cellPdf, uniformPdf, GUIDE_UNIFORM_SHARE ), uniformPdf );
93
+
94
+ };
95
+
96
+ /**
97
+ * A light path's start on the disc facing `toLight`: from a learned cell with chance 1 − GUIDE_UNIFORM_SHARE
98
+ * (`choice` < that, rescaled into the cell pick), else uniformly over the disc. `xi` places it within either.
99
+ */
100
+ export const sampleGuidedDisc = ( bdpt, guide, toLight, choice, xi ) => {
101
+
102
+ const { u, v } = discFrame( toLight );
103
+ const st = vec2( 0.0 ).toVar();
104
+ const bin = guideBin( toLight ).toVar();
105
+ If( learned( bdpt, guide, bin ).and( choice.lessThan( 1 - GUIDE_UNIFORM_SHARE ) ), () => {
106
+
107
+ const target = choice.div( 1 - GUIDE_UNIFORM_SHARE ).toVar();
108
+ const lo = int( 0 ).toVar();
109
+ const hi = int( GUIDE_CELLS - 1 ).toVar();
110
+ Loop( lo.lessThan( hi ), () => {
111
+
112
+ const mid = lo.add( hi ).div( 2 ).toVar();
113
+ If( guide.load( ivec2( mid, bin ) ).x.lessThanEqual( target ), () => {
114
+
115
+ lo.assign( mid.add( 1 ) );
116
+
117
+ } ).Else( () => {
118
+
119
+ hi.assign( mid );
120
+
121
+ } );
122
+
123
+ } );
124
+ const cell = vec2( float( lo.mod( int( GUIDE_CELL_SIDE ) ) ), float( lo.div( int( GUIDE_CELL_SIDE ) ) ) );
125
+ st.assign( cell.add( xi ).div( GUIDE_CELL_SIDE ).mul( 2.0 ).sub( 1.0 ) );
126
+
127
+ } ).Else( () => {
128
+
129
+ const r = sqrt( xi.x );
130
+ const phi = xi.y.mul( 2 * Math.PI );
131
+ st.assign( vec2( r.mul( cos( phi ) ), r.mul( sin( phi ) ) ) );
132
+
133
+ } );
134
+ return bdpt.sceneCenter.add( toLight.mul( bdpt.sceneRadius ) ).add( u.mul( st.x ).add( v.mul( st.y ) ).mul( bdpt.sceneRadius ) );
135
+
136
+ };
137
+
138
+ /** A camera path escaping at `p` toward `toLight`: one count for light paths that would have reached p. */
139
+ export const recordEscape = ( counters, bdpt, toLight, p ) => {
140
+
141
+ const cell = guideCell( discCoords( bdpt, toLight, p ) ).toVar();
142
+ If( bdpt.guideLearning.greaterThan( uint( 0 ) ).and( cell.greaterThanEqual( int( 0 ) ) ), () => {
143
+
144
+ atomicAdd( counters.element( uint( COUNTER.GUIDE ).add( uint( guideBin( toLight ).mul( int( GUIDE_CELLS ) ).add( cell ) ) ) ), uint( 1 ) );
145
+
146
+ } );
147
+
148
+ };
149
+
150
+ /** Folds the counts into the build buffer's rows (GUIDE_ROW_STRIDE floats each), one thread a direction bin. */
151
+ export function buildGuideKernel( { counters, out } ) {
152
+
153
+ return Fn( () => {
154
+
155
+ const bin = instanceIndex;
156
+ If( bin.greaterThanEqual( uint( GUIDE_BINS ) ), () => {
157
+
158
+ Return();
159
+
160
+ } );
161
+
162
+ const base = uint( COUNTER.GUIDE ).add( bin.mul( uint( GUIDE_CELLS ) ) ).toVar();
163
+ const total = float( 0.0 ).toVar();
164
+ Loop( { start: int( 0 ), end: int( GUIDE_CELLS ), type: 'int', condition: '<' }, ( { i } ) => {
165
+
166
+ total.addAssign( float( atomicLoad( counters.element( base.add( uint( i ) ) ) ) ) );
167
+
168
+ } );
169
+
170
+ const row = bin.mul( uint( GUIDE_ROW_STRIDE ) ).toVar();
171
+ const running = float( 0.0 ).toVar();
172
+ Loop( { start: int( 0 ), end: int( GUIDE_CELLS ), type: 'int', condition: '<' }, ( { i } ) => {
173
+
174
+ running.addAssign( float( atomicLoad( counters.element( base.add( uint( i ) ) ) ) ) );
175
+ out.element( row.add( uint( i ) ) ).assign( select( total.greaterThan( 0.0 ),
176
+ select( i.equal( int( GUIDE_CELLS - 1 ) ), float( 1.0 ), running.div( total ) ), float( 0.0 ) ) );
177
+
178
+ } );
179
+ out.element( row.add( uint( GUIDE_CELLS ) ) ).assign( total );
180
+
181
+ } )().compute( GUIDE_BINS, [ GUIDE_BINS ] );
182
+
183
+ }
184
+
185
+ /** Forgets every count: what camera paths saw before a reset says nothing about the scene after it. */
186
+ export function buildGuideClearKernel( { counters } ) {
187
+
188
+ return Fn( () => {
189
+
190
+ If( instanceIndex.lessThan( uint( GUIDE_COUNTER_WORDS ) ), () => {
191
+
192
+ atomicStore( counters.element( uint( COUNTER.GUIDE ).add( instanceIndex ) ), uint( 0 ) );
193
+
194
+ } );
195
+
196
+ } )().compute( GUIDE_COUNTER_WORDS, [ 256 ] );
197
+
198
+ }