rayzee 9.4.1 → 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 (145) hide show
  1. package/README.md +226 -25
  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 -44125
  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 +23 -14
  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/SimpleSky.js +1 -62
  74. package/src/Processor/SolarLimb.js +3 -0
  75. package/src/Processor/TextureCreator.js +1 -1
  76. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  77. package/src/Processor/Workers/BVHWorker.js +5 -18
  78. package/src/Processor/Workers/CDFWorker.js +6 -183
  79. package/src/Processor/archiveFormats.js +7 -0
  80. package/src/Processor/utils.js +2 -1
  81. package/src/RayzeeRenderer.js +3902 -0
  82. package/src/RenderSettings.js +49 -41
  83. package/src/Stages/ASVGF.js +5 -7
  84. package/src/Stages/AutoExposure.js +3 -12
  85. package/src/Stages/Compositor.js +8 -23
  86. package/src/Stages/NRD.js +9 -2
  87. package/src/Stages/NormalDepth.js +9 -6
  88. package/src/Stages/PathTracer.js +254 -38
  89. package/src/Stages/PathTracerStage.js +11 -16
  90. package/src/Storage/BLASCache.js +1 -1
  91. package/src/Storage/CDFCache.js +18 -17
  92. package/src/Storage/DownloadCache.js +1 -1
  93. package/src/Storage/SceneGraphCodec.js +46 -3
  94. package/src/Storage/SpillStore.js +1 -1
  95. package/src/Storage/StorageManager.js +2 -4
  96. package/src/Storage/areas.js +4 -0
  97. package/src/TSL/BVHTraversal.js +62 -24
  98. package/src/TSL/Bidirectional.js +204 -0
  99. package/src/TSL/BidirectionalLamps.js +132 -0
  100. package/src/TSL/Clearcoat.js +15 -6
  101. package/src/TSL/Common.js +19 -14
  102. package/src/TSL/ConnectKernel.js +147 -0
  103. package/src/TSL/Debugger.js +1 -1
  104. package/src/TSL/Displacement.js +4 -3
  105. package/src/TSL/EmissiveSampling.js +64 -17
  106. package/src/TSL/Environment.js +77 -106
  107. package/src/TSL/EnvironmentCDF.js +85 -39
  108. package/src/TSL/ExtendKernel.js +8 -3
  109. package/src/TSL/FinalWriteKernel.js +4 -4
  110. package/src/TSL/GenerateKernel.js +18 -4
  111. package/src/TSL/HitFacet.js +35 -5
  112. package/src/TSL/LightBVHSampling.js +40 -9
  113. package/src/TSL/LightGenerateKernel.js +293 -0
  114. package/src/TSL/LightGuide.js +198 -0
  115. package/src/TSL/LightSplatKernel.js +165 -0
  116. package/src/TSL/LightsCore.js +13 -38
  117. package/src/TSL/LightsDirect.js +28 -29
  118. package/src/TSL/LightsIndirect.js +3 -1
  119. package/src/TSL/LightsSampling.js +90 -38
  120. package/src/TSL/MaterialEvaluation.js +113 -31
  121. package/src/TSL/MaterialProperties.js +44 -17
  122. package/src/TSL/MaterialTransmission.js +52 -47
  123. package/src/TSL/MergeKernel.js +218 -0
  124. package/src/TSL/PathTracerCore.js +65 -29
  125. package/src/TSL/Random.js +4 -0
  126. package/src/TSL/SceneResources.js +51 -0
  127. package/src/TSL/ShadeKernel.js +881 -103
  128. package/src/TSL/Struct.js +10 -0
  129. package/src/TSL/Sun.js +1 -1
  130. package/src/TSL/TextureSampling.js +65 -60
  131. package/src/addons/archives.js +14 -0
  132. package/src/addons/bidirectional.js +13 -0
  133. package/src/addons/color.js +32 -0
  134. package/src/addons/formats.js +16 -0
  135. package/src/addons/physicalSky.js +14 -0
  136. package/src/addons/storage.js +19 -0
  137. package/src/core.js +35 -0
  138. package/src/index.js +3 -1
  139. package/src/integrators/BidirectionalIntegrator.js +832 -0
  140. package/src/managers/EnvironmentManager.js +94 -91
  141. package/src/managers/MaterialDataManager.js +42 -0
  142. package/src/managers/UniformManager.js +0 -1
  143. package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
  144. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  145. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -311,8 +311,7 @@ function transmittanceSpectrum( coefficients, depth ) {
311
311
 
312
312
  }
313
313
 
314
- // Hestroffer & Magnan 1998: I(µ)/I(1) = µ^α, α = −0.023 + 0.292 / λ[µm], at each primary's centroid.
315
- export const LIMB_DARKENING_EXPONENT = [ 610, 550, 465 ].map( l => - 0.023 + 0.292 / ( l / 1000 ) );
314
+ export { LIMB_DARKENING_EXPONENT } from './SolarLimb.js';
316
315
 
317
316
  /** Solid angle of a cone of the given half-angle, without the cancellation `2π(1 − cos)` suffers. */
318
317
  export function coneSolidAngle( halfAngle ) {
@@ -1,4 +1,3 @@
1
- import { TreeletOptimizer } from './TreeletOptimizer.js';
2
1
  import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
3
2
  // Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
4
3
  import { createLogger, fmt } from '../utils/Logger.js';
@@ -104,23 +103,11 @@ export class BVHBuilder {
104
103
  totalSplitAttempts: 0,
105
104
  mortonSortTime: 0,
106
105
  totalBuildTime: 0,
107
- treeletOptimizationTime: 0,
108
- treeletsProcessed: 0,
109
- treeletsImproved: 0,
110
- averageSAHImprovement: 0,
111
106
  reinsertionOptimizationTime: 0,
112
107
  reinsertionsApplied: 0,
113
108
  reinsertionIterations: 0
114
109
  };
115
110
 
116
- // Off: repairs LBVH trees, so on this binned-SAH build it buys 0.005% tree SAH for 72%
117
- // of build time.
118
- this.enableTreeletOptimization = false;
119
- this.treeletSize = 5;
120
- this.treeletOptimizationPasses = 1;
121
- this.treeletMinImprovement = 0.02;
122
- this.treeletComplexityThreshold = 50000;
123
-
124
111
  // Reinsertion optimization configuration
125
112
  this.enableReinsertionOptimization = true;
126
113
  this.reinsertionBatchSizeRatio = 0.02;
@@ -201,22 +188,6 @@ export class BVHBuilder {
201
188
 
202
189
  }
203
190
 
204
- setTreeletConfig( config ) {
205
-
206
- if ( config.enabled !== undefined ) this.enableTreeletOptimization = config.enabled;
207
- if ( config.size !== undefined ) this.treeletSize = Math.max( 3, Math.min( 12, config.size ) );
208
- if ( config.passes !== undefined ) this.treeletOptimizationPasses = Math.max( 1, Math.min( 3, config.passes ) );
209
- if ( config.minImprovement !== undefined ) this.treeletMinImprovement = Math.max( 0.001, config.minImprovement );
210
- if ( config.complexityThreshold !== undefined ) this.treeletComplexityThreshold = config.complexityThreshold;
211
-
212
- }
213
-
214
- disableTreeletOptimization() {
215
-
216
- this.enableTreeletOptimization = false;
217
-
218
- }
219
-
220
191
  setReinsertionConfig( config ) {
221
192
 
222
193
  if ( config.enabled !== undefined ) this.enableReinsertionOptimization = config.enabled;
@@ -442,7 +413,7 @@ export class BVHBuilder {
442
413
 
443
414
  worker.onmessage = ( e ) => {
444
415
 
445
- const { bvhData, triangles: transferredTriangles, originalToBvh, error, progress, treeletStats } = e.data;
416
+ const { bvhData, triangles: transferredTriangles, originalToBvh, error, progress, splitStats } = e.data;
446
417
 
447
418
  if ( error ) {
448
419
 
@@ -459,9 +430,9 @@ export class BVHBuilder {
459
430
 
460
431
  }
461
432
 
462
- if ( treeletStats ) {
433
+ if ( splitStats ) {
463
434
 
464
- this.splitStats = treeletStats;
435
+ this.splitStats = splitStats;
465
436
 
466
437
  }
467
438
 
@@ -493,12 +464,6 @@ export class BVHBuilder {
493
464
  depth,
494
465
  reportProgress: !! progressCallback,
495
466
  sharedReorderBuffer,
496
- treeletOptimization: {
497
- enabled: this.enableTreeletOptimization,
498
- size: this.treeletSize,
499
- passes: this.treeletOptimizationPasses,
500
- minImprovement: this.treeletMinImprovement
501
- },
502
467
  reinsertionOptimization: {
503
468
  enabled: this.enableReinsertionOptimization,
504
469
  batchSizeRatio: this.reinsertionBatchSizeRatio,
@@ -571,10 +536,6 @@ export class BVHBuilder {
571
536
  totalSplitAttempts: 0,
572
537
  mortonSortTime: 0,
573
538
  totalBuildTime: 0,
574
- treeletOptimizationTime: 0,
575
- treeletsProcessed: 0,
576
- treeletsImproved: 0,
577
- averageSAHImprovement: 0,
578
539
  reinsertionOptimizationTime: 0,
579
540
  reinsertionsApplied: 0,
580
541
  reinsertionIterations: 0,
@@ -608,58 +569,7 @@ export class BVHBuilder {
608
569
  const root = this.buildNodeRecursive( 0, n, depth, progressCallback );
609
570
  this.splitStats.sahBuildTime = performance.now() - sahStart;
610
571
 
611
- // Phase 4: Treelet optimization
612
- if ( this.enableTreeletOptimization && this.totalTriangles > 1000 ) {
613
-
614
- const isLargeScene = this.totalTriangles > this.treeletComplexityThreshold;
615
- const adaptiveTreeletSize = isLargeScene ? 3 : this.treeletSize;
616
-
617
- const optimizer = new TreeletOptimizer( this.traversalCost, this.intersectionCost );
618
- optimizer.setTreeletSize( adaptiveTreeletSize );
619
- optimizer.setMinImprovement( this.treeletMinImprovement );
620
-
621
- const optimizationStartTime = performance.now();
622
-
623
- for ( let pass = 0; pass < this.treeletOptimizationPasses; pass ++ ) {
624
-
625
- const passCallback = progressCallback ? ( status ) => {
626
-
627
- progressCallback( `Treelet optimization pass ${pass + 1}/${this.treeletOptimizationPasses}: ${status}` );
628
-
629
- } : null;
630
-
631
- try {
632
-
633
- optimizer.optimizeBVH( root, passCallback );
634
-
635
- } catch ( error ) {
636
-
637
- log.error( `treelet optimizer failed in pass ${pass + 1}:`, error );
638
- break;
639
-
640
- }
641
-
642
- // optimizeBVH resets stats internally, so afterStats reflects this pass only
643
- const afterStats = optimizer.getStatistics();
644
- const passTime = performance.now() - optimizationStartTime;
645
- if ( ( afterStats.treeletsImproved === 0 && pass > 0 ) || passTime > 15000 ) {
646
-
647
- break;
648
-
649
- }
650
-
651
- }
652
-
653
- const treeletTime = performance.now() - optimizationStartTime;
654
- this.splitStats.treeletOptimizationTime = treeletTime;
655
- const treeletStats = optimizer.getStatistics();
656
- this.splitStats.treeletsProcessed = treeletStats.treeletsProcessed;
657
- this.splitStats.treeletsImproved = treeletStats.treeletsImproved;
658
- this.splitStats.averageSAHImprovement = treeletStats.averageSAHImprovement;
659
-
660
- }
661
-
662
- // Phase 4b: Reinsertion optimization (Meister & Bittner)
572
+ // Phase 4: Reinsertion optimization (Meister & Bittner)
663
573
  if ( this.enableReinsertionOptimization && this.totalTriangles > 1000 ) {
664
574
 
665
575
  const reinsertionOptimizer = new ReinsertionOptimizer( this.traversalCost, this.intersectionCost );
@@ -728,7 +638,6 @@ export class BVHBuilder {
728
638
  log.debug(
729
639
  `${fmt.n( n )} tris → ${fmt.n( this.totalNodes )} nodes in ${fmt.ms( total )}` +
730
640
  ` | SAH ${s.sahSplits} objMed ${s.objectMedianSplits} spatMed ${s.spatialMedianSplits} failed ${s.failedSplits}` +
731
- ( s.treeletsProcessed ? ` | treelets ${s.treeletsImproved}/${s.treeletsProcessed} improved` : '' ) +
732
641
  ( s.reinsertionsApplied ? ` | reinsertions ${s.reinsertionsApplied}` : '' )
733
642
  );
734
643
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * An environment's sampling table: piecewise constant over cells of at most EXACT_TABLE_MAX_WIDTH × half that,
3
+ * so a direction's density is its cell's share, read back from the same cumulative sums it is drawn from.
4
+ * MIS-compensated (Karlík et al. 2019): each texel weighs what it has above the mean, and BSDF-sampled rays
5
+ * cover the rest. Drawn by inverting the sums, which keeps the samples' stratification (an alias table does not:
6
+ * it doubled a furnace's noise). Two guides per entry (Chen & Hsu's cutpoints, GUIDES_PER_ENTRY) land most
7
+ * draws on their entry at once. Pure math: CDFWorker runs it too.
8
+ */
9
+
10
+ export const EXACT_TABLE_MAX_WIDTH = 1024;
11
+
12
+ // Every cell keeps this share of the mean, so light tracing or NEE alone covers the whole sphere.
13
+ const FLOOR = 1e-4;
14
+
15
+ /** Guides per running-sum entry: step g of n · GUIDES_PER_ENTRY points at the first entry above it. */
16
+ export const GUIDES_PER_ENTRY = 2;
17
+
18
+ /** Cells the table has for a width × height environment; TSL/Environment.js derives the same. */
19
+ export function exactTableSize( width, height ) {
20
+
21
+ const k = Math.max( 1, Math.ceil( width / EXACT_TABLE_MAX_WIDTH ) );
22
+ return { k, width: Math.ceil( width / k ), height: Math.ceil( height / k ) };
23
+
24
+ }
25
+
26
+ // For each of m equal steps of an n-entry running sum, the first entry above the step's start; n − 1 past the end.
27
+ function cutpoints( cdf, offset, n, out, outOffset, m ) {
28
+
29
+ let x = 0;
30
+ for ( let g = 0; g < m; g ++ ) {
31
+
32
+ while ( x < n - 1 && cdf[ offset + x ] <= g / m ) x ++;
33
+ out[ outOffset + g ] = x;
34
+
35
+ }
36
+
37
+ }
38
+
39
+ /**
40
+ * @param {Float32Array} floatData - RGBA, row 0 the nadir
41
+ * @param {{ filtered?: boolean }} [options] - filtered false: each texel weighs as itself (the physical sky's GPU twin)
42
+ * @returns {{ exactWidth: number, exactHeight: number, exactConditional: Float32Array, exactMarginal: Float32Array,
43
+ * exactRowGuide: Float32Array, exactMarginalGuide: Float32Array, radianceIntegral: number }}
44
+ * each row's running sum over its cells and the rows' over all, both ending at 1, and their guides (GUIDES_PER_ENTRY
45
+ * an entry); ∫ luminance dω.
46
+ */
47
+ export function buildExactEnvironmentTable( floatData, width, height, { filtered = true } = {} ) {
48
+
49
+ const { k, width: w, height: h } = exactTableSize( width, height );
50
+ const lum = new Float32Array( width * height );
51
+ let raw = 0;
52
+ for ( let y = 0; y < height; y ++ ) {
53
+
54
+ const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
55
+ for ( let x = 0; x < width; x ++ ) {
56
+
57
+ const i = 4 * ( y * width + x );
58
+ const l = 0.2126 * floatData[ i ] + 0.7152 * floatData[ i + 1 ] + 0.0722 * floatData[ i + 2 ];
59
+ lum[ y * width + x ] = Number.isFinite( l ) && l > 0 ? l : 0;
60
+ raw += lum[ y * width + x ] * sinTheta;
61
+
62
+ }
63
+
64
+ }
65
+
66
+ // Each texel weighs as the bilinear filter's mean over it — 1/8, 6/8, 1/8 of its neighbours along each axis — so
67
+ // a cell has weight wherever the filtered map has light, and a sharp texel's neighbours only their share of it.
68
+ const TAP = [ 1 / 8, 6 / 8, 1 / 8 ];
69
+ const eachTexel = ( visit ) => {
70
+
71
+ for ( let y = 0; y < height; y ++ ) {
72
+
73
+ const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
74
+ const row = Math.floor( y / k ) * w;
75
+ const ys = [ Math.max( y - 1, 0 ), y, Math.min( y + 1, height - 1 ) ];
76
+ for ( let x = 0; x < width; x ++ ) {
77
+
78
+ let value = lum[ y * width + x ];
79
+ if ( filtered ) {
80
+
81
+ const xs = [ ( x + width - 1 ) % width, x, ( x + 1 ) % width ];
82
+ value = 0;
83
+ for ( let j = 0; j < 3; j ++ ) for ( let i = 0; i < 3; i ++ ) value += TAP[ j ] * TAP[ i ] * lum[ ys[ j ] * width + xs[ i ] ];
84
+
85
+ }
86
+
87
+ visit( row + Math.floor( x / k ), value * sinTheta );
88
+
89
+ }
90
+
91
+ }
92
+
93
+ };
94
+
95
+ let filteredTotal = 0;
96
+ eachTexel( ( cell, weight ) => void ( filteredTotal += weight ) );
97
+ const mean = filteredTotal / ( width * height );
98
+ const cells = new Float64Array( w * h );
99
+ let compensated = 0;
100
+ eachTexel( ( cell, weight ) => {
101
+
102
+ cells[ cell ] += Math.max( weight - mean, 0 );
103
+ compensated += Math.max( weight - mean, 0 );
104
+
105
+ } );
106
+ // A flat map has nothing above its mean: then the raw weights.
107
+ if ( ! ( compensated > 0 ) ) {
108
+
109
+ cells.fill( 0 );
110
+ eachTexel( ( cell, weight ) => void ( cells[ cell ] += weight ) );
111
+
112
+ }
113
+
114
+ const floor = filteredTotal > 0 ? FLOOR * filteredTotal / ( w * h ) : 0;
115
+ const exactConditional = new Float32Array( w * h );
116
+ const exactMarginal = new Float32Array( h );
117
+ const rowSums = new Float64Array( h );
118
+ let total = 0;
119
+ for ( let y = 0; y < h; y ++ ) {
120
+
121
+ let sum = 0;
122
+ for ( let x = 0; x < w; x ++ ) sum += cells[ y * w + x ] + floor;
123
+ let cumulative = 0;
124
+ for ( let x = 0; x < w; x ++ ) {
125
+
126
+ cumulative += cells[ y * w + x ] + floor;
127
+ exactConditional[ y * w + x ] = sum > 0 ? ( x === w - 1 ? 1 : cumulative / sum ) : 0;
128
+
129
+ }
130
+
131
+ rowSums[ y ] = sum;
132
+ total += sum;
133
+
134
+ }
135
+
136
+ let cumulative = 0;
137
+ for ( let y = 0; y < h; y ++ ) {
138
+
139
+ cumulative += rowSums[ y ];
140
+ exactMarginal[ y ] = total > 0 ? ( y === h - 1 ? 1 : cumulative / total ) : 0;
141
+
142
+ }
143
+
144
+ const G = GUIDES_PER_ENTRY;
145
+ const exactRowGuide = new Float32Array( G * w * h );
146
+ const exactMarginalGuide = new Float32Array( G * h );
147
+ for ( let y = 0; y < h; y ++ ) cutpoints( exactConditional, y * w, w, exactRowGuide, G * y * w, G * w );
148
+ cutpoints( exactMarginal, 0, h, exactMarginalGuide, 0, G * h );
149
+
150
+ return {
151
+ exactWidth: w, exactHeight: h, exactConditional, exactMarginal, exactRowGuide, exactMarginalGuide,
152
+ radianceIntegral: 2 * Math.PI * Math.PI * raw / ( width * height ),
153
+ };
154
+
155
+ }
156
+
157
+ /**
158
+ * The table as the CDF texture holds it, ( w + 1 ) × h RGBA floats: texel ( x, y ) holds row y's entry x — its running
159
+ * sum, the one below it (0 at the first), and the guides of steps 2x and 2x + 1 — and texel ( w, y ) the rows' entry y
160
+ * likewise. TSL/Environment.js reads this layout.
161
+ */
162
+ export function packExactTable( { exactWidth: w, exactHeight: h, exactConditional, exactMarginal, exactRowGuide, exactMarginalGuide } ) {
163
+
164
+ const stride = w + 1;
165
+ const data = new Float32Array( 4 * stride * h );
166
+ for ( let y = 0; y < h; y ++ ) {
167
+
168
+ let o = 4 * y * stride;
169
+ for ( let x = 0, i = y * w; x < w; x ++, i ++, o += 4 ) {
170
+
171
+ data[ o ] = exactConditional[ i ];
172
+ data[ o + 1 ] = x > 0 ? exactConditional[ i - 1 ] : 0;
173
+ data[ o + 2 ] = exactRowGuide[ 2 * i ];
174
+ data[ o + 3 ] = exactRowGuide[ 2 * i + 1 ];
175
+
176
+ }
177
+
178
+ data[ o ] = exactMarginal[ y ];
179
+ data[ o + 1 ] = y > 0 ? exactMarginal[ y - 1 ] : 0;
180
+ data[ o + 2 ] = exactMarginalGuide[ 2 * y ];
181
+ data[ o + 3 ] = exactMarginalGuide[ 2 * y + 1 ];
182
+
183
+ }
184
+
185
+ return { data, width: stride, height: h };
186
+
187
+ }
@@ -1,44 +1,7 @@
1
1
  import { DataUtils, HalfFloatType, FloatType, SRGBColorSpace } from 'three';
2
2
  import CDFWorker from './Workers/CDFWorker.js?worker&inline';
3
3
  import { createWorker } from '../Platform.js';
4
-
5
- /**
6
- * Binary search to find the closest index
7
- */
8
- function binarySearchFindClosestIndexOf( array, targetValue, offset = 0, count = array.length ) {
9
-
10
- let lower = offset;
11
- let upper = offset + count - 1;
12
-
13
- while ( lower < upper ) {
14
-
15
- const mid = ( lower + upper ) >> 1;
16
-
17
- if ( array[ mid ] < targetValue ) {
18
-
19
- lower = mid + 1;
20
-
21
- } else {
22
-
23
- upper = mid;
24
-
25
- }
26
-
27
- }
28
-
29
- return lower - offset;
30
-
31
- }
32
-
33
- /**
34
- * Calculate luminance from RGB values
35
- */
36
- function colorToLuminance( r, g, b ) {
37
-
38
- // https://en.wikipedia.org/wiki/Relative_luminance
39
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
40
-
41
- }
4
+ import { buildExactEnvironmentTable } from './EnvironmentExactTable.js';
42
5
 
43
6
  /**
44
7
  * sRGB to linear conversion (IEC 61966-2-1 transfer function)
@@ -154,27 +117,24 @@ export function extractFloatData( envMap ) {
154
117
  }
155
118
 
156
119
  /**
157
- * EquirectHDRInfo - Importance sampling data for equirectangular HDR maps
158
- *
159
- * Builds inverted marginal and conditional CDFs from an HDR environment map.
160
- * Outputs Float32Arrays consumed directly by StorageInstancedBufferAttribute
161
- * on the GPU — no intermediate DataTexture or HalfFloat conversion needed.
162
- *
163
- * Supports two modes:
164
- * - `updateFrom(hdr)`: synchronous, runs on main thread
165
- * - `updateFromAsync(hdr)`: offloads CDF math to a Web Worker
120
+ * EquirectHDRInfo - an equirectangular environment's sampling table (EnvironmentExactTable.js), built on the
121
+ * main thread (`updateFrom`) or in CDFWorker (`updateFromAsync`). `totalSum` is the map's ∫ luminance dω, 0
122
+ * when there is nothing to sample.
166
123
  */
167
124
  export class EquirectHDRInfo {
168
125
 
169
126
  constructor() {
170
127
 
171
- // Placeholder data matching the default storage buffer sizes in PathTracer
172
- this.marginalData = new Float32Array( [ 0, 1 ] );
173
- this.conditionalData = new Float32Array( [ 0, 0, 1, 1 ] );
174
128
  this.totalSum = 0;
175
- this.compensationDelta = 0;
176
129
  this.width = 0;
177
130
  this.height = 0;
131
+ this.exactConditional = null;
132
+ this.exactMarginal = null;
133
+ this.exactRowGuide = null;
134
+ this.exactMarginalGuide = null;
135
+ this.exactWidth = 0;
136
+ this.exactHeight = 0;
137
+ this.radianceIntegral = 0;
178
138
 
179
139
  this._worker = null;
180
140
 
@@ -182,8 +142,10 @@ export class EquirectHDRInfo {
182
142
 
183
143
  dispose() {
184
144
 
185
- this.marginalData = null;
186
- this.conditionalData = null;
145
+ this.exactConditional = null;
146
+ this.exactMarginal = null;
147
+ this.exactRowGuide = null;
148
+ this.exactMarginalGuide = null;
187
149
 
188
150
  if ( this._worker ) {
189
151
 
@@ -194,28 +156,28 @@ export class EquirectHDRInfo {
194
156
 
195
157
  }
196
158
 
159
+ _adopt( table, width, height ) {
160
+
161
+ Object.assign( this, table );
162
+ this.width = width;
163
+ this.height = height;
164
+ this.totalSum = table.radianceIntegral;
165
+
166
+ }
167
+
197
168
  /**
198
- * Synchronous CDF build on main thread (fallback path).
169
+ * Synchronous build on the main thread (fallback path).
199
170
  */
200
171
  updateFrom( hdr ) {
201
172
 
202
173
  const { floatData, width, height } = extractFloatData( hdr );
203
-
204
- const result = EquirectHDRInfo.computeCDF( floatData, width, height );
205
-
206
- this.marginalData = result.marginalData;
207
- this.conditionalData = result.conditionalData;
208
- this.totalSum = result.totalSum;
209
- this.compensationDelta = result.compensationDelta;
210
- this.width = width;
211
- this.height = height;
174
+ this._adopt( buildExactEnvironmentTable( floatData, width, height ), width, height );
212
175
 
213
176
  }
214
177
 
215
178
  /**
216
- * Async CDF build offloaded to a Web Worker.
217
- * Float extraction (HalfFloat → Float32) runs on main thread (needs Three.js DataUtils),
218
- * then the pure-math CDF computation runs off-thread.
179
+ * The build offloaded to a Web Worker. Float extraction (HalfFloat → Float32) runs on the main thread
180
+ * (it needs three's DataUtils); the table's math runs off it.
219
181
  * @returns {Promise<void>}
220
182
  */
221
183
  async updateFromAsync( hdr ) {
@@ -253,12 +215,8 @@ export class EquirectHDRInfo {
253
215
 
254
216
  } );
255
217
 
256
- this.marginalData = result.marginalData;
257
- this.conditionalData = result.conditionalData;
258
- this.totalSum = result.totalSum;
259
- this.compensationDelta = result.compensationDelta;
260
- this.width = result.width;
261
- this.height = result.height;
218
+ const { width: w, height: h, ...table } = result;
219
+ this._adopt( table, w, h );
262
220
 
263
221
  } finally {
264
222
 
@@ -273,155 +231,4 @@ export class EquirectHDRInfo {
273
231
 
274
232
  }
275
233
 
276
- /**
277
- * Pure-math CDF computation. Used by both the sync path and CDFWorker.
278
- * Static so it can be called without an instance.
279
- */
280
- static computeCDF( floatData, width, height ) {
281
-
282
- const numPixels = width * height;
283
-
284
- // Pass 1: compute per-pixel luminance weighted by sin(theta) and raw total sum.
285
- // sin(theta) compensates for the equirectangular projection: pixels near the poles
286
- // cover less solid angle, so weighting by sin(theta) makes the CDF proportional to
287
- // luminance per solid angle rather than luminance per pixel.
288
- const pixelWeights = new Float32Array( numPixels );
289
- let rawTotalSum = 0.0;
290
-
291
- for ( let y = 0; y < height; y ++ ) {
292
-
293
- const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
294
-
295
- for ( let x = 0; x < width; x ++ ) {
296
-
297
- const i = y * width + x;
298
- const w = colorToLuminance(
299
- floatData[ 4 * i ],
300
- floatData[ 4 * i + 1 ],
301
- floatData[ 4 * i + 2 ],
302
- ) * sinTheta;
303
- pixelWeights[ i ] = w;
304
- rawTotalSum += w;
305
-
306
- }
307
-
308
- }
309
-
310
- // MIS Compensation (Karlík et al. 2019, Eq. 14)
311
- // With equal sample allocation (c_I = 0.5): delta = 2*(1 - 0.5)*meanWeight = meanWeight
312
- // Subtracting mean sharpens the env map PDF, reducing oversampling
313
- // of dim regions already well-covered by BSDF sampling.
314
- const meanWeight = rawTotalSum / numPixels;
315
- let compensatedTotalSum = 0.0;
316
-
317
- for ( let i = 0; i < numPixels; i ++ ) {
318
-
319
- pixelWeights[ i ] = Math.max( 0, pixelWeights[ i ] - meanWeight );
320
- compensatedTotalSum += pixelWeights[ i ];
321
-
322
- }
323
-
324
- // Fall back to raw weights if compensation zeroed everything (uniform env map)
325
- const useCompensation = compensatedTotalSum > 0;
326
- const totalSumValue = useCompensation ? compensatedTotalSum : rawTotalSum;
327
- const compensationDelta = useCompensation ? meanWeight : 0;
328
-
329
- if ( ! useCompensation ) {
330
-
331
- for ( let y = 0; y < height; y ++ ) {
332
-
333
- const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
334
-
335
- for ( let x = 0; x < width; x ++ ) {
336
-
337
- const i = y * width + x;
338
- pixelWeights[ i ] = colorToLuminance(
339
- floatData[ 4 * i ],
340
- floatData[ 4 * i + 1 ],
341
- floatData[ 4 * i + 2 ],
342
- ) * sinTheta;
343
-
344
- }
345
-
346
- }
347
-
348
- }
349
-
350
- // Pass 2: build conditional and marginal CDFs from (compensated) weights
351
- const cdfConditional = new Float32Array( numPixels );
352
- const cdfMarginal = new Float32Array( height );
353
-
354
- let cumulativeWeightMarginal = 0.0;
355
-
356
- for ( let y = 0; y < height; y ++ ) {
357
-
358
- let cumulativeRowWeight = 0.0;
359
- for ( let x = 0; x < width; x ++ ) {
360
-
361
- const i = y * width + x;
362
- cumulativeRowWeight += pixelWeights[ i ];
363
- cdfConditional[ i ] = cumulativeRowWeight;
364
-
365
- }
366
-
367
- // Normalize row CDF to [0, 1]
368
- if ( cumulativeRowWeight !== 0 ) {
369
-
370
- for ( let i = y * width, l = y * width + width; i < l; i ++ ) {
371
-
372
- cdfConditional[ i ] /= cumulativeRowWeight;
373
-
374
- }
375
-
376
- }
377
-
378
- cumulativeWeightMarginal += cumulativeRowWeight;
379
- cdfMarginal[ y ] = cumulativeWeightMarginal;
380
-
381
- }
382
-
383
- // Normalize marginal CDF to [0, 1]
384
- if ( cumulativeWeightMarginal !== 0 ) {
385
-
386
- for ( let i = 0, l = cdfMarginal.length; i < l; i ++ ) {
387
-
388
- cdfMarginal[ i ] /= cumulativeWeightMarginal;
389
-
390
- }
391
-
392
- }
393
-
394
- // Create inverted CDF arrays (Float32 directly for storage buffers)
395
- const marginalData = new Float32Array( height );
396
- const conditionalData = new Float32Array( numPixels );
397
-
398
- // Invert marginal CDF
399
- for ( let i = 0; i < height; i ++ ) {
400
-
401
- const dist = ( i + 1 ) / height;
402
- const row = binarySearchFindClosestIndexOf( cdfMarginal, dist );
403
-
404
- marginalData[ i ] = ( row + 0.5 ) / height;
405
-
406
- }
407
-
408
- // Invert conditional CDFs
409
- for ( let y = 0; y < height; y ++ ) {
410
-
411
- for ( let x = 0; x < width; x ++ ) {
412
-
413
- const i = y * width + x;
414
- const dist = ( x + 1 ) / width;
415
- const col = binarySearchFindClosestIndexOf( cdfConditional, dist, y * width, width );
416
-
417
- conditionalData[ i ] = ( col + 0.5 ) / width;
418
-
419
- }
420
-
421
- }
422
-
423
- return { marginalData, conditionalData, totalSum: totalSumValue, compensationDelta };
424
-
425
- }
426
-
427
234
  }