rayzee 10.0.0 → 11.1.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 (176) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-Hn35o6Z5.js +5145 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-CBw8hkeu.js +946 -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-BzNpiVHZ.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-Bk7bHg2B.js} +55 -120
  14. package/dist/PathTracer-72XufKAr.js +29976 -0
  15. package/dist/PhysicalSky-BgjdV88k.js +1219 -0
  16. package/dist/RayzeeRenderer-1QRO97eq.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-tnAoUL1j.js +94 -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-C67jbci-.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 +20 -7
  52. package/src/Processor/ArchiveImporter.js +1265 -0
  53. package/src/Processor/ArchiveReader.js +22 -4
  54. package/src/Processor/AssetLoader.js +327 -1763
  55. package/src/Processor/AtmosphereModel.js +1 -2
  56. package/src/Processor/BVHBuilder.js +14 -126
  57. package/src/Processor/BVHLeafFold.js +4 -6
  58. package/src/Processor/BVHRefitter.js +67 -46
  59. package/src/Processor/BufferLayout.js +291 -0
  60. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  61. package/src/Processor/EnvironmentExactTable.js +187 -0
  62. package/src/Processor/EquirectHDRInfo.js +30 -223
  63. package/src/Processor/FileFormats.js +116 -0
  64. package/src/Processor/GLTFDecoders.js +85 -0
  65. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  66. package/src/Processor/GeometryExtractor.js +15 -5
  67. package/src/Processor/HostMemory.js +7 -1
  68. package/src/Processor/IESParser.js +47 -35
  69. package/src/Processor/InstanceTable.js +1 -1
  70. package/src/Processor/KernelManager.js +35 -2
  71. package/src/Processor/MaterialPacking.js +10 -8
  72. package/src/Processor/PBRT/PBRTMaterials.js +254 -42
  73. package/src/Processor/PBRT/PBRTParser.js +49 -19
  74. package/src/Processor/PBRT/PBRTScattering.js +93 -0
  75. package/src/Processor/PBRT/PBRTSceneBuilder.js +1050 -75
  76. package/src/Processor/PBRT/PBRTTextureBake.js +179 -0
  77. package/src/Processor/PBRT/PFM.js +59 -0
  78. package/src/Processor/PackedRayBuffer.js +116 -9
  79. package/src/Processor/ParallelBVHBuilder.js +3 -6
  80. package/src/Processor/PhysicalSky.js +5 -4
  81. package/src/Processor/PlatformImageLoader.js +50 -6
  82. package/src/Processor/QueueManager.js +39 -5
  83. package/src/Processor/ReinsertionOptimizer.js +2 -2
  84. package/src/Processor/SceneMetadata.js +30 -5
  85. package/src/Processor/SceneProcessor.js +53 -62
  86. package/src/Processor/ShaderBuilder.js +6 -16
  87. package/src/Processor/SimpleSky.js +1 -62
  88. package/src/Processor/SolarLimb.js +3 -0
  89. package/src/Processor/SplitBLAS.js +1 -1
  90. package/src/Processor/StorageTexturePool.js +22 -1
  91. package/src/Processor/TLASBuilder.js +1 -3
  92. package/src/Processor/TextureBuckets.js +237 -0
  93. package/src/Processor/TextureCreator.js +181 -8
  94. package/src/Processor/ToneMapGPU.js +64 -15
  95. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  96. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  97. package/src/Processor/Workers/BVHWorker.js +5 -18
  98. package/src/Processor/Workers/CDFWorker.js +6 -183
  99. package/src/Processor/Workers/TexturesWorker.js +103 -18
  100. package/src/Processor/archiveFormats.js +7 -0
  101. package/src/Processor/utils.js +2 -1
  102. package/src/RayzeeRenderer.js +3900 -0
  103. package/src/RenderSettings.js +68 -62
  104. package/src/Stages/ASVGF.js +13 -14
  105. package/src/Stages/AutoExposure.js +17 -17
  106. package/src/Stages/BilateralFilter.js +2 -1
  107. package/src/Stages/Compositor.js +8 -23
  108. package/src/Stages/DenoiserSettings.js +182 -0
  109. package/src/Stages/EdgeFilter.js +2 -1
  110. package/src/Stages/MotionVector.js +1 -1
  111. package/src/Stages/NRD.js +11 -5
  112. package/src/Stages/NormalDepth.js +12 -8
  113. package/src/Stages/PathTracer.js +336 -43
  114. package/src/Stages/PathTracerStage.js +69 -66
  115. package/src/Stages/Variance.js +1 -1
  116. package/src/Storage/BLASCache.js +1 -1
  117. package/src/Storage/CDFCache.js +18 -17
  118. package/src/Storage/DownloadCache.js +1 -1
  119. package/src/Storage/SceneGraphCodec.js +46 -3
  120. package/src/Storage/SpillStore.js +1 -1
  121. package/src/Storage/StorageManager.js +45 -9
  122. package/src/Storage/areas.js +4 -0
  123. package/src/TSL/BVHTraversal.js +50 -25
  124. package/src/TSL/Bidirectional.js +208 -0
  125. package/src/TSL/BidirectionalLamps.js +132 -0
  126. package/src/TSL/Clearcoat.js +15 -6
  127. package/src/TSL/Common.js +20 -15
  128. package/src/TSL/ConnectKernel.js +167 -0
  129. package/src/TSL/Debugger.js +1 -1
  130. package/src/TSL/Displacement.js +4 -3
  131. package/src/TSL/EmissiveSampling.js +65 -18
  132. package/src/TSL/Environment.js +77 -106
  133. package/src/TSL/EnvironmentCDF.js +85 -39
  134. package/src/TSL/ExtendKernel.js +8 -3
  135. package/src/TSL/FinalWriteKernel.js +4 -4
  136. package/src/TSL/GenerateKernel.js +18 -4
  137. package/src/TSL/HitFacet.js +35 -5
  138. package/src/TSL/LightBVHSampling.js +40 -9
  139. package/src/TSL/LightGenerateKernel.js +293 -0
  140. package/src/TSL/LightGuide.js +198 -0
  141. package/src/TSL/LightSplatKernel.js +180 -0
  142. package/src/TSL/LightVisibility.js +111 -0
  143. package/src/TSL/LightsCore.js +13 -38
  144. package/src/TSL/LightsDirect.js +28 -29
  145. package/src/TSL/LightsIndirect.js +3 -1
  146. package/src/TSL/LightsSampling.js +239 -51
  147. package/src/TSL/MaterialEvaluation.js +113 -31
  148. package/src/TSL/MaterialProperties.js +44 -17
  149. package/src/TSL/MaterialTransmission.js +52 -47
  150. package/src/TSL/MergeKernel.js +224 -0
  151. package/src/TSL/PathTracerCore.js +65 -29
  152. package/src/TSL/Random.js +5 -1
  153. package/src/TSL/SceneResources.js +51 -0
  154. package/src/TSL/ShadeKernel.js +985 -101
  155. package/src/TSL/Struct.js +10 -0
  156. package/src/TSL/Sun.js +1 -1
  157. package/src/TSL/TextureSampling.js +66 -61
  158. package/src/addons/archives.js +14 -0
  159. package/src/addons/bidirectional.js +13 -0
  160. package/src/addons/color.js +32 -0
  161. package/src/addons/formats.js +16 -0
  162. package/src/addons/physicalSky.js +14 -0
  163. package/src/addons/storage.js +19 -0
  164. package/src/core.js +31 -0
  165. package/src/index.js +13 -23
  166. package/src/integrators/BidirectionalIntegrator.js +838 -0
  167. package/src/managers/AnimationManager.js +88 -6
  168. package/src/managers/CameraManager.js +103 -46
  169. package/src/managers/DenoisingManager.js +14 -10
  170. package/src/managers/EnvironmentManager.js +125 -103
  171. package/src/managers/GoboManager.js +9 -0
  172. package/src/managers/MaterialDataManager.js +48 -37
  173. package/src/managers/UniformManager.js +32 -10
  174. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  175. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  176. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -0,0 +1,179 @@
1
+ /**
2
+ * pbrt textures the engine has no node for, baked on the CPU into an image it samples: the `mix` texture,
3
+ * an image texture's `scale` above 1 or `invert`, and a `mix` material whose amount is a texture.
4
+ *
5
+ * A layer is an image as pbrt evaluates it: linear values, top row first, `channels` 1 (a float texture reads
6
+ * an image's alpha where it has one, else the mean of its colour) or 3, already scaled and inverted, and the uv
7
+ * mapping pbrt applies before the lookup (st = scale · uv + delta, image row from the top = 1 − t).
8
+ */
9
+
10
+ const MAX_BAKE_SIZE = 4096;
11
+
12
+ export const srgbToLinear = ( v ) => ( v <= 0.04045 ? v / 12.92 : ( ( v + 0.055 ) / 1.055 ) ** 2.4 );
13
+ export const linearToSRGB = ( v ) => ( v <= 0.0031308 ? v * 12.92 : 1.055 * v ** ( 1 / 2.4 ) - 0.055 );
14
+
15
+ /**
16
+ * @param {{ data: ArrayLike<number>, width: number, height: number, channels?: number, topDown?: boolean }} pixels -
17
+ * 8-bit (0–255) or float values, `channels` per texel (4 if unsaid)
18
+ * @param {object} options
19
+ * @param {number} options.channels - 1 for a float texture, 3 for a spectrum one
20
+ * @param {(v: number) => number} [options.decode] - 8-bit value / 255 → linear; omitted for float data
21
+ * @param {number} [options.scale=1]
22
+ * @param {boolean} [options.invert=false]
23
+ * @param {{ su: number, sv: number, du: number, dv: number }} [options.mapping]
24
+ */
25
+ export function makeLayer( pixels, { channels, decode = null, scale = 1, invert = false, mapping = null } ) {
26
+
27
+ const { width, height } = pixels;
28
+ const stride = pixels.channels ?? 4;
29
+ const data = new Float32Array( width * height * channels );
30
+ const topDown = pixels.topDown !== false;
31
+ // pbrt's encoding applies to every channel of an 8-bit image, alpha included.
32
+ const read = ( i ) => ( decode ? decode( pixels.data[ i ] / 255 ) : pixels.data[ i ] );
33
+ const fromAlpha = channels === 1 && stride === 4 && hasAlpha( pixels );
34
+ const mean = channels === 1 && stride >= 3 && ! fromAlpha;
35
+
36
+ for ( let y = 0; y < height; y ++ ) {
37
+
38
+ const row = topDown ? y : height - 1 - y;
39
+ for ( let x = 0; x < width; x ++ ) {
40
+
41
+ const src = ( row * width + x ) * stride, dst = ( y * width + x ) * channels;
42
+ for ( let c = 0; c < channels; c ++ ) {
43
+
44
+ const raw = fromAlpha ? read( src + 3 )
45
+ : mean ? ( read( src ) + read( src + 1 ) + read( src + 2 ) ) / 3
46
+ : read( src + Math.min( c, stride - 1 ) );
47
+ const v = scale * raw;
48
+ data[ dst + c ] = invert ? Math.max( 0, 1 - v ) : v;
49
+
50
+ }
51
+
52
+ }
53
+
54
+ }
55
+
56
+ return { width, height, channels, data, su: 1, sv: 1, du: 0, dv: 0, ...mapping };
57
+
58
+ }
59
+
60
+ /** Whether an RGBA image's alpha is anything but opaque — pbrt reads it as an RGB image when it is not. */
61
+ export function hasAlpha( pixels ) {
62
+
63
+ const { data } = pixels;
64
+ const one = pixels.float ? 1 : 255;
65
+ for ( let i = 3; i < data.length; i += 4 ) if ( data[ i ] !== one ) return true;
66
+ return false;
67
+
68
+ }
69
+
70
+ /** The layer's mean value (a float layer's only channel; a spectrum layer's average of three). */
71
+ export function layerMean( layer ) {
72
+
73
+ let sum = 0;
74
+ for ( let i = 0; i < layer.data.length; i ++ ) sum += layer.data[ i ];
75
+ return layer.data.length ? sum / layer.data.length : 0;
76
+
77
+ }
78
+
79
+ // Bilinear, repeating, at pbrt's (s, t).
80
+ function sample( layer, s, t, out ) {
81
+
82
+ const { width: w, height: h, channels: n, data } = layer;
83
+ const x = s * w - 0.5, y = ( 1 - t ) * h - 0.5;
84
+ const x0 = Math.floor( x ), y0 = Math.floor( y );
85
+ const fx = x - x0, fy = y - y0;
86
+ const wrap = ( i, size ) => ( ( i % size ) + size ) % size;
87
+ const xa = wrap( x0, w ), xb = wrap( x0 + 1, w ), ya = wrap( y0, h ), yb = wrap( y0 + 1, h );
88
+
89
+ for ( let c = 0; c < 3; c ++ ) {
90
+
91
+ const k = Math.min( c, n - 1 );
92
+ const top = data[ ( ya * w + xa ) * n + k ] * ( 1 - fx ) + data[ ( ya * w + xb ) * n + k ] * fx;
93
+ const bottom = data[ ( yb * w + xa ) * n + k ] * ( 1 - fx ) + data[ ( yb * w + xb ) * n + k ] * fx;
94
+ out[ c ] = top * ( 1 - fy ) + bottom * fy;
95
+
96
+ }
97
+
98
+ return out;
99
+
100
+ }
101
+
102
+ /**
103
+ * Each term is `{ rgb }` (a constant) or `{ layer, tint? }` (an image, times `tint`); `clamp` clamps it to [0, 1]
104
+ * first, as pbrt clamps a colour texture read as an albedo, before anything mixes it. Evaluates
105
+ * `combine( values )` over the texel grid of the largest image among the terms, in that image's uv mapping;
106
+ * a term with another mapping is sampled through it. The result is sRGB bytes laid out for a DataTexture
107
+ * (bottom row first), clamped to [0, 1] as pbrt clamps an albedo. Null when every term is a constant.
108
+ * @param {Array<{ rgb?: number[], layer?: object, tint?: number[] }>} terms
109
+ * @param {(values: number[][]) => number[]} combine - linear RGB from each term's linear RGB
110
+ * @param {object} [options]
111
+ * @param {(values: number[][]) => number} [options.alphaOf] - the alpha channel, linear; opaque without it
112
+ * @param {boolean} [options.linear=false] - linear bytes, for a data map
113
+ * @returns {{ data: Uint8Array, width: number, height: number, mapping: { su: number, sv: number, du: number, dv: number } } | null}
114
+ */
115
+ export function bake( terms, combine, { alphaOf = null, linear = false } = {} ) {
116
+
117
+ let driver = null;
118
+ for ( const term of terms ) if ( term.layer && ( ! driver || term.layer.width * term.layer.height > driver.width * driver.height ) ) driver = term.layer;
119
+ if ( ! driver ) return null;
120
+
121
+ const width = Math.min( driver.width, MAX_BAKE_SIZE ), height = Math.min( driver.height, MAX_BAKE_SIZE );
122
+ const out = new Uint8Array( width * height * 4 );
123
+ const values = terms.map( () => [ 0, 0, 0 ] );
124
+
125
+ for ( let j = 0; j < height; j ++ ) {
126
+
127
+ const t = ( j + 0.5 ) / height;
128
+ const v = ( t - driver.dv ) / driver.sv;
129
+
130
+ for ( let i = 0; i < width; i ++ ) {
131
+
132
+ const s = ( i + 0.5 ) / width;
133
+ const u = ( s - driver.du ) / driver.su;
134
+
135
+ for ( let k = 0; k < terms.length; k ++ ) {
136
+
137
+ const term = terms[ k ], value = values[ k ];
138
+ if ( term.layer ) {
139
+
140
+ const L = term.layer;
141
+ sample( L, L === driver ? s : L.su * u + L.du, L === driver ? t : L.sv * v + L.dv, value );
142
+ if ( term.tint ) for ( let c = 0; c < 3; c ++ ) value[ c ] *= term.tint[ c ];
143
+
144
+ } else {
145
+
146
+ value[ 0 ] = term.rgb[ 0 ]; value[ 1 ] = term.rgb[ 1 ]; value[ 2 ] = term.rgb[ 2 ];
147
+
148
+ }
149
+
150
+ if ( term.clamp ) for ( let c = 0; c < 3; c ++ ) value[ c ] = Math.min( 1, Math.max( 0, value[ c ] ) );
151
+
152
+ }
153
+
154
+ const rgb = combine( values );
155
+ const o = ( j * width + i ) * 4;
156
+ for ( let c = 0; c < 3; c ++ ) {
157
+
158
+ const v = Math.min( 1, Math.max( 0, rgb[ c ] ) );
159
+ out[ o + c ] = Math.round( 255 * ( linear ? v : linearToSRGB( v ) ) );
160
+
161
+ }
162
+
163
+ out[ o + 3 ] = alphaOf ? Math.round( 255 * Math.min( 1, Math.max( 0, alphaOf( values ) ) ) ) : 255;
164
+
165
+ }
166
+
167
+ }
168
+
169
+ return { data: out, width, height, mapping: { su: driver.su, sv: driver.sv, du: driver.du, dv: driver.dv } };
170
+
171
+ }
172
+
173
+ /** pbrt's mix: ( 1 − amount ) · a + amount · b, amount read from its first channel. */
174
+ export const mixOf = ( [ a, b, amount ] ) => {
175
+
176
+ const t = amount[ 0 ];
177
+ return [ a[ 0 ] + ( b[ 0 ] - a[ 0 ] ) * t, a[ 1 ] + ( b[ 1 ] - a[ 1 ] ) * t, a[ 2 ] + ( b[ 2 ] - a[ 2 ] ) * t ];
178
+
179
+ };
@@ -0,0 +1,59 @@
1
+ import { DataTexture, FloatType, LinearFilter, RGBAFormat } from 'three';
2
+
3
+ /**
4
+ * Portable float map: "PF" (RGB) or "Pf" (grey), width and height, then a scale whose sign is the byte
5
+ * order, then rows bottom first — the order three's EXR and HDR loaders fill a DataTexture in.
6
+ * A scale other than ±1 multiplies the pixels, as pbrt reads it.
7
+ * @param {Uint8Array} bytes
8
+ * @returns {{ data: Float32Array, width: number, height: number }} RGBA, row 0 the bottom
9
+ */
10
+ export function decodePFM( bytes ) {
11
+
12
+ let pos = 0;
13
+ const token = () => {
14
+
15
+ while ( pos < bytes.length && bytes[ pos ] <= 32 ) pos ++;
16
+ const start = pos;
17
+ while ( pos < bytes.length && bytes[ pos ] > 32 ) pos ++;
18
+ return String.fromCharCode( ...bytes.subarray( start, pos ) );
19
+
20
+ };
21
+
22
+ const magic = token();
23
+ if ( magic !== 'PF' && magic !== 'Pf' ) throw new Error( 'not a PFM image' );
24
+ const width = parseInt( token(), 10 );
25
+ const height = parseInt( token(), 10 );
26
+ const scale = parseFloat( token() );
27
+ pos ++;
28
+
29
+ const channels = magic === 'PF' ? 3 : 1;
30
+ const count = width * height;
31
+ if ( ! ( width > 0 && height > 0 ) || ! Number.isFinite( scale ) || scale === 0 ) throw new Error( 'bad PFM header' );
32
+ if ( bytes.length - pos < count * channels * 4 ) throw new Error( 'PFM image is truncated' );
33
+
34
+ const view = new DataView( bytes.buffer, bytes.byteOffset + pos, count * channels * 4 );
35
+ const little = scale < 0;
36
+ const gain = Math.abs( scale );
37
+ const data = new Float32Array( count * 4 );
38
+ for ( let i = 0; i < count; i ++ ) {
39
+
40
+ for ( let c = 0; c < 3; c ++ ) data[ i * 4 + c ] = view.getFloat32( ( i * channels + ( channels === 3 ? c : 0 ) ) * 4, little ) * gain;
41
+ data[ i * 4 + 3 ] = 1;
42
+
43
+ }
44
+
45
+ return { data, width, height };
46
+
47
+ }
48
+
49
+ /** @param {Uint8Array} bytes */
50
+ export function pfmTexture( bytes ) {
51
+
52
+ const { data, width, height } = decodePFM( bytes );
53
+ const texture = new DataTexture( data, width, height, RGBAFormat, FloatType );
54
+ texture.minFilter = LinearFilter;
55
+ texture.magFilter = LinearFilter;
56
+ texture.needsUpdate = true;
57
+ return texture;
58
+
59
+ }
@@ -44,11 +44,19 @@ export const HIT = {
44
44
  NORMAL_MAT: 1, // uvec4(octNormal, matIndex, instanceLeaf + 1, 0)
45
45
  // The path's RNG state, here rather than in a buffer of its own: Shade was at the device's
46
46
  // 10 storage buffers, and the second triangle buffer needed the slot. Extend never writes it.
47
- RNG: 2, // uvec4(rngState, 0, 0, 0)
47
+ // Bidirectional mode keeps the path's MIS partial sums in .y/.z.
48
+ RNG: 2, // uvec4(rngState, bits(dVCM), bits(dVC), 0)
49
+ // Bidirectional only: a camera vertex awaiting its connection, or a light path's emission.
50
+ VERTEX: 3, // 4 slots, a vertex record (see writeVertexRecord)
48
51
  };
49
52
 
53
+ // Bidirectional: the light vertex cache follows the path regions — Shade has no binding left.
54
+ export const HIT_STRIDE_BIDIRECTIONAL = HIT_STRIDE + 4;
55
+ export const LIGHT_VERTEX_STRIDE = 4;
56
+
50
57
  // SoA region stride, baked into the shader graph at build time; single instance, rebuilt on resize.
51
58
  let _cap = 0;
59
+ let _lightVertexBase = 0;
52
60
 
53
61
  const soa = ( id, slot ) => ( slot === 0 ? id : id.add( slot * _cap ) );
54
62
 
@@ -68,9 +76,10 @@ export class PackedRayBuffer {
68
76
  // image resolution and fixed for the device — the image is streamed through the pool in row bands
69
77
  // (see docs/internal/specs/wavefront-chunked-pool.md), so the SoA stride _cap never changes with
70
78
  // resolution and the wavefront kernels are built once (no resize-triggered WGSL regen).
71
- constructor( capacity = 0, renderer = null ) {
79
+ constructor( capacity = 0, renderer = null, lightVertices = 0 ) {
72
80
 
73
81
  this.capacity = 0;
82
+ this.lightVertices = 0;
74
83
  this._renderer = renderer;
75
84
  this._attrs = {};
76
85
 
@@ -78,16 +87,23 @@ export class PackedRayBuffer {
78
87
  this.rayBuffer = null;
79
88
  this.hitBuffer = null;
80
89
 
81
- if ( capacity > 0 ) this.allocate( capacity );
90
+ if ( capacity > 0 ) this.allocate( capacity, lightVertices );
82
91
 
83
92
  }
84
93
 
85
- allocate( capacity ) {
94
+ /**
95
+ * @param {number} capacity - paths in flight
96
+ * @param {number} [lightVertices] - light vertex cache slots; non-zero selects the bidirectional layout
97
+ */
98
+ allocate( capacity, lightVertices = 0 ) {
86
99
 
87
100
  this.dispose();
88
101
 
89
102
  this.capacity = capacity;
103
+ this.lightVertices = lightVertices;
90
104
  _cap = capacity;
105
+ const hitSlots = lightVertices > 0 ? HIT_STRIDE_BIDIRECTIONAL : HIT_STRIDE;
106
+ _lightVertexBase = capacity * hitSlots;
91
107
 
92
108
  // count=0 so StorageBufferNode.getHash() shares the buffer → RW and RO nodes bind the same GPU data.
93
109
  const rayCount = capacity * RAY_STRIDE;
@@ -98,7 +114,7 @@ export class PackedRayBuffer {
98
114
  ro: storage( rayAttr, 'vec4' ).toReadOnly(),
99
115
  };
100
116
 
101
- const hitCount = capacity * HIT_STRIDE;
117
+ const hitCount = capacity * hitSlots + lightVertices * LIGHT_VERTEX_STRIDE;
102
118
  const hitAttr = gpuOnlyStorageAttribute( hitCount, 4, Uint32Array );
103
119
  this._attrs.hit = hitAttr;
104
120
  this.hitBuffer = {
@@ -115,11 +131,11 @@ export class PackedRayBuffer {
115
131
 
116
132
  }
117
133
 
118
- // Reallocates only if a larger budget capacity is requested; returns true if it did.
119
- resize( capacity ) {
134
+ // Reallocates if a larger budget or a different light vertex cache is requested; returns true if it did.
135
+ resize( capacity, lightVertices = 0 ) {
120
136
 
121
- if ( capacity <= this.capacity && this.capacity > 0 ) return false;
122
- this.allocate( capacity );
137
+ if ( capacity <= this.capacity && this.capacity > 0 && lightVertices === this.lightVertices ) return false;
138
+ this.allocate( capacity, lightVertices );
123
139
  return true;
124
140
 
125
141
  }
@@ -132,6 +148,7 @@ export class PackedRayBuffer {
132
148
  this.rayBuffer = null;
133
149
  this.hitBuffer = null;
134
150
  this.capacity = 0;
151
+ this.lightVertices = 0;
135
152
 
136
153
  }
137
154
 
@@ -282,6 +299,96 @@ export const readRngState = ( buf, id ) => buf.element( soa( id, HIT.RNG ) ).x;
282
299
 
283
300
  export const writeRngState = ( buf, id, state ) => buf.element( soa( id, HIT.RNG ) ).assign( uvec4( state, 0, 0, 0 ) );
284
301
 
302
+ /** The subpath's MIS partial sums (Georgiev 2012, "Implementing Vertex Connection and Merging"). */
303
+ export const readMisState = ( buf, id ) => {
304
+
305
+ const v = buf.element( soa( id, HIT.RNG ) );
306
+ return { dVCM: uintBitsToFloat( v.y ), dVC: uintBitsToFloat( v.z ) };
307
+
308
+ };
309
+
310
+ export const writeRngMis = ( buf, id, state, dVCM, dVC ) =>
311
+ buf.element( soa( id, HIT.RNG ) ).assign( uvec4( state, floatBitsToUint( dVCM ), floatBitsToUint( dVC ), uint( 0 ) ) );
312
+
313
+ // A vertex record is four uvec4s:
314
+ // 0 bits(position.xyz), tag
315
+ // 1 bits(throughput.xyz), oct(V) — V points back along the subpath it was reached by
316
+ // 2 oct(N), oct(facet normal), material index | extra << 24, bits(uv.x) — both normals on V's side
317
+ // 3 bits(uv.y), bits(dVCM), bits(dVC), next — the merge grid's list (MergeKernel), in the cache only
318
+ // `at( k )` addresses quad k, in a path's VERTEX slots or in the cache.
319
+ export const pendingVertex = ( id ) => ( k ) => soa( id, HIT.VERTEX + k );
320
+ export const cachedVertex = ( slot ) => ( k ) => uint( slot ).mul( LIGHT_VERTEX_STRIDE ).add( _lightVertexBase + k );
321
+
322
+ export const writeVertexRecord = ( buf, at, v ) => {
323
+
324
+ buf.element( at( 0 ) ).assign( uvec4( floatBitsToUint( v.position ), v.tag ) );
325
+ buf.element( at( 1 ) ).assign( uvec4( floatBitsToUint( v.throughput ), packNormalOct( v.V ) ) );
326
+ buf.element( at( 2 ) ).assign( uvec4( packNormalOct( v.N ), packNormalOct( v.facetN ),
327
+ uint( v.materialIndex ).bitOr( uint( v.extra ).bitAnd( uint( 0xFF ) ).shiftLeft( uint( 24 ) ) ), floatBitsToUint( v.uv.x ) ) );
328
+ buf.element( at( 3 ) ).assign( uvec4( floatBitsToUint( v.uv.y ), floatBitsToUint( v.dVCM ), floatBitsToUint( v.dVC ), uint( 0 ) ) );
329
+
330
+ };
331
+
332
+ export const readVertexTag = ( buf, at ) => buf.element( at( 0 ) ).w;
333
+ export const readVertexPosition = ( buf, at ) => uintBitsToFloat( buf.element( at( 0 ) ).xyz );
334
+ export const readVertexNext = ( buf, at ) => buf.element( at( 3 ) ).w;
335
+ export const writeVertexNext = ( buf, at, next ) => buf.element( at( 3 ) ).w.assign( next );
336
+
337
+ export const readVertexRecord = ( buf, at ) => {
338
+
339
+ const q0 = buf.element( at( 0 ) ).toVar();
340
+ const q1 = buf.element( at( 1 ) ).toVar();
341
+ const q2 = buf.element( at( 2 ) ).toVar();
342
+ const q3 = buf.element( at( 3 ) ).toVar();
343
+ return {
344
+ position: uintBitsToFloat( q0.xyz ),
345
+ tag: q0.w,
346
+ throughput: uintBitsToFloat( q1.xyz ),
347
+ V: unpackTriangleNormal( q1.w ),
348
+ N: unpackTriangleNormal( q2.x ),
349
+ facetN: unpackTriangleNormal( q2.y ),
350
+ materialIndex: q2.z.bitAnd( uint( 0xFFFFFF ) ),
351
+ uv: vec2( uintBitsToFloat( q2.w ), uintBitsToFloat( q3.x ) ),
352
+ dVCM: uintBitsToFloat( q3.y ),
353
+ dVC: uintBitsToFloat( q3.z ),
354
+ extra: q2.z.shiftRight( uint( 24 ) ),
355
+ };
356
+
357
+ };
358
+
359
+ // A light path's vertex count, in its first slot's tag lane, stamped with the frame's tag.
360
+ export const readLightPathLength = ( buf, at, frameTag ) => {
361
+
362
+ const word = buf.element( at( 0 ) ).w;
363
+ return select( word.shiftRight( uint( 8 ) ).equal( frameTag ), word.bitAnd( uint( 0xFF ) ), uint( 0 ) );
364
+
365
+ };
366
+
367
+ export const writeLightPathLength = ( buf, at, frameTag, length ) => {
368
+
369
+ const keep = buf.element( at( 0 ) ).xyz.toVar();
370
+ buf.element( at( 0 ) ).assign( uvec4( keep, frameTag.shiftLeft( uint( 8 ) ).bitOr( uint( length ) ) ) );
371
+
372
+ };
373
+
374
+ /** A light path's point of emission, held in its VERTEX slot until the first hit settles its MIS. */
375
+ export const writeLightOrigin = ( buf, id, triangle, instanceLeaf, cosLight, scale ) =>
376
+ buf.element( soa( id, HIT.VERTEX ) ).assign( uvec4(
377
+ uint( triangle ), uint( int( instanceLeaf ).add( int( 1 ) ) ), floatBitsToUint( cosLight ), floatBitsToUint( scale ),
378
+ ) );
379
+
380
+ export const readLightOrigin = ( buf, id ) => {
381
+
382
+ const v = buf.element( soa( id, HIT.VERTEX ) ).toVar();
383
+ return {
384
+ triangle: int( v.x ),
385
+ instanceLeaf: int( v.y ).sub( int( 1 ) ),
386
+ cosLight: uintBitsToFloat( v.z ),
387
+ scale: uintBitsToFloat( v.w ),
388
+ };
389
+
390
+ };
391
+
285
392
  export const writeHitPacked = ( buf, id, distance, triIndex, baryU, baryV, normal, matIndex, instanceLeaf, facet = uint( 0 ) ) => {
286
393
 
287
394
  buf.element( soa( id, HIT.DIST_TRI_BARY ) )
@@ -31,7 +31,7 @@ const MAX_PARALLEL_WORKERS = 8;
31
31
  * @param {Uint32Array} triangles - Triangle records (20 lanes each)
32
32
  * @param {number} depth - Maximum BVH depth
33
33
  * @param {Function|null} progressCallback - Optional progress callback (0-100)
34
- * @param {Object} config - Builder config (maxLeafSize, numBins, treelet settings, etc.)
34
+ * @param {Object} config - Builder config (maxLeafSize, numBins, reinsertion settings, etc.)
35
35
  * @returns {Promise<{bvhData: Float32Array, bvhRoot: true, order?: Uint32Array, triangles?: Uint32Array, reorderedTriangles?: Uint32Array, splitStats: Object}>}
36
36
  * the parallel build returns the input `triangles` and the BVH `order` of them (record i of the
37
37
  * result is record order[i] of the input); its single-worker fallback returns `reorderedTriangles`.
@@ -192,7 +192,6 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
192
192
  depth,
193
193
  parallelDepth,
194
194
  reportProgress: !! progressCallback,
195
- treeletOptimization: config.treeletOptimization
196
195
  } );
197
196
 
198
197
  } )().catch( ( error ) => {
@@ -413,7 +412,6 @@ async function handlePhase2(
413
412
  numBins: config.numBins,
414
413
  maxBins: config.maxBins,
415
414
  minBins: config.minBins,
416
- treeletConfig: config.treeletOptimization,
417
415
  reinsertionConfig: config.reinsertionOptimization,
418
416
  reportProgress: !! progressCallback
419
417
  } );
@@ -442,7 +440,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
442
440
 
443
441
  worker.onmessage = ( e ) => {
444
442
 
445
- const { bvhData, triangles: transferredTriangles, originalToBvh, error, progress, treeletStats } = e.data;
443
+ const { bvhData, triangles: transferredTriangles, originalToBvh, error, progress, splitStats } = e.data;
446
444
 
447
445
  if ( error ) {
448
446
 
@@ -464,7 +462,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
464
462
  ? new Uint32Array( sharedReorderBuffer )
465
463
  : transferredTriangles;
466
464
 
467
- resolve( { bvhData, bvhRoot: true, reorderedTriangles, originalToBvh: originalToBvh || null, splitStats: treeletStats || {} } );
465
+ resolve( { bvhData, bvhRoot: true, reorderedTriangles, originalToBvh: originalToBvh || null, splitStats: splitStats || {} } );
468
466
 
469
467
  };
470
468
 
@@ -484,7 +482,6 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
484
482
  depth,
485
483
  reportProgress: !! progressCallback,
486
484
  sharedReorderBuffer,
487
- treeletOptimization: config.treeletOptimization,
488
485
  reinsertionOptimization: config.reinsertionOptimization,
489
486
  foldLeaves: config.foldLeaves
490
487
  }, [ transferBuffer ] );
@@ -1,5 +1,5 @@
1
1
  import {
2
- RGBAFormat, RedFormat, FloatType, LinearFilter, RepeatWrapping, ClampToEdgeWrapping,
2
+ RGBAFormat, FloatType, LinearFilter, RepeatWrapping, ClampToEdgeWrapping,
3
3
  EquirectangularReflectionMapping, LinearSRGBColorSpace, DataTexture, Vector3, Vector4,
4
4
  } from 'three';
5
5
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
@@ -50,7 +50,8 @@ export class PhysicalSky {
50
50
  this._texture.wrapS = RepeatWrapping;
51
51
  this._texture.wrapT = ClampToEdgeWrapping;
52
52
  this._texture.generateMipmaps = false;
53
- this._cdfTexture = gpuTexture( width + 1, height, RedFormat );
53
+ // packExactTable's layout (EnvironmentExactTable.js): running sums and guides, RGBA.
54
+ this._cdfTexture = gpuTexture( width + 1, height, RGBAFormat );
54
55
  this._rec709Weights = spectrumToRec709( SKY_RADIANCE_SCALE );
55
56
 
56
57
  this._attrs = null;
@@ -209,7 +210,7 @@ export class PhysicalSky {
209
210
  * @param {number} p.altitude - metres
210
211
  * @param {number} p.sunAngularDiameter - radians
211
212
  * @param {number} [p.sunStrength=1]
212
- * @returns {{ texture: DataTexture, cdfTexture: DataTexture, sun: Object, stats: Promise<{ totalSum: number, compensationDelta: number }> }}
213
+ * @returns {{ texture: DataTexture, cdfTexture: DataTexture, sun: Object, stats: Promise<{ totalSum: number, radianceIntegral: number }> }}
213
214
  */
214
215
  bake( renderer, p ) {
215
216
 
@@ -258,7 +259,7 @@ export class PhysicalSky {
258
259
  const stats = renderer.getArrayBufferAsync( this._attrs.cdfStats ).then( buffer => {
259
260
 
260
261
  const s = new Float32Array( buffer );
261
- return { totalSum: s[ 4 ], compensationDelta: s[ 5 ] };
262
+ return { totalSum: s[ 4 ], radianceIntegral: 2 * Math.PI * Math.PI * s[ 0 ] / ( this.width * this.height ) };
262
263
 
263
264
  } );
264
265
 
@@ -59,6 +59,32 @@ async function fetchBytes( url ) {
59
59
 
60
60
  }
61
61
 
62
+ function imageBytes( parser, def ) {
63
+
64
+ return def.bufferView !== undefined
65
+ ? parser.getDependency( 'bufferView', def.bufferView ).then( ( buffer ) => new Uint8Array( buffer ) )
66
+ : fetchBytes( parser.options.manager.resolveURL( LoaderUtils.resolveURL( def.uri, parser.options.path ) ) );
67
+
68
+ }
69
+
70
+ // GLTFLoader hands an image its extension decodes (KHR_texture_basisu → KTX2Loader) a loader of its own; three's way to
71
+ // it is an object URL through `self.URL`, so here the loader parses the bytes.
72
+ const ownsLoader = ( parser, loader ) => !! loader && loader !== parser.textureLoader && typeof loader.parse === 'function';
73
+
74
+ function loadWithOwnLoader( parser, sourceIndex, loader ) {
75
+
76
+ if ( parser.sourceCache[ sourceIndex ] !== undefined ) return parser.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() );
77
+
78
+ const promise = imageBytes( parser, parser.json.images[ sourceIndex ] ).then( ( bytes ) => new Promise( ( resolve, reject ) => {
79
+
80
+ loader.parse( bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength ), resolve, reject );
81
+
82
+ } ) );
83
+ parser.sourceCache[ sourceIndex ] = promise;
84
+ return promise;
85
+
86
+ }
87
+
62
88
  /**
63
89
  * An image file as a texture, decoded by the platform: the counterpart of TextureLoader for hosts
64
90
  * with no DOM. Rows stay top first with `flipY` on, exactly as TextureLoader leaves an image.
@@ -71,6 +97,27 @@ export async function loadPlatformImage( url, mimeType = mimeTypeOf( { uri: url
71
97
 
72
98
  }
73
99
 
100
+ /**
101
+ * A GLTFLoader plugin for a runtime with no way to decode an image (Node without `decodeImage`): each image fails with
102
+ * what to configure, where three.js's own path threw `self is not defined` and took the whole load down.
103
+ */
104
+ export function missingImageDecoderPlugin( parser, onFailure = null ) {
105
+
106
+ parser.loadImageSource = function ( sourceIndex, loader ) {
107
+
108
+ if ( ownsLoader( this, loader ) ) return loadWithOwnLoader( this, sourceIndex, loader );
109
+
110
+ const def = this.json.images[ sourceIndex ];
111
+ const error = new Error( 'no image decoder in this runtime: configurePlatform( nodePlatform( { decodeImage } ) ) — see "Running in Node"' );
112
+ onFailure?.( def.uri ?? `bufferView ${def.bufferView}`, error );
113
+ return Promise.reject( error );
114
+
115
+ };
116
+
117
+ return { name: 'RAYZEE_missing_image_decoder' };
118
+
119
+ }
120
+
74
121
  /**
75
122
  * A GLTFLoader plugin for hosts with the platform's `decodeImage`: every image of the model, embedded
76
123
  * or not, is decoded by the host and becomes a DataTexture of its pixels, which the texture packer
@@ -82,19 +129,16 @@ export async function loadPlatformImage( url, mimeType = mimeTypeOf( { uri: url
82
129
  */
83
130
  export function platformImagesPlugin( parser, onFailure = null ) {
84
131
 
85
- parser.loadImageSource = function ( sourceIndex ) {
132
+ parser.loadImageSource = function ( sourceIndex, loader ) {
86
133
 
134
+ if ( ownsLoader( this, loader ) ) return loadWithOwnLoader( this, sourceIndex, loader );
87
135
  if ( this.sourceCache[ sourceIndex ] !== undefined ) return this.sourceCache[ sourceIndex ].then( ( texture ) => texture.clone() );
88
136
 
89
137
  const def = this.json.images[ sourceIndex ];
90
138
  const mimeType = mimeTypeOf( def );
91
139
  const where = def.uri ?? `bufferView ${def.bufferView}`;
92
140
 
93
- const bytes = def.bufferView !== undefined
94
- ? this.getDependency( 'bufferView', def.bufferView ).then( ( buffer ) => new Uint8Array( buffer ) )
95
- : fetchBytes( this.options.manager.resolveURL( LoaderUtils.resolveURL( def.uri, this.options.path ) ) );
96
-
97
- const promise = bytes.then( ( data ) => decodeBytes( data, mimeType ) ).then( ( pixels ) => {
141
+ const promise = imageBytes( this, def ).then( ( data ) => decodeBytes( data, mimeType ) ).then( ( pixels ) => {
98
142
 
99
143
  const texture = pixelTexture( pixels, { shared: true } );
100
144
  if ( def.extras && typeof def.extras === 'object' ) Object.assign( texture.userData, def.extras );
@@ -31,9 +31,20 @@ export const COUNTER = {
31
31
  // Survivors' summed throughput (max channel, fixed-point ENERGY_SCALE). The early exit gates on this,
32
32
  // not the count: past Russian roulette a few survivors carry the weight of many.
33
33
  ACTIVE_ENERGY: 7,
34
+ // Words read back; the light guide's escape counts (TSL/LightGuide.js) follow, never read back.
34
35
  COUNT: 8,
36
+ GUIDE: 8,
35
37
  };
36
38
 
39
+ /** The light guide's counts: 16 direction bins × 64 × 64 disc cells (TSL/LightGuide.js). */
40
+ export const GUIDE_COUNTER_WORDS = 16 * 64 * 64;
41
+
42
+ /** Learned light visibility (TSL/LightVisibility.js): tries and visible per cell and light kind, learned then fresh. */
43
+ export const VISIBILITY_BASE = COUNTER.GUIDE + GUIDE_COUNTER_WORDS;
44
+ export const VISIBILITY_CELLS = 1 << 17;
45
+ export const VISIBILITY_KINDS = 4;
46
+ export const VISIBILITY_COUNTER_WORDS = VISIBILITY_CELLS * VISIBILITY_KINDS * 2;
47
+
37
48
  export const ENERGY_SCALE = 64;
38
49
  export const ENERGY_RAY_CLAMP = 65535;
39
50
 
@@ -52,6 +63,9 @@ export const RAY_FLAG = {
52
63
  REDIRECTED: 1 << 18, // bit 18: ray has been redirected (refraction/reflection/SSS/opaque scatter) since the camera, so env it reaches is transported light (sharp), NOT the direct backdrop. NOT set by pure alpha/transparent passthrough (direction unchanged) → env through cutout holes is still the backdrop (blur/intensity/show/color/ground-projection). Set via bitOr only (positive mask) so it never disturbs ACTIVE/bounce bits.
53
64
  SUN_NEE: 1 << 19, // bit 19: the last opaque scatter's sun NEE could have drawn this direction (MIS partner for a sun hit on miss)
54
65
  UNDER_SURFACE: 1 << 20, // bit 20: the last opaque scatter left below its own facet (shading normal allowed it), so the ray culls back faces to leave that shell
66
+ LIGHT_PATH: 1 << 21, // bit 21: bidirectional light subpath (traced from an emitter, gathers nothing)
67
+ LIGHT_EMITTED: 1 << 22, // bit 22: light subpath not yet past its first interaction (its MIS start is still open)
68
+ EMISSION_ONLY: 1 << 23, // bit 23: camera ray past its last bounce, traced only for the light it hits
55
69
  };
56
70
 
57
71
  export class QueueManager {
@@ -64,6 +78,7 @@ export class QueueManager {
64
78
  this._renderer = renderer;
65
79
  this.capacity = 0;
66
80
  this.counters = null;
81
+ this._visibilityTable = false;
67
82
  // A/B alternate: one read by current bounce, other written by compaction
68
83
  this.activeIndices = null;
69
84
  this.activeIndicesRO = null;
@@ -86,14 +101,13 @@ export class QueueManager {
86
101
  this.dispose();
87
102
  this.capacity = capacity;
88
103
 
89
- // explicit attribute (not attributeArray) so it can be referenced for async readback
90
- this._countersAttr = new StorageInstancedBufferAttribute( new Uint32Array( COUNTER.COUNT ), 1 );
91
- this.counters = storage( this._countersAttr, 'uint' ).toAtomic();
104
+ this._allocateCounters();
92
105
 
93
- // per-bounce snapshots for the async readback: [0, MAX) ACTIVE_RAY_COUNT, [MAX, 2·MAX) ACTIVE_ENERGY
106
+ // per-bounce snapshots for the async readback: [0, MAX) ACTIVE_RAY_COUNT, [MAX, 2·MAX) ACTIVE_ENERGY,
107
+ // then the same pair for the bidirectional light pass
94
108
  this.MAX_BOUNCE_SNAPSHOTS = 32;
95
109
  this._bounceCountsAttr = new StorageInstancedBufferAttribute(
96
- new Uint32Array( 2 * this.MAX_BOUNCE_SNAPSHOTS ), 1,
110
+ new Uint32Array( 4 * this.MAX_BOUNCE_SNAPSHOTS ), 1,
97
111
  );
98
112
  this.bounceCounts = storage( this._bounceCountsAttr, 'uint' );
99
113
 
@@ -135,6 +149,26 @@ export class QueueManager {
135
149
 
136
150
  }
137
151
 
152
+ _allocateCounters() {
153
+
154
+ // explicit attribute (not attributeArray) so it can be referenced for async readback
155
+ const words = VISIBILITY_BASE + ( this._visibilityTable ? 2 * VISIBILITY_COUNTER_WORDS : 0 );
156
+ this._countersAttr = new StorageInstancedBufferAttribute( new Uint32Array( words ), 1 );
157
+ this.counters = storage( this._countersAttr, 'uint' ).toAtomic();
158
+
159
+ }
160
+
161
+ /** Room for the learned light visibility in the counter buffer; kernels bound to the old buffer must rebuild. */
162
+ setVisibilityTable( enabled ) {
163
+
164
+ if ( this._visibilityTable === !! enabled ) return;
165
+ this._visibilityTable = !! enabled;
166
+ if ( ! this._countersAttr ) return;
167
+ freeStorageAttribute( this._renderer, this._countersAttr );
168
+ this._allocateCounters();
169
+
170
+ }
171
+
138
172
  // returns true if reallocation occurred
139
173
  resize( capacity ) {
140
174