rayzee 10.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -0,0 +1,147 @@
1
+ /**
2
+ * ConnectKernel.js — connects each camera vertex Shade left pending to one vertex of a random light
3
+ * path, weighted by how many of that path's vertices fit the bounce budget (Davidovič et al. 2014).
4
+ */
5
+
6
+ import {
7
+ Fn, float, vec2, vec4, int, uint, If, instanceIndex, atomicLoad, Return, dot, sqrt, min, max, length,
8
+ } from 'three/tsl';
9
+
10
+ import { getRandomSample1D, pcgHash, SAMPLER_DIMS_PER_BOUNCE } from './Random.js';
11
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
12
+ import { calculateMaterialPDF } from './LightsSampling.js';
13
+ import { traceShadowRayRefractiveOpaque } from './LightsDirect.js';
14
+ import { traverseBVHShadow } from './BVHTraversal.js';
15
+ import { offsetRayOrigin, SHADOW_END, luminance } from './Common.js';
16
+ import { regularizePathContribution } from './PathTracerCore.js';
17
+ import {
18
+ resolveSurfaceMaterial, lightEndCosine, misPartial, misWeight, strategyWeight, STRATEGY, DIM_CONNECT_PATH, DIM_CONNECT_VERTEX,
19
+ } from './Bidirectional.js';
20
+ import { COUNTER } from '../Processor/QueueManager.js';
21
+ import {
22
+ readRayRadiance, writeRayRadiance, readRngState,
23
+ pendingVertex, cachedVertex, readVertexTag, readVertexRecord, readLightPathLength,
24
+ } from '../Processor/PackedRayBuffer.js';
25
+
26
+ const WG_SIZE = 256;
27
+
28
+ export function buildConnectKernel( params ) {
29
+
30
+ const {
31
+ rayBufferRW, hitBufferRO, activeIndicesRO, counters,
32
+ bvhBuffer, triangleBuffer, materialBuffer,
33
+ resolution, frame, accumFrame, currentBounce, chunkRowBase,
34
+ lightPaths, slotsPerPath, lightTag, passTag, strategyView,
35
+ maxBounceCount, globalIlluminationIntensity, fireflyThreshold,
36
+ // Vertex merging's η² at a point (Bidirectional.js mergeVmAt), or null without it.
37
+ mergeVm = null,
38
+ } = params;
39
+
40
+ return Fn( () => {
41
+
42
+ const tid = instanceIndex;
43
+ If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
44
+
45
+ Return();
46
+
47
+ } );
48
+
49
+ const rayID = activeIndicesRO.element( tid ).toVar();
50
+ const pending = pendingVertex( rayID );
51
+ If( readVertexTag( hitBufferRO, pending ).notEqual( passTag ), () => {
52
+
53
+ Return();
54
+
55
+ } );
56
+
57
+ const camRecord = readVertexRecord( hitBufferRO, pending );
58
+ const cam = { ...camRecord, vm: mergeVm?.( camRecord.position ) ?? null };
59
+ const cameraVertices = int( cam.extra ).add( int( 1 ) ).toVar();
60
+
61
+ const resX = int( resolution.x );
62
+ const globalPixel = int( rayID ).add( chunkRowBase.mul( resX ) );
63
+ const pixel = vec2( float( globalPixel.mod( resX ) ).add( 0.5 ), float( globalPixel.div( resX ) ).add( 0.5 ) ).toVar();
64
+ const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
65
+ // Only PCG reads it; hashed so the path's own stream, already saved by Shade, is not replayed.
66
+ const rng = pcgHash( { state: readRngState( hitBufferRO, rayID ).bitXor( uint( 0x9E3779B9 ) ) } ).toVar();
67
+ const uPath = getRandomSample1D( pixel, int( 0 ), dimBase.add( int( DIM_CONNECT_PATH ) ), rng, resolution, frame ).toVar();
68
+ const uVertex = getRandomSample1D( pixel, int( 0 ), dimBase.add( int( DIM_CONNECT_VERTEX ) ), rng, resolution, frame ).toVar();
69
+
70
+ const path = min( uint( float( lightPaths ).mul( uPath ) ), lightPaths.sub( uint( 1 ) ) ).toVar();
71
+ const first = path.mul( slotsPerPath ).toVar();
72
+ const stored = int( readLightPathLength( hitBufferRO, cachedVertex( first ), lightTag ) );
73
+ const usable = min( stored, int( maxBounceCount ).add( int( 1 ) ).sub( cameraVertices ) ).toVar();
74
+ If( usable.lessThan( int( 1 ) ), () => {
75
+
76
+ Return();
77
+
78
+ } );
79
+
80
+ const c = min( int( float( usable ).mul( uVertex ) ), usable.sub( int( 1 ) ) ).toVar();
81
+ const lightRecord = readVertexRecord( hitBufferRO, cachedVertex( first.add( uint( c ) ) ) );
82
+ const light = { ...lightRecord, vm: mergeVm?.( lightRecord.position ) ?? null };
83
+
84
+ const toLight = light.position.sub( cam.position ).toVar();
85
+ const dist2 = max( dot( toLight, toLight ), 1e-12 ).toVar();
86
+ const dir = toLight.div( sqrt( dist2 ) ).toVar();
87
+
88
+ const cosCamera = dot( dir, cam.N ).toVar();
89
+ const cosCameraFacet = dot( dir, cam.facetN ).toVar();
90
+ const cosLightFacet = dot( dir, light.facetN ).negate().toVar();
91
+ // The camera end tests as NEE does; at the light end the camera vertex is the viewer, which only the facet bounds.
92
+ If( cosCamera.lessThanEqual( 0.0 ).or( cosCameraFacet.lessThanEqual( 0.0 ) ).or( cosLightFacet.lessThanEqual( 0.0 ) ), () => {
93
+
94
+ Return();
95
+
96
+ } );
97
+
98
+ const cameraMaterial = resolveSurfaceMaterial( cam.materialIndex, cam.uv, cam.N, materialBuffer );
99
+ const lightMaterial = resolveSurfaceMaterial( light.materialIndex, light.uv, light.N, materialBuffer );
100
+ const toCamera = dir.negate();
101
+
102
+ const fCamera = evaluateMaterialResponse( cam.V, dir, cam.N, cameraMaterial ).toVar();
103
+ const fLight = evaluateMaterialResponse( toCamera, light.V, light.N, lightMaterial ).toVar();
104
+ const geometry = cosCamera.mul( lightEndCosine( light.V, light.N, light.facetN, toCamera ) ).div( dist2 );
105
+
106
+ const cameraForward = calculateMaterialPDF( cam.V, dir, cam.N, cameraMaterial );
107
+ const cameraReverse = calculateMaterialPDF( dir, cam.V, cam.N, cameraMaterial );
108
+ const lightForward = calculateMaterialPDF( light.V, toCamera, light.N, lightMaterial );
109
+ const lightReverse = calculateMaterialPDF( toCamera, light.V, light.N, lightMaterial );
110
+
111
+ // Georgiev (40)-(41): each side's sum, from the other side's density of reaching it.
112
+ const wLight = misPartial( cameraForward.mul( cosLightFacet ).div( dist2 ), light, lightReverse );
113
+ const wCamera = misPartial( lightForward.mul( cosCameraFacet ).div( dist2 ), cam, cameraReverse );
114
+
115
+ const scattering = cameraVertices.add( c ).add( int( 1 ) );
116
+ // Alone, a path of k scattering vertices is reached by k − 1 connections.
117
+ const weight = strategyWeight( strategyView, STRATEGY.CONNECT, misWeight( wLight, wCamera ), float( 1.0 ).div( float( scattering.sub( int( 1 ) ) ) ) );
118
+ const contribution = regularizePathContribution(
119
+ cam.throughput.mul( fCamera ).mul( fLight ).mul( light.throughput )
120
+ .mul( geometry.mul( weight ).mul( float( usable ) ).mul( globalIlluminationIntensity ) ),
121
+ float( scattering.sub( int( 1 ) ) ), fireflyThreshold, int( accumFrame ),
122
+ ).toVar();
123
+
124
+ If( luminance( contribution ).greaterThan( 0.0 ), () => {
125
+
126
+ const origin = offsetRayOrigin( cam.position, cam.facetN ).toVar();
127
+ const segment = offsetRayOrigin( light.position, light.facetN ).sub( origin ).toVar();
128
+ const segmentLength = length( segment ).toVar();
129
+ const visibility = traceShadowRayRefractiveOpaque(
130
+ origin, segment.div( segmentLength ), segmentLength.mul( SHADOW_END ),
131
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
132
+ );
133
+
134
+ If( visibility.greaterThan( 0.0 ), () => {
135
+
136
+ const radiance = readRayRadiance( rayBufferRW, rayID ).toVar();
137
+ writeRayRadiance( rayBufferRW, rayID, vec4( radiance.xyz.add( contribution.mul( visibility ) ), radiance.w ) );
138
+
139
+ } );
140
+
141
+ } );
142
+
143
+ } );
144
+
145
+ }
146
+
147
+ export { WG_SIZE as CONNECT_WG_SIZE };
@@ -72,7 +72,7 @@ export const TraceDebugMode = Fn( ( [
72
72
  visMode, debugVisScale,
73
73
  // Screen info
74
74
  pixelCoord, resolution,
75
- // Material textures are read from the module-level bucket nodes (setMaterialBucketTextures).
75
+ // Material textures come from the kernel's SceneResources.
76
76
  // Camera matrices (for depth debug mode)
77
77
  cameraProjectionMatrix, cameraViewMatrix,
78
78
  // Frame counter (for stochastic debug modes)
@@ -1,8 +1,9 @@
1
+ import { sceneResources } from './SceneResources.js';
1
2
  import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, uintBitsToFloat } from 'three/tsl';
2
3
 
3
4
  import { struct } from './patches.js';
4
5
  import { triangleRow, instanceRows, instanceDirToWorld } from './Common.js';
5
- import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
+ import { sampleDisplacementMap, bucketTexelSize } from './TextureSampling.js';
6
7
 
7
8
  // Ray-displacement intersection configuration
8
9
  const MAX_MARCH_STEPS = 32;
@@ -32,7 +33,7 @@ export const DisplacementResult = struct( {
32
33
  */
33
34
  export const refineDisplacedIntersection = Fn( ( [
34
35
  ray, hitInfo, triangleBuffer, material, bounceIndex, bvhBuffer, instanceLeaf
35
- ] ) => {
36
+ ], builder ) => {
36
37
 
37
38
  const resultHitPoint = hitInfo.hitPoint.toVar();
38
39
  const resultUV = hitInfo.uv.toVar();
@@ -205,7 +206,7 @@ export const refineDisplacedIntersection = Fn( ( [
205
206
 
206
207
  // Compute displaced normal from height-field gradients using UV tangent vectors.
207
208
  // Finite-difference step = one texel of the SELECTED bucket array's real dimensions.
208
- const texel = bucketTexelSize( getLinearBucketTextures(), material.displacementMapIndex ).toVar();
209
+ const texel = bucketTexelSize( sceneResources( builder ).linearBuckets, material.displacementMapIndex ).toVar();
209
210
  const hC = finalHeight;
210
211
  const hU = sampleDisplacementMap(
211
212
  material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
@@ -32,11 +32,13 @@ import {
32
32
  computeDotProductsAniso, instanceRows, instanceNormalToWorld, instancePointToWorld,
33
33
  unpackTriangleNormal, triangleRow, SHADOW_END
34
34
  } from './Common.js';
35
- import { TRI_MATERIAL_MASK } from '../EngineDefaults.js';
35
+ import { TRI_MATERIAL_MASK } from '../Processor/BufferLayout.js';
36
36
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
37
37
  import { calculateMaterialPDFFromDots } from './LightsSampling.js';
38
- import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
38
+ import { evaluateMaterialResponseFromDots, evaluateDiffuseTransmission } from './MaterialEvaluation.js';
39
+ import { diffuseTransmissionWeight, diffuseTransmissionPdf } from './MaterialProperties.js';
39
40
  import { DotProducts } from './Struct.js';
41
+ import { triangleSide, sideAccepts } from './BVHTraversal.js';
40
42
 
41
43
  // ================================================================================
42
44
  // STRUCTS
@@ -54,6 +56,21 @@ export const EmissiveSample = struct( {
54
56
  valid: 'bool', // Whether sample is valid
55
57
  } );
56
58
 
59
+ // EmissiveSample plus the triangle it landed on, for the bidirectional MIS weight.
60
+ export const EmissiveSampleIndexed = struct( {
61
+ position: 'vec3',
62
+ normal: 'vec3',
63
+ emission: 'vec3',
64
+ direction: 'vec3',
65
+ distance: 'float',
66
+ pdf: 'float',
67
+ area: 'float',
68
+ cosThetaLight: 'float',
69
+ valid: 'bool',
70
+ triangleIndex: 'int',
71
+ faceNormal: 'vec3', // winding normal
72
+ } );
73
+
57
74
  export const EmissiveContributionResult = struct( {
58
75
  contribution: 'vec3',
59
76
  hasEmissive: 'bool',
@@ -285,6 +302,7 @@ export const TriangleData = struct( {
285
302
  v0: 'vec3', v1: 'vec3', v2: 'vec3',
286
303
  n0: 'vec3', n1: 'vec3', n2: 'vec3',
287
304
  materialIndex: 'int',
305
+ side: 'int', // 0 front, 1 back, 2 double (BVHTraversal.js triangleSide)
288
306
  } );
289
307
 
290
308
  // Fetch triangle vertices from storage buffer
@@ -323,6 +341,7 @@ export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer, bvhBuffe
323
341
  return TriangleData( {
324
342
  v0, v1, v2, n0, n1, n2,
325
343
  materialIndex: int( uvMat.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ),
344
+ side: triangleSide( uvMat.z ),
326
345
  } );
327
346
 
328
347
  } );
@@ -359,14 +378,16 @@ export const calculateEmissiveLightPdf = Fn( ( [
359
378
 
360
379
  // Area: PDF = (power/totalPower) / area, converted to solid angle
361
380
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
362
- const cosLight = max( dot( rayDir.negate(), geoNormal ), 0.001 );
381
+ const cosLight = max( abs( dot( rayDir, geoNormal ) ), 0.001 );
363
382
  const distSq = hitDistance.mul( hitDistance );
364
383
  const pdfArea = selectionPdf.div( area );
365
384
  result.assign( pdfArea.mul( distSq ).div( cosLight ) );
366
385
 
367
386
  } );
368
387
 
369
- return max( result, MIN_PDF );
388
+ // The sampler draws only the sides the triangle emits from (a unit normal: sideAccepts has a threshold).
389
+ const winding = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
390
+ return select( sideAccepts( triData.side, dot( rayDir, winding ) ), max( result, MIN_PDF ), float( 0.0 ) );
370
391
 
371
392
  } );
372
393
 
@@ -378,7 +399,7 @@ export const calculateEmissiveLightPdf = Fn( ( [
378
399
  // CDF values are stored in the .b channel of the emissive buffer.
379
400
  // `emissiveOffset` is the vec4-element offset into the packed light buffer
380
401
  // where emissive entries start (0 if using a non-packed buffer).
381
- const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissiveTriangleCount, rand ] ) => {
402
+ export const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissiveTriangleCount, rand ] ) => {
382
403
 
383
404
  const lo = int( 0 ).toVar();
384
405
  const hi = emissiveTriangleCount.sub( 1 ).toVar();
@@ -407,15 +428,17 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissive
407
428
  // Sample from emissive triangle index using CDF importance sampling.
408
429
  // `emissiveTriangleBuffer` may be the shared packed light buffer; `emissiveVec4Offset`
409
430
  // gives the vec4 offset where emissive entries begin.
410
- export const sampleEmissiveTriangle = Fn( ( [
431
+ const makeSampleEmissiveTriangle = ( indexed ) => Fn( ( [
411
432
  hitPoint, surfaceNormal,
412
433
  rngState,
413
434
  pixelCoord, resolution, frame, dimBase,
414
435
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
415
436
  triangleBuffer, bvhBuffer,
437
+ // The surface passes light through diffusely: a point behind it is as good as one in front.
438
+ throughSurface,
416
439
  ] ) => {
417
440
 
418
- const result = EmissiveSample( {
441
+ const fields = {
419
442
  position: vec3( 0.0 ),
420
443
  normal: vec3( 0.0 ),
421
444
  emission: vec3( 0.0 ),
@@ -425,7 +448,10 @@ export const sampleEmissiveTriangle = Fn( ( [
425
448
  area: float( 0.0 ),
426
449
  cosThetaLight: float( 0.0 ),
427
450
  valid: false,
428
- } ).toVar();
451
+ };
452
+ const result = ( indexed
453
+ ? EmissiveSampleIndexed( { ...fields, triangleIndex: int( - 1 ), faceNormal: vec3( 0.0 ) } )
454
+ : EmissiveSample( fields ) ).toVar();
429
455
 
430
456
  // Check if we have emissive triangles
431
457
  If( emissiveTriangleCount.greaterThan( int( 0 ) ), () => {
@@ -469,9 +495,9 @@ export const sampleEmissiveTriangle = Fn( ( [
469
495
  const samplePos = sphResult.position;
470
496
 
471
497
  const surfaceFacing = dot( dir, surfaceNormal );
472
- const emissiveFacing = dot( dir, geoNormal.negate() );
498
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
473
499
 
474
- If( surfaceFacing.greaterThan( 0.0 ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
500
+ If( surfaceFacing.greaterThan( 0.0 ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ), () => {
475
501
 
476
502
  // Interpolate normal at sampled point via barycentric coords
477
503
  const bary = barycentricFromPoint( samplePos, triData.v0, triData.v1, triData.v2 );
@@ -495,6 +521,12 @@ export const sampleEmissiveTriangle = Fn( ( [
495
521
  result.area.assign( area );
496
522
  result.cosThetaLight.assign( emissiveFacing );
497
523
  result.valid.assign( true );
524
+ if ( indexed ) {
525
+
526
+ result.triangleIndex.assign( triangleIndex );
527
+ result.faceNormal.assign( geoNormal );
528
+
529
+ }
498
530
 
499
531
  } );
500
532
 
@@ -512,9 +544,11 @@ export const sampleEmissiveTriangle = Fn( ( [
512
544
  const dir = toEmissive.div( dist );
513
545
 
514
546
  const surfaceFacing = dot( dir, surfaceNormal );
515
- const emissiveFacing = dot( dir, sampleNormal.negate() );
547
+ // The facet's cosine, not the shading normal's: it is what turns an area density into a solid-angle one.
548
+ const emissiveFacing = abs( dot( dir, geoNormal ) );
516
549
 
517
- If( surfaceFacing.greaterThan( 0.0 ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
550
+ // Edge-on (a two-sided emitter passes its side test there) the density is infinite and the light zero.
551
+ If( surfaceFacing.greaterThan( 0.0 ).or( throughSurface ).and( sideAccepts( triData.side, dot( dir, geoNormal ) ) ).and( emissiveFacing.greaterThan( 0.0 ) ), () => {
518
552
 
519
553
  // PDF: CDF selection (power/totalPower) * uniform area (1/area)
520
554
  // Converted to solid angle: pdfArea * distSq / cosLight
@@ -530,6 +564,12 @@ export const sampleEmissiveTriangle = Fn( ( [
530
564
  result.area.assign( area );
531
565
  result.cosThetaLight.assign( emissiveFacing );
532
566
  result.valid.assign( true );
567
+ if ( indexed ) {
568
+
569
+ result.triangleIndex.assign( triangleIndex );
570
+ result.faceNormal.assign( geoNormal );
571
+
572
+ }
533
573
 
534
574
  } );
535
575
 
@@ -541,6 +581,9 @@ export const sampleEmissiveTriangle = Fn( ( [
541
581
 
542
582
  } );
543
583
 
584
+ export const sampleEmissiveTriangle = /*@__PURE__*/ makeSampleEmissiveTriangle( false );
585
+ export const sampleEmissiveTriangleIndexed = /*@__PURE__*/ makeSampleEmissiveTriangle( true );
586
+
544
587
  // ================================================================================
545
588
  // EMISSIVE TRIANGLE DIRECT LIGHTING CONTRIBUTION
546
589
  // ================================================================================
@@ -573,11 +616,12 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
573
616
  // down-weights BSDF hits deletes emitter energy from deep GI.
574
617
 
575
618
  // Sample emissive triangle (CDF importance-weighted)
619
+ const transmits = material.diffuseTransmission.greaterThan( 0.0 );
576
620
  const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
577
621
  hitPoint, normal, rngState,
578
622
  pixelCoord, resolution, frame, dimBase,
579
623
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
580
- triangleBuffer, bvhBuffer,
624
+ triangleBuffer, bvhBuffer, transmits,
581
625
  ) );
582
626
 
583
627
  If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
@@ -586,10 +630,12 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
586
630
  result.emissionOnly.assign( emissiveSample.emission );
587
631
  result.distance.assign( emissiveSample.distance );
588
632
 
589
- // Check geometric validity
590
- const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
633
+ // Check geometric validity: in front, or behind a surface that passes light through diffusely
634
+ const back = transmits.and( dot( normal, emissiveSample.direction ).lessThan( 0.0 ) )
635
+ .and( dot( emissiveSample.direction, geomNormal ).lessThan( 0.0 ) ).toVar();
636
+ const NoL = abs( dot( normal, emissiveSample.direction ) );
591
637
 
592
- If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ), () => {
638
+ If( back.or( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ) ), () => {
593
639
 
594
640
  // Aimed at the sampled point and stopped a relative hair short, as for area lights.
595
641
  const rayOrigin = hitPoint.add( calculateRayOffsetFn( hitPoint, geomNormal, emissiveSample.direction ) );
@@ -602,8 +648,9 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
602
648
  // Share H + dot products between BRDF eval and PDF (computeDotProducts
603
649
  // would otherwise run twice with identical inputs).
604
650
  const dots = DotProducts.wrap( computeDotProductsAniso( normal, viewDir, emissiveSample.direction, material ) );
605
- const brdfValue = evaluateMaterialResponseFromDots( material, dots );
606
- const brdfPdf = calculateMaterialPDFFromDots( material, dots );
651
+ const brdfValue = select( back, evaluateDiffuseTransmission( material, max( dot( normal, viewDir ), 0.001 ) ), evaluateMaterialResponseFromDots( material, dots ) );
652
+ const brdfPdf = select( back,
653
+ diffuseTransmissionPdf( diffuseTransmissionWeight( material ), NoL ), calculateMaterialPDFFromDots( material, dots ) );
607
654
 
608
655
  // MIS weight: balance light sampling vs BRDF sampling
609
656
  const misWeight = select(
@@ -1,6 +1,6 @@
1
- import { Fn, wgslFn, vec2, vec3, vec4, ivec2, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
1
+ import { Fn, wgslFn, vec2, vec3, ivec2, float, int, If, Loop, sin, sqrt, min, max, clamp, select } from 'three/tsl';
2
2
 
3
- import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
3
+ import { EXACT_TABLE_MAX_WIDTH, GUIDES_PER_ENTRY } from '../Processor/EnvironmentExactTable.js';
4
4
 
5
5
  // Convert direction to UV coordinates for equirectangular map
6
6
  // Exact implementation from three-gpu-pathtracer
@@ -28,125 +28,96 @@ export const equirectUvToDirection = /*@__PURE__*/ wgslFn( `
28
28
  }
29
29
  ` );
30
30
 
31
- // Evaluate PDF for a given direction (for MIS)
32
- // Returns vec4(color.rgb, pdf) since TSL cannot use inout params
33
- // Uses MIS-compensated PDF (Karlík et al. 2019): max(0, lum - delta) / compensatedTotalSum
34
- export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix, envTotalSum, envCompensationDelta, envResolution ] ) => {
31
+ // The table (EnvironmentExactTable.js packExactTable): texel ( x, y ) is row y's entry x — its running sum, the one
32
+ // below it, and the guides of steps 2x and 2x + 1 — and texel ( w, y ) the rows' entry y likewise. Sizes as exactTableSize.
33
+ const exactTable = ( cdfTexture, envResolution ) => {
35
34
 
36
- const result = vec4( 0.0 ).toVar();
35
+ const W = int( envResolution.x );
36
+ const H = int( envResolution.y );
37
+ const k = max( W.add( int( EXACT_TABLE_MAX_WIDTH - 1 ) ).div( int( EXACT_TABLE_MAX_WIDTH ) ), int( 1 ) );
38
+ const w = W.add( k ).sub( int( 1 ) ).div( k ).toVar();
39
+ const h = H.add( k ).sub( int( 1 ) ).div( k ).toVar();
40
+ return { w, h, entry: ( x, y ) => cdfTexture.load( ivec2( x, y ) ) };
37
41
 
38
- If( envTotalSum.equal( 0.0 ), () => {
42
+ };
39
43
 
40
- // Exclude black environments from MIS
41
- result.assign( vec4( 0.0 ) );
44
+ // The first of `count` entries whose running sum is above target: the guide of target's step names it at once unless
45
+ // the step holds more than one entry boundary, or f32 rounding of target × steps picked the step beside it; then a
46
+ // search from one entry below the guide to the guide two steps on. Returns the entry with its sum and the one below.
47
+ const invert = ( count, entryAt, target ) => {
42
48
 
43
- } ).Else( () => {
49
+ const steps = count.mul( int( GUIDES_PER_ENTRY ) ).toVar();
50
+ const step = clamp( int( target.mul( float( steps ) ) ), int( 0 ), steps.sub( int( 1 ) ) ).toVar();
51
+ const odd = step.bitAnd( int( 1 ) ).equal( int( 1 ) ).toVar();
52
+ const guides = entryAt( step.shiftRight( int( 1 ) ) ).toVar();
53
+ const index = int( select( odd, guides.w, guides.z ) ).toVar();
54
+ const e = entryAt( index ).toVar();
55
+ If( target.lessThan( e.y ).or( target.greaterThanEqual( e.x ).and( index.lessThan( count.sub( int( 1 ) ) ) ) ), () => {
44
56
 
45
- const uv = equirectDirectionToUv( { direction, environmentMatrix } ).toVar();
46
- const color = texture( environment, uv, 0 ).rgb.toVar();
57
+ const lo = max( index.sub( int( 1 ) ), int( 0 ) ).toVar();
58
+ const ahead = entryAt( step.shiftRight( int( 1 ) ).add( int( 1 ) ) );
59
+ const hi = select( step.add( int( 2 ) ).lessThan( steps ), int( select( odd, ahead.w, ahead.z ) ), count.sub( int( 1 ) ) ).toVar();
60
+ Loop( lo.lessThan( hi ), () => {
47
61
 
48
- // sin(theta) matches the CDF's solid-angle weighting (lum * sinTheta)
49
- const sinTheta = sin( uv.y.mul( Math.PI ) ).toVar();
50
- const lum = dot( color, REC709_LUMINANCE_COEFFICIENTS );
51
- const weightedLum = lum.mul( sinTheta );
52
- // MIS Compensation: subtract delta to match the sharpened CDF
53
- const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) );
54
- const pdf = compensatedWeight.div( envTotalSum );
62
+ const mid = lo.add( hi ).div( 2 ).toVar();
63
+ If( entryAt( mid ).x.lessThanEqual( target ), () => {
55
64
 
56
- // Inline equirectDirectionPdf using the uv + sinTheta already in scope —
57
- // the helper would otherwise re-derive uv via atan2+acos and recompute sin.
58
- const dirPdf = sinTheta.greaterThan( 0.0 ).select(
59
- float( 1.0 ).div( float( 2.0 * Math.PI * Math.PI ).mul( sinTheta ) ),
60
- float( 0.0 )
61
- );
62
- const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf );
65
+ lo.assign( mid.add( 1 ) );
63
66
 
64
- result.assign( vec4( color, finalPdf ) );
67
+ } ).Else( () => {
68
+
69
+ hi.assign( mid );
70
+
71
+ } );
72
+
73
+ } );
74
+ index.assign( lo );
75
+ e.assign( entryAt( lo ) );
65
76
 
66
77
  } );
78
+ return { index, top: e.x, bottom: e.y };
67
79
 
68
- return result;
80
+ };
69
81
 
70
- } );
82
+ // Cell density over uv, per steradian at v.
83
+ const cellPdf = ( share, w, h, v ) => {
71
84
 
72
- // Sample environment map using importance sampling
73
- // Returns vec4(direction.xyz, pdf). Optionally writes sampled color to colorOutput.
74
- // Exact implementation from three-gpu-pathtracer
75
- export const sampleEquirectProbability = Fn( ( [
76
- environment,
77
- envCDFTexture,
78
- environmentMatrix,
79
- environmentIntensity,
80
- envTotalSum,
81
- envCompensationDelta,
82
- envResolution,
83
- r,
84
- colorOutput
85
- ] ) => {
86
-
87
- // CDF texture layout: (W+1)×H R32F — conditional[cy*W+cx] at texel (cx,cy); marginal[cy] at column W.
88
- const cdfMarginalCol = int( envResolution.x ).toVar();
89
-
90
- // Sample marginal CDF for V coordinate (1D, linear interpolation)
91
- const marginalSize = envResolution.y;
92
- const mIdx = clamp( r.x.mul( marginalSize.sub( 1.0 ) ), 0.0, marginalSize.sub( 1.0 ) );
93
- const mI0 = int( floor( mIdx ) );
94
- const mI1 = min( mI0.add( 1 ), int( marginalSize ).sub( 1 ) );
95
- const mFrac = fract( mIdx );
96
- const v = mix(
97
- envCDFTexture.load( ivec2( cdfMarginalCol, mI0 ) ).x,
98
- envCDFTexture.load( ivec2( cdfMarginalCol, mI1 ) ).x,
99
- mFrac,
100
- ).toVar();
101
-
102
- // Sample conditional CDF for U coordinate (2D grid, bilinear interpolation)
103
- const condW = envResolution.x;
104
- const condH = envResolution.y;
105
- const cxf = clamp( r.y.mul( condW.sub( 1.0 ) ), 0.0, condW.sub( 1.0 ) );
106
- const cyf = clamp( v.mul( condH.sub( 1.0 ) ), 0.0, condH.sub( 1.0 ) );
107
- const cx0 = int( floor( cxf ) );
108
- const cy0 = int( floor( cyf ) );
109
- const cx1 = min( cx0.add( 1 ), int( condW ).sub( 1 ) );
110
- const cy1 = min( cy0.add( 1 ), int( condH ).sub( 1 ) );
111
- const fx = fract( cxf );
112
- const fy = fract( cyf );
113
- const v00 = envCDFTexture.load( ivec2( cx0, cy0 ) ).x;
114
- const v10 = envCDFTexture.load( ivec2( cx1, cy0 ) ).x;
115
- const v01 = envCDFTexture.load( ivec2( cx0, cy1 ) ).x;
116
- const v11 = envCDFTexture.load( ivec2( cx1, cy1 ) ).x;
117
- const u = mix( mix( v00, v10, fx ), mix( v01, v11, fx ), fy ).toVar();
118
-
119
- const uv = vec2( u, v ).toVar();
120
-
121
- // Convert UV to direction
122
- const direction = equirectUvToDirection( { uv, environmentMatrix } ).toVar();
123
-
124
- // Sample color
125
- const color = texture( environment, uv, 0 ).rgb.mul( environmentIntensity ).toVar();
126
-
127
- // Write color to output parameter (avoids redundant CDF texture lookups)
128
- colorOutput.assign( color );
129
-
130
- // Calculate PDF — sin(theta) weighting + MIS Compensation (Karlík et al. 2019)
131
- const sinTheta = sin( uv.y.mul( Math.PI ) ).toVar();
132
- const lum = dot( color.div( environmentIntensity ), REC709_LUMINANCE_COEFFICIENTS );
133
- const weightedLum = lum.mul( sinTheta );
134
- const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) );
135
- const pdf = compensatedWeight.div( envTotalSum );
136
-
137
- // Inline equirectDirectionPdf — uv + sinTheta are already in scope, so we
138
- // skip the helper's redundant uv-from-direction + sin recompute.
139
- const dirPdf = sinTheta.greaterThan( 0.0 ).select(
140
- float( 1.0 ).div( float( 2.0 * Math.PI * Math.PI ).mul( sinTheta ) ),
141
- float( 0.0 )
142
- );
143
- const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf );
144
-
145
- return vec4( direction, finalPdf );
85
+ const sinTheta = sin( v.mul( Math.PI ) );
86
+ return select( sinTheta.greaterThan( 0.0 ).and( share.greaterThan( 0.0 ) ),
87
+ share.mul( float( w ) ).mul( float( h ) ).div( max( sinTheta.mul( 2 * Math.PI * Math.PI ), 1e-30 ) ), float( 0.0 ) );
146
88
 
147
- } );
89
+ };
90
+
91
+ /** A direction drawn exactly from the table, and its density per steradian. */
92
+ export function sampleEnvironmentExact( cdfTexture, environmentMatrix, envResolution, xi ) {
93
+
94
+ const { w, h, entry } = exactTable( cdfTexture, envResolution );
95
+ const row = invert( h, ( i ) => entry( w, i ), xi.y );
96
+ const y = row.index.toVar();
97
+ const cell = invert( w, ( i ) => entry( i, y ), xi.x );
98
+ const pRow = row.top.sub( row.bottom ).toVar();
99
+ const pCell = cell.top.sub( cell.bottom ).toVar();
100
+
101
+ const fv = clamp( xi.y.sub( row.bottom ).div( max( pRow, 1e-30 ) ), 0.0, 0.99999994 );
102
+ const fu = clamp( xi.x.sub( cell.bottom ).div( max( pCell, 1e-30 ) ), 0.0, 0.99999994 );
103
+ const uv = vec2( float( cell.index ).add( fu ).div( float( w ) ), float( y ).add( fv ).div( float( h ) ) ).toVar();
104
+
105
+ return { direction: equirectUvToDirection( { uv, environmentMatrix } ), pdf: cellPdf( pRow.mul( pCell ), w, h, uv.y ) };
106
+
107
+ }
108
+
109
+ /** sampleEnvironmentExact's density per steradian for `direction`. */
110
+ export function environmentPdfExact( cdfTexture, environmentMatrix, envResolution, direction ) {
111
+
112
+ const { w, h, entry } = exactTable( cdfTexture, envResolution );
113
+ const uv = equirectDirectionToUv( { direction, environmentMatrix } ).toVar();
114
+ const x = clamp( int( uv.x.mul( float( w ) ) ), int( 0 ), w.sub( int( 1 ) ) ).toVar();
115
+ const y = clamp( int( uv.y.mul( float( h ) ) ), int( 0 ), h.sub( int( 1 ) ) ).toVar();
116
+ const rowEntry = entry( w, y ).toVar();
117
+ const cellEntry = entry( x, y ).toVar();
118
+ return cellPdf( rowEntry.x.sub( rowEntry.y ).mul( cellEntry.x.sub( cellEntry.y ) ), w, h, uv.y );
148
119
 
149
- // Note: powerHeuristic() is defined in Common.js
120
+ }
150
121
 
151
122
  // Simple environment lookup (no importance sampling) — native WGSL
152
123
  export const sampleEnvironment = /*@__PURE__*/ wgslFn( `