rayzee 9.0.0 → 9.1.1

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 (95) hide show
  1. package/README.md +139 -8
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10353 -5337
  6. package/dist/rayzee.umd.js +265 -265
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +38 -56
  10. package/src/EngineEvents.js +3 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/PathTracerApp.js +290 -19
  16. package/src/Processor/ArchiveCache.js +120 -0
  17. package/src/Processor/ArchiveReader.js +179 -32
  18. package/src/Processor/AssetLoader.js +514 -76
  19. package/src/Processor/AtmosphereModel.js +465 -0
  20. package/src/Processor/BVHBuilder.js +4 -1
  21. package/src/Processor/BVHLeafFold.js +100 -0
  22. package/src/Processor/BVHRefitter.js +98 -214
  23. package/src/Processor/ChunkedRecords.js +189 -4
  24. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  25. package/src/Processor/GeometryExtractor.js +238 -40
  26. package/src/Processor/InstanceTable.js +55 -37
  27. package/src/Processor/KernelManager.js +4 -1
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +38 -0
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/NormalDepth.js +7 -6
  48. package/src/Stages/PathTracer.js +108 -4
  49. package/src/Stages/PathTracerStage.js +251 -104
  50. package/src/Storage/BLASCache.js +190 -0
  51. package/src/Storage/CDFCache.js +77 -0
  52. package/src/Storage/DownloadCache.js +266 -0
  53. package/src/Storage/GeometrySpill.js +217 -0
  54. package/src/Storage/SceneGraphCodec.js +503 -0
  55. package/src/Storage/SpillStore.js +112 -0
  56. package/src/Storage/StorageManager.js +934 -0
  57. package/src/Storage/StorageOps.js +137 -0
  58. package/src/Storage/StorageWorker.js +37 -0
  59. package/src/Storage/events.js +29 -0
  60. package/src/Storage/identity.js +77 -0
  61. package/src/Storage/index.js +9 -0
  62. package/src/Storage/inlineTransport.js +24 -0
  63. package/src/Storage/locks.js +99 -0
  64. package/src/Storage/openStorage.js +87 -0
  65. package/src/Storage/sceneCachePolicy.js +12 -0
  66. package/src/Storage/shared.js +9 -0
  67. package/src/Storage/transport.js +62 -0
  68. package/src/TSL/Atmosphere.js +684 -0
  69. package/src/TSL/BVHTraversal.js +288 -239
  70. package/src/TSL/Common.js +11 -2
  71. package/src/TSL/Displacement.js +6 -6
  72. package/src/TSL/EmissiveSampling.js +5 -5
  73. package/src/TSL/EnvironmentCDF.js +176 -0
  74. package/src/TSL/GenerateKernel.js +3 -3
  75. package/src/TSL/HitFacet.js +4 -4
  76. package/src/TSL/LightsDirect.js +8 -8
  77. package/src/TSL/LightsSampling.js +55 -37
  78. package/src/TSL/Random.js +1 -0
  79. package/src/TSL/ShadeKernel.js +75 -19
  80. package/src/TSL/Sun.js +40 -0
  81. package/src/TSL/TextureSampling.js +6 -7
  82. package/src/index.js +18 -0
  83. package/src/managers/CameraManager.js +169 -1
  84. package/src/managers/EnvironmentManager.js +263 -33
  85. package/src/managers/GoboManager.js +19 -8
  86. package/src/managers/IESManager.js +3 -3
  87. package/src/managers/LightManager.js +98 -5
  88. package/src/managers/MaterialDataManager.js +22 -2
  89. package/src/managers/TransformManager.js +26 -0
  90. package/src/managers/UniformManager.js +5 -3
  91. package/src/managers/VideoRenderManager.js +3 -1
  92. package/src/managers/timeline/CameraTrack.js +22 -0
  93. package/src/managers/timeline/TimelineManager.js +14 -0
  94. package/src/version.js +3 -0
  95. package/src/Processor/ProceduralSky.js +0 -291
@@ -0,0 +1,465 @@
1
+ // Earth's clear-sky atmosphere after Bruneton's reference model: kilometres, and 16 bin-averaged
2
+ // spectral bins of 25 nm over 380–780 nm. No three.js import, so tests use it as is.
3
+
4
+ export const GROUND_RADIUS = 6360;
5
+ export const TOP_RADIUS = 6420;
6
+ export const RAYLEIGH_SCALE_HEIGHT = 8;
7
+ export const MIE_SCALE_HEIGHT = 1.2;
8
+ export const OZONE_PEAK_ALTITUDE = 25;
9
+ export const OZONE_HALF_WIDTH = 15;
10
+ const MIE_ALBEDO = 0.9;
11
+ export const MIE_PHASE_G = 0.76;
12
+ const ANGSTROM_ALPHA = 1.3;
13
+
14
+ export const LAMBDA_COUNT = 16;
15
+ const LAMBDA_MIN = 380;
16
+ const LAMBDA_STEP = 25;
17
+
18
+ /** Horizontal illuminance of a clear noon reads ~4 engine units, like the bundled HDRIs, instead of ~150. */
19
+ export const SKY_RADIANCE_SCALE = 1 / 32;
20
+
21
+ // ASTM G-173 extraterrestrial irradiance, 360–830 nm every 10 nm, W m⁻² nm⁻¹.
22
+ const SOLAR_IRRADIANCE = [
23
+ 1.11776, 1.14259, 1.01249, 1.14716, 1.72765, 1.73054, 1.6887, 1.61253,
24
+ 1.91198, 2.03474, 2.02042, 2.02212, 1.93377, 1.95809, 1.91686, 1.8298,
25
+ 1.8685, 1.8931, 1.85149, 1.8504, 1.8341, 1.8345, 1.8147, 1.78158, 1.7533,
26
+ 1.6965, 1.68194, 1.64654, 1.6048, 1.52143, 1.55622, 1.5113, 1.474, 1.4482,
27
+ 1.41018, 1.36775, 1.34188, 1.31429, 1.28303, 1.26758, 1.2367, 1.2082,
28
+ 1.18737, 1.14683, 1.12362, 1.1058, 1.07124, 1.04992,
29
+ ];
30
+
31
+ // Ozone absorption cross section at 233 K (IUP Bremen), 360–830 nm every 10 nm, m².
32
+ const OZONE_CROSS_SECTION = [
33
+ 1.18e-27, 2.182e-28, 2.818e-28, 6.636e-28, 1.527e-27, 2.763e-27, 5.52e-27,
34
+ 8.451e-27, 1.582e-26, 2.316e-26, 3.669e-26, 4.924e-26, 7.752e-26, 9.016e-26,
35
+ 1.48e-25, 1.602e-25, 2.139e-25, 2.755e-25, 3.091e-25, 3.5e-25, 4.266e-25,
36
+ 4.672e-25, 4.398e-25, 4.701e-25, 5.019e-25, 4.305e-25, 3.74e-25, 3.215e-25,
37
+ 2.662e-25, 2.238e-25, 1.852e-25, 1.473e-25, 1.209e-25, 9.423e-26, 7.455e-26,
38
+ 6.566e-26, 5.105e-26, 4.15e-26, 4.228e-26, 3.237e-26, 2.451e-26, 2.801e-26,
39
+ 2.534e-26, 1.624e-26, 1.465e-26, 2.078e-26, 1.383e-26, 7.105e-27,
40
+ ];
41
+
42
+ // CIE 1931 2° colour matching functions, 360–830 nm every 5 nm: x̄, ȳ, z̄.
43
+ const CIE_1931 = [
44
+ 0.0001299, 0.000003917, 0.0006061, 0.0002321, 0.000006965, 0.001086, 0.0004149, 0.00001239, 0.001946,
45
+ 0.0007416, 0.00002202, 0.003486, 0.001368, 0.000039, 0.006450001, 0.002236, 0.000064, 0.01054999,
46
+ 0.004243, 0.00012, 0.02005001, 0.00765, 0.000217, 0.03621, 0.01431, 0.000396, 0.06785001,
47
+ 0.02319, 0.00064, 0.1102, 0.04351, 0.00121, 0.2074, 0.07763, 0.00218, 0.3713,
48
+ 0.13438, 0.004, 0.6456, 0.21477, 0.0073, 1.0390501, 0.2839, 0.0116, 1.3856,
49
+ 0.3285, 0.01684, 1.62296, 0.34828, 0.023, 1.74706, 0.34806, 0.0298, 1.7826,
50
+ 0.3362, 0.038, 1.77211, 0.3187, 0.048, 1.7441, 0.2908, 0.06, 1.6692,
51
+ 0.2511, 0.0739, 1.5281, 0.19536, 0.09098, 1.28764, 0.1421, 0.1126, 1.0419,
52
+ 0.09564, 0.13902, 0.8129501, 0.05795001, 0.1693, 0.6162, 0.03201, 0.20802, 0.46518,
53
+ 0.0147, 0.2586, 0.3533, 0.0049, 0.323, 0.272, 0.0024, 0.4073, 0.2123,
54
+ 0.0093, 0.503, 0.1582, 0.0291, 0.6082, 0.1117, 0.06327, 0.71, 0.07824999,
55
+ 0.1096, 0.7932, 0.05725001, 0.1655, 0.862, 0.04216, 0.2257499, 0.9148501, 0.02984,
56
+ 0.2904, 0.954, 0.0203, 0.3597, 0.9803, 0.0134, 0.4334499, 0.9949501, 0.008749999,
57
+ 0.5120501, 1.0, 0.005749999, 0.5945, 0.995, 0.0039, 0.6784, 0.9786, 0.002749999,
58
+ 0.7621, 0.952, 0.0021, 0.8425, 0.9154, 0.0018, 0.9163, 0.87, 0.001650001,
59
+ 0.9786, 0.8163, 0.0014, 1.0263, 0.757, 0.0011, 1.0567, 0.6949, 0.001,
60
+ 1.0622, 0.631, 0.0008, 1.0456, 0.5668, 0.0006, 1.0026, 0.503, 0.00034,
61
+ 0.9384, 0.4412, 0.00024, 0.8544499, 0.381, 0.00019, 0.7514, 0.321, 0.0001,
62
+ 0.6424, 0.265, 0.00004999999, 0.5419, 0.217, 0.00003, 0.4479, 0.175, 0.00002,
63
+ 0.3608, 0.1382, 0.00001, 0.2835, 0.107, 0, 0.2187, 0.0816, 0,
64
+ 0.1649, 0.061, 0, 0.1212, 0.04458, 0, 0.0874, 0.032, 0,
65
+ 0.0636, 0.0232, 0, 0.04677, 0.017, 0, 0.0329, 0.01192, 0,
66
+ 0.0227, 0.00821, 0, 0.01584, 0.005723, 0, 0.01135916, 0.004102, 0,
67
+ 0.008110916, 0.002929, 0, 0.005790346, 0.002091, 0, 0.004109457, 0.001484, 0,
68
+ 0.002899327, 0.001047, 0, 0.00204919, 0.00074, 0, 0.001439971, 0.00052, 0,
69
+ 0.0009999493, 0.0003611, 0, 0.0006900786, 0.0002492, 0, 0.0004760213, 0.0001719, 0,
70
+ 0.0003323011, 0.00012, 0, 0.0002348261, 0.0000848, 0, 0.0001661505, 0.00006, 0,
71
+ 0.000117413, 0.0000424, 0, 0.00008307527, 0.00003, 0, 0.00005870652, 0.0000212, 0,
72
+ 0.00004150994, 0.00001499, 0, 0.00002935326, 0.0000106, 0, 0.00002067383, 0.0000074657, 0,
73
+ 0.00001455977, 0.0000052578, 0, 0.00001025398, 0.0000037029, 0, 0.000007221456, 0.0000026078, 0,
74
+ 0.000005085868, 0.0000018366, 0, 0.000003581652, 0.0000012934, 0, 0.000002522525, 0.00000091093, 0,
75
+ 0.000001776509, 0.00000064153, 0, 0.000001251141, 0.00000045181, 0,
76
+ ];
77
+
78
+ export const XYZ_TO_REC709 = [
79
+ 3.2406, - 1.5372, - 0.4986,
80
+ - 0.9689, 1.8758, 0.0415,
81
+ 0.0557, - 0.204, 1.057,
82
+ ];
83
+
84
+ const DOBSON_UNIT = 2.687e20; // molecules m⁻²
85
+
86
+ // Rayleigh optical depth of the US standard atmosphere at sea level (Bodhaine et al. 1999, eq. 30).
87
+ function rayleighOpticalDepth( lambda ) {
88
+
89
+ const l2 = ( lambda / 1000 ) ** 2;
90
+ return 0.0021520 * ( 1.0455996 - 341.29061 / l2 - 0.90230850 * l2 ) / ( 1 + 0.0027059889 / l2 - 85.968563 * l2 );
91
+
92
+ }
93
+
94
+ function sampleTable( table, first, step, lambda, stride = 1, column = 0 ) {
95
+
96
+ const rows = table.length / stride;
97
+ const u = ( lambda - first ) / step;
98
+ if ( u <= 0 ) return table[ column ];
99
+ if ( u >= rows - 1 ) return table[ ( rows - 1 ) * stride + column ];
100
+ const i = Math.floor( u );
101
+ const f = u - i;
102
+ return table[ i * stride + column ] * ( 1 - f ) + table[ ( i + 1 ) * stride + column ] * f;
103
+
104
+ }
105
+
106
+ function binAverage( k, f ) {
107
+
108
+ let sum = 0;
109
+ const lo = LAMBDA_MIN + k * LAMBDA_STEP;
110
+ for ( let i = 0; i < LAMBDA_STEP; i ++ ) sum += f( lo + i + 0.5 );
111
+ return sum / LAMBDA_STEP;
112
+
113
+ }
114
+
115
+ function bins( f ) {
116
+
117
+ const out = new Float64Array( LAMBDA_COUNT );
118
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) out[ k ] = binAverage( k, f );
119
+ return out;
120
+
121
+ }
122
+
123
+ export const WAVELENGTHS = Float64Array.from( { length: LAMBDA_COUNT }, ( _, k ) => LAMBDA_MIN + ( k + 0.5 ) * LAMBDA_STEP );
124
+ export const SOLAR_SPECTRUM = bins( l => sampleTable( SOLAR_IRRADIANCE, 360, 10, l ) );
125
+ const RAYLEIGH_SPECTRUM = bins( l => rayleighOpticalDepth( l ) / RAYLEIGH_SCALE_HEIGHT );
126
+ const OZONE_SPECTRUM = bins( l => sampleTable( OZONE_CROSS_SECTION, 360, 10, l ) );
127
+ const MIE_SPECTRUM = bins( l => Math.pow( l / 1000, - ANGSTROM_ALPHA ) );
128
+
129
+ // ∫ over each bin of x̄, ȳ, z̄ — so XYZ = Σ L(bin) · CMF(bin) for bin-averaged radiance L.
130
+ const CMF_BINS = ( () => {
131
+
132
+ const out = new Float64Array( LAMBDA_COUNT * 3 );
133
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
134
+
135
+ for ( let c = 0; c < 3; c ++ ) {
136
+
137
+ out[ k * 3 + c ] = binAverage( k, l => sampleTable( CIE_1931, 360, 5, l, 3, c ) ) * LAMBDA_STEP;
138
+
139
+ }
140
+
141
+ }
142
+
143
+ return out;
144
+
145
+ } )();
146
+
147
+ /**
148
+ * The 3×16 matrix taking bin-averaged spectral radiance to linear Rec.709, rows R, G, B.
149
+ * @param {number} [scale]
150
+ */
151
+ export function spectrumToRec709( scale = 1 ) {
152
+
153
+ const out = new Float32Array( 3 * LAMBDA_COUNT );
154
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
155
+
156
+ const X = CMF_BINS[ k * 3 ], Y = CMF_BINS[ k * 3 + 1 ], Z = CMF_BINS[ k * 3 + 2 ];
157
+ for ( let c = 0; c < 3; c ++ ) {
158
+
159
+ const m = XYZ_TO_REC709;
160
+ out[ c * LAMBDA_COUNT + k ] = ( m[ c * 3 ] * X + m[ c * 3 + 1 ] * Y + m[ c * 3 + 2 ] * Z ) * scale;
161
+
162
+ }
163
+
164
+ }
165
+
166
+ return out;
167
+
168
+ }
169
+
170
+ export function spectrumToXYZ( spectrum ) {
171
+
172
+ let X = 0, Y = 0, Z = 0;
173
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
174
+
175
+ X += spectrum[ k ] * CMF_BINS[ k * 3 ];
176
+ Y += spectrum[ k ] * CMF_BINS[ k * 3 + 1 ];
177
+ Z += spectrum[ k ] * CMF_BINS[ k * 3 + 2 ];
178
+
179
+ }
180
+
181
+ return [ X, Y, Z ];
182
+
183
+ }
184
+
185
+ export function xyzToRec709( [ X, Y, Z ] ) {
186
+
187
+ const m = XYZ_TO_REC709;
188
+ return [
189
+ m[ 0 ] * X + m[ 1 ] * Y + m[ 2 ] * Z,
190
+ m[ 3 ] * X + m[ 4 ] * Y + m[ 5 ] * Z,
191
+ m[ 6 ] * X + m[ 7 ] * Y + m[ 8 ] * Z,
192
+ ];
193
+
194
+ }
195
+
196
+ /** Ångström turbidity coefficient from Linke-style turbidity (Preetham et al. 1999, appendix). */
197
+ export function angstromBeta( turbidity ) {
198
+
199
+ return Math.max( 0, 0.04608365822050 * turbidity - 0.04586025928522 );
200
+
201
+ }
202
+
203
+ // Partition of unity over the visible range, so a grey albedo stays exactly grey.
204
+ function albedoSpectrum( [ r, g, b ] ) {
205
+
206
+ const smooth = ( a, c, x ) => {
207
+
208
+ const t = Math.min( Math.max( ( x - a ) / ( c - a ), 0 ), 1 );
209
+ return t * t * ( 3 - 2 * t );
210
+
211
+ };
212
+
213
+ return Float64Array.from( WAVELENGTHS, l => {
214
+
215
+ const wb = 1 - smooth( 470, 520, l );
216
+ const wr = smooth( 560, 610, l );
217
+ return b * wb + r * wr + g * ( 1 - wb - wr );
218
+
219
+ } );
220
+
221
+ }
222
+
223
+ /**
224
+ * Per-bin coefficients at unit density, km⁻¹.
225
+ * @param {Object} p
226
+ * @param {number} [p.airDensity=1] - Rayleigh multiplier
227
+ * @param {number} [p.turbidity=2] - 1 is aerosol-free
228
+ * @param {number} [p.ozone=300] - column, Dobson units
229
+ * @param {number[]} [p.groundAlbedo=[0.3,0.3,0.3]] - linear Rec.709
230
+ */
231
+ export function atmosphereCoefficients( { airDensity = 1, turbidity = 2, ozone = 300, groundAlbedo = [ 0.3, 0.3, 0.3 ] } = {} ) {
232
+
233
+ const aerosol = angstromBeta( turbidity ) / MIE_SCALE_HEIGHT;
234
+ const ozonePeak = ozone * DOBSON_UNIT / ( OZONE_HALF_WIDTH * 1000 ) * 1000; // molecules m⁻³ → per km of path
235
+
236
+ const rayleigh = new Float64Array( LAMBDA_COUNT );
237
+ const mieExtinction = new Float64Array( LAMBDA_COUNT );
238
+ const mieScattering = new Float64Array( LAMBDA_COUNT );
239
+ const ozoneAbsorption = new Float64Array( LAMBDA_COUNT );
240
+
241
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
242
+
243
+ rayleigh[ k ] = RAYLEIGH_SPECTRUM[ k ] * airDensity;
244
+ mieExtinction[ k ] = MIE_SPECTRUM[ k ] * aerosol;
245
+ mieScattering[ k ] = mieExtinction[ k ] * MIE_ALBEDO;
246
+ ozoneAbsorption[ k ] = OZONE_SPECTRUM[ k ] * ozonePeak;
247
+
248
+ }
249
+
250
+ return { rayleigh, mieScattering, mieExtinction, ozone: ozoneAbsorption, albedo: albedoSpectrum( groundAlbedo ) };
251
+
252
+ }
253
+
254
+ export const rayleighDensity = h => Math.exp( - h / RAYLEIGH_SCALE_HEIGHT );
255
+ export const mieDensity = h => Math.exp( - h / MIE_SCALE_HEIGHT );
256
+ export const ozoneDensity = h => Math.max( 0, 1 - Math.abs( h - OZONE_PEAK_ALTITUDE ) / OZONE_HALF_WIDTH );
257
+
258
+ /** sin of how far the horizon sits below level, seen from `altitude` km: directions with y below −this hit the ground. */
259
+ export function horizonDipSin( altitude ) {
260
+
261
+ const h = Math.max( altitude, 0 );
262
+ return Math.sqrt( h * ( 2 * GROUND_RADIUS + h ) ) / ( GROUND_RADIUS + h );
263
+
264
+ }
265
+
266
+ /**
267
+ * Density-weighted path length (km) from `altitude` along zenith cosine `mu` to the top of the
268
+ * atmosphere: [ Rayleigh, Mie, ozone ]. Null when the ray meets the ground.
269
+ */
270
+ export function opticalDepth( altitude, mu, steps = 1024 ) {
271
+
272
+ const h0 = Math.max( altitude, 0 );
273
+ const r = GROUND_RADIUS + h0;
274
+ const b = r * mu;
275
+ const cGround = h0 * ( 2 * GROUND_RADIUS + h0 );
276
+ if ( mu < 0 && b * b >= cGround ) return null;
277
+
278
+ const tTop = - b + Math.sqrt( b * b + ( TOP_RADIUS - r ) * ( TOP_RADIUS + r ) );
279
+ let tr = 0, tm = 0, to = 0;
280
+ for ( let i = 0; i < steps; i ++ ) {
281
+
282
+ // Squared spacing: the density is near the start of every ray that leaves the ground.
283
+ const t0 = tTop * ( i / steps ) ** 2;
284
+ const t1 = tTop * ( ( i + 1 ) / steps ) ** 2;
285
+ const t = 0.5 * ( t0 + t1 );
286
+ const r2mRg2 = cGround + t * ( t + 2 * b );
287
+ const h = r2mRg2 / ( Math.sqrt( r * r + t * ( t + 2 * b ) ) + GROUND_RADIUS );
288
+ const dt = t1 - t0;
289
+ tr += rayleighDensity( h ) * dt;
290
+ tm += mieDensity( h ) * dt;
291
+ to += ozoneDensity( h ) * dt;
292
+
293
+ }
294
+
295
+ return [ tr, tm, to ];
296
+
297
+ }
298
+
299
+ function transmittanceSpectrum( coefficients, depth ) {
300
+
301
+ const out = new Float64Array( LAMBDA_COUNT );
302
+ if ( ! depth ) return out;
303
+ const [ tr, tm, to ] = depth;
304
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
305
+
306
+ out[ k ] = Math.exp( - ( coefficients.rayleigh[ k ] * tr + coefficients.mieExtinction[ k ] * tm + coefficients.ozone[ k ] * to ) );
307
+
308
+ }
309
+
310
+ return out;
311
+
312
+ }
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 ) );
316
+
317
+ /** Solid angle of a cone of the given half-angle, without the cancellation `2π(1 − cos)` suffers. */
318
+ export function coneSolidAngle( halfAngle ) {
319
+
320
+ const s = Math.sin( halfAngle / 2 );
321
+ return 4 * Math.PI * s * s;
322
+
323
+ }
324
+
325
+ /**
326
+ * The sun as a light: its disc-average radiance (engine units, linear Rec.709) over the part of
327
+ * the disc above the horizon. Near the horizon the transmittance changes across the disc by
328
+ * several times, so it is averaged over the disc rather than read at its centre.
329
+ * @param {Object} p
330
+ * @param {number[]} p.direction - unit, y up
331
+ * @param {number} p.altitude - km
332
+ * @param {number} p.angularDiameter - radians
333
+ * @param {number} [p.strength=1]
334
+ * @param {Object} p.coefficients - from atmosphereCoefficients
335
+ */
336
+ export function sunLight( { direction, altitude, angularDiameter, strength = 1, coefficients } ) {
337
+
338
+ const halfAngle = angularDiameter / 2;
339
+ const solidAngle = coneSolidAngle( halfAngle );
340
+ const dip = horizonDipSin( altitude );
341
+ const [ dx, dy, dz ] = direction;
342
+ const sinHalf = Math.sin( halfAngle );
343
+
344
+ const transmittance = new Float64Array( LAMBDA_COUNT );
345
+ let visible = 0;
346
+
347
+ const accumulate = mu => {
348
+
349
+ if ( mu < - dip ) return;
350
+ const t = transmittanceSpectrum( coefficients, opticalDepth( altitude, mu, 512 ) );
351
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) transmittance[ k ] += t[ k ];
352
+ visible ++;
353
+
354
+ };
355
+
356
+ if ( dy - sinHalf > 0.1 ) {
357
+
358
+ accumulate( dy );
359
+
360
+ } else {
361
+
362
+ // Only the disc's height changes the transmittance: rows at equal-area steps down it.
363
+ const rows = 48;
364
+ const cosEl = Math.sqrt( Math.max( 0, 1 - dy * dy ) );
365
+ for ( let i = 0; i < rows; i ++ ) {
366
+
367
+ const q = ( i + 0.5 ) / rows;
368
+ let lo = - 1, hi = 1;
369
+ for ( let it = 0; it < 30; it ++ ) {
370
+
371
+ const m = 0.5 * ( lo + hi );
372
+ const area = ( Math.asin( m ) + m * Math.sqrt( 1 - m * m ) + Math.PI / 2 ) / Math.PI;
373
+ if ( area < q ) lo = m; else hi = m;
374
+
375
+ }
376
+
377
+ accumulate( dy + 0.5 * ( lo + hi ) * sinHalf * cosEl );
378
+
379
+ }
380
+
381
+ }
382
+
383
+ const radiance = new Float64Array( LAMBDA_COUNT );
384
+ const count = dy - sinHalf > 0.1 ? 1 : 48;
385
+ if ( visible > 0 ) {
386
+
387
+ for ( let k = 0; k < LAMBDA_COUNT; k ++ ) {
388
+
389
+ radiance[ k ] = SOLAR_SPECTRUM[ k ] * strength / solidAngle * transmittance[ k ] / visible;
390
+
391
+ }
392
+
393
+ }
394
+
395
+ const rgb = xyzToRec709( spectrumToXYZ( radiance ) ).map( v => Math.max( 0, v * SKY_RADIANCE_SCALE ) );
396
+
397
+ return {
398
+ radiance: rgb,
399
+ visibleFraction: visible / count,
400
+ solidAngle,
401
+ direction: [ dx, dy, dz ],
402
+ };
403
+
404
+ }
405
+
406
+ export const rayleighPhase = nu => 3 / ( 16 * Math.PI ) * ( 1 + nu * nu );
407
+
408
+ export function miePhase( nu, g = MIE_PHASE_G ) {
409
+
410
+ const g2 = g * g;
411
+ return 3 / ( 8 * Math.PI ) * ( 1 - g2 ) / ( 2 + g2 ) * ( 1 + nu * nu ) / Math.pow( 1 + g2 - 2 * g * nu, 1.5 );
412
+
413
+ }
414
+
415
+ /**
416
+ * How much of each phase function falls in each incoming cell: rows are viewing directions
417
+ * (zenith cosine uniform over [−1, 1] × azimuth from the sun uniform over [0, π]), columns are
418
+ * incoming cells (zenith cosine bands × azimuth sectors over [0, π], each with its mirror image).
419
+ * Rayleigh rows first, then Mie; every row sums to 1.
420
+ */
421
+ export function phaseCellWeights( viewsMu, viewsPhi, bandsMu, sectorsPhi, sub = 8 ) {
422
+
423
+ const cells = bandsMu * sectorsPhi;
424
+ const rows = viewsMu * viewsPhi;
425
+ const out = new Float32Array( 2 * rows * cells );
426
+ [ rayleighPhase, nu => miePhase( nu ) ].forEach( ( phase, lobe ) => {
427
+
428
+ for ( let v = 0; v < rows; v ++ ) {
429
+
430
+ const muV = - 1 + 2 * Math.floor( v / viewsPhi ) / ( viewsMu - 1 );
431
+ const phiV = Math.PI * ( v % viewsPhi ) / ( viewsPhi - 1 );
432
+ const sV = Math.sqrt( Math.max( 0, 1 - muV * muV ) );
433
+ const row = ( lobe * rows + v ) * cells;
434
+ let total = 0;
435
+ for ( let c = 0; c < cells; c ++ ) {
436
+
437
+ const band = Math.floor( c / sectorsPhi ), sector = c % sectorsPhi;
438
+ let w = 0;
439
+ for ( let i = 0; i < sub; i ++ ) {
440
+
441
+ const mu = - 1 + 2 * ( band + ( i + 0.5 ) / sub ) / bandsMu;
442
+ const s = Math.sqrt( Math.max( 0, 1 - mu * mu ) );
443
+ for ( let j = 0; j < sub; j ++ ) {
444
+
445
+ const phi = Math.PI * ( sector + ( j + 0.5 ) / sub ) / sectorsPhi;
446
+ w += phase( muV * mu + sV * s * Math.cos( phiV - phi ) ) + phase( muV * mu + sV * s * Math.cos( phiV + phi ) );
447
+
448
+ }
449
+
450
+ }
451
+
452
+ out[ row + c ] = w;
453
+ total += w;
454
+
455
+ }
456
+
457
+ for ( let c = 0; c < cells; c ++ ) out[ row + c ] /= total;
458
+
459
+ }
460
+
461
+ } );
462
+
463
+ return out;
464
+
465
+ }
@@ -2,6 +2,7 @@ import { TreeletOptimizer } from './TreeletOptimizer.js';
2
2
  import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
3
3
  // Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
4
4
  import { createLogger, fmt } from '../utils/Logger.js';
5
+ import { foldLeaves } from './BVHLeafFold.js';
5
6
 
6
7
  const log = createLogger( 'bvh' );
7
8
 
@@ -68,6 +69,7 @@ export class BVHBuilder {
68
69
 
69
70
  this.useWorker = true;
70
71
  this.maxLeafSize = 8;
72
+ this.foldLeaves = false;
71
73
  this.numBins = 32;
72
74
  this.minBins = 8;
73
75
  this.maxBins = 64;
@@ -539,7 +541,8 @@ export class BVHBuilder {
539
541
  _buildSyncAndFlatten( triangles, depth, progressCallback ) {
540
542
 
541
543
  const root = this.buildSync( triangles, depth, progressCallback );
542
- const bvhData = this.flattenBVH( root );
544
+ const flat = this.flattenBVH( root );
545
+ const bvhData = this.foldLeaves ? foldLeaves( flat ) : flat;
543
546
  // Return reordered triangles if available (avoids 362MB copy)
544
547
  const reorderedTriangles = this.reorderedTriangleData || null;
545
548
  const originalToBvh = this.originalToBvhMap || null;
@@ -0,0 +1,100 @@
1
+ // Inline copies of EngineDefaults' BVH_LEAF_MARKERS.TRIANGLE_LEAF, BVH_MAX_INDEX and
2
+ // BVH_FOLDED_LEAF_MAX: this runs in the BVH workers, which cannot import it. Keep in step.
3
+ const TRIANGLE_LEAF = 0x40000000;
4
+ const BVH_MAX_INDEX = 0x40000000;
5
+ const BVH_FOLDED_LEAF_MAX = 15;
6
+ const FLOATS_PER_NODE = 16;
7
+ const FOLDED = 0x80000000;
8
+
9
+ /** A folded leaf as its parent stores it (EngineDefaults: BVH_FOLDED_LEAF_MAX). */
10
+ export const foldedRef = ( first, count ) => ( ~ ( ( first << 4 ) | count ) ) >>> 0;
11
+ export const isFoldedRef = ref => ref >= FOLDED;
12
+ export const foldedFirst = ref => ( ( ~ ref ) >>> 0 ) >>> 4;
13
+ export const foldedCount = ref => ( ~ ref ) & 15;
14
+
15
+ /**
16
+ * Folds every small triangle leaf into its parent ({@link foldedRef}), so the leaf node
17
+ * goes: close to half of a binary BVH's nodes, and traversal no longer fetches a node only to read a
18
+ * triangle range. Walked from the root in pre-order, which keeps each child after its parent, as a
19
+ * refit needs, and drops nodes nothing reaches.
20
+ * @param {Float32Array} data - a flattened BLAS, every leaf a node
21
+ * @returns {Float32Array}
22
+ */
23
+ export function foldLeaves( data ) {
24
+
25
+ const src = new Uint32Array( data.buffer, data.byteOffset, data.length );
26
+ const nodeCount = data.length / FLOATS_PER_NODE;
27
+ const folds = n => {
28
+
29
+ const o = n * FLOATS_PER_NODE;
30
+ return src[ o + 3 ] === TRIANGLE_LEAF && src[ o + 1 ] > 0 && src[ o + 1 ] <= BVH_FOLDED_LEAF_MAX;
31
+
32
+ };
33
+
34
+ const renumbered = new Uint32Array( nodeCount );
35
+ const kept = new Uint32Array( nodeCount );
36
+ let count = 0;
37
+ const stack = [ 0 ];
38
+ while ( stack.length ) {
39
+
40
+ const n = stack.pop();
41
+ renumbered[ n ] = count;
42
+ kept[ count ++ ] = n;
43
+ const o = n * FLOATS_PER_NODE;
44
+ if ( src[ o + 3 ] >= BVH_MAX_INDEX ) continue;
45
+ if ( ! folds( src[ o + 7 ] ) ) stack.push( src[ o + 7 ] );
46
+ if ( ! folds( src[ o + 3 ] ) ) stack.push( src[ o + 3 ] );
47
+
48
+ }
49
+
50
+ const out = new Float32Array( count * FLOATS_PER_NODE );
51
+ const idx = new Uint32Array( out.buffer );
52
+ for ( let k = 0; k < count; k ++ ) {
53
+
54
+ const from = kept[ k ] * FLOATS_PER_NODE;
55
+ const o = k * FLOATS_PER_NODE;
56
+ out.set( data.subarray( from, from + FLOATS_PER_NODE ), o );
57
+ if ( src[ from + 3 ] >= BVH_MAX_INDEX ) continue;
58
+
59
+ idx[ o + 3 ] = childRef( src, src[ from + 3 ], folds, renumbered );
60
+ idx[ o + 7 ] = childRef( src, src[ from + 7 ], folds, renumbered );
61
+
62
+ }
63
+
64
+ return out;
65
+
66
+ }
67
+
68
+ function childRef( src, child, folds, renumbered ) {
69
+
70
+ const o = child * FLOATS_PER_NODE;
71
+ return folds( child ) ? foldedRef( src[ o ], src[ o + 1 ] ) : renumbered[ child ];
72
+
73
+ }
74
+
75
+ const rebaseRef = ( ref, nodeOffset, triOffset ) => ( ref >= FOLDED
76
+ ? foldedRef( foldedFirst( ref ) + triOffset, foldedCount( ref ) )
77
+ : ref + nodeOffset );
78
+
79
+ /**
80
+ * Moves one BLAS's references: node indices by `nodeOffset`, triangle offsets (a leaf node's,
81
+ * or a folded leaf's in its parent) by `triOffset`.
82
+ * @param {Uint32Array} idx - u32 view of the nodes
83
+ */
84
+ export function rebaseNodes( idx, nodeOffset, triOffset, from = 0, to = idx.length ) {
85
+
86
+ for ( let o = from; o < to; o += FLOATS_PER_NODE ) {
87
+
88
+ if ( idx[ o + 3 ] === TRIANGLE_LEAF ) {
89
+
90
+ idx[ o ] += triOffset;
91
+ continue;
92
+
93
+ }
94
+
95
+ idx[ o + 3 ] = rebaseRef( idx[ o + 3 ], nodeOffset, triOffset );
96
+ idx[ o + 7 ] = rebaseRef( idx[ o + 7 ], nodeOffset, triOffset );
97
+
98
+ }
99
+
100
+ }