rayzee 6.0.1 → 6.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 (43) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2419 -2078
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +48 -52
  6. package/dist/rayzee.umd.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/EngineDefaults.js +3 -0
  9. package/src/PathTracerApp.js +18 -8
  10. package/src/Pipeline/RenderStage.js +3 -0
  11. package/src/Processor/IESParser.js +340 -0
  12. package/src/Processor/LightSerializer.js +32 -4
  13. package/src/Processor/SceneProcessor.js +0 -1
  14. package/src/Processor/ShaderBuilder.js +40 -1
  15. package/src/Processor/Workers/TexturesWorker.js +1 -1
  16. package/src/RenderSettings.js +3 -0
  17. package/src/Stages/NormalDepth.js +3 -19
  18. package/src/Stages/PathTracer.js +15 -9
  19. package/src/TSL/BVHTraversal.js +4 -6
  20. package/src/TSL/Common.js +1 -1
  21. package/src/TSL/Debugger.js +0 -2
  22. package/src/TSL/EmissiveSampling.js +20 -22
  23. package/src/TSL/Environment.js +60 -14
  24. package/src/TSL/Fresnel.js +13 -4
  25. package/src/TSL/LightsCore.js +238 -5
  26. package/src/TSL/LightsDirect.js +16 -5
  27. package/src/TSL/LightsIndirect.js +4 -37
  28. package/src/TSL/LightsSampling.js +119 -185
  29. package/src/TSL/MaterialEvaluation.js +25 -14
  30. package/src/TSL/MaterialProperties.js +14 -34
  31. package/src/TSL/MaterialTransmission.js +18 -37
  32. package/src/TSL/PathTracer.js +5 -4
  33. package/src/TSL/PathTracerCore.js +144 -139
  34. package/src/TSL/Struct.js +7 -1
  35. package/src/TSL/TextureSampling.js +2 -2
  36. package/src/index.js +2 -0
  37. package/src/managers/AnimationManager.js +3 -6
  38. package/src/managers/DenoisingManager.js +1 -1
  39. package/src/managers/GoboManager.js +277 -0
  40. package/src/managers/IESManager.js +268 -0
  41. package/src/managers/LightManager.js +33 -1
  42. package/src/managers/TransformManager.js +3 -3
  43. package/src/managers/UniformManager.js +5 -5
@@ -0,0 +1,277 @@
1
+ import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
2
+
3
+ /**
4
+ * Manages projection masks ("gobos" / "cookies") for spot lights.
5
+ *
6
+ * Loads a library of grayscale images into a single GPU `DataArrayTexture`;
7
+ * spot lights reference layers by index via `light.userData.gobo`. The TSL
8
+ * shader projects the surface direction onto the light's local plane and
9
+ * multiplies emission by the mask, producing classic "light through a window"
10
+ * or dappled-foliage shadows without any extra geometry.
11
+ *
12
+ * Library layers must all share the same square resolution (default 256×256).
13
+ * Images are rasterised through a 2D canvas so non-square sources scale to fit.
14
+ *
15
+ * Usage:
16
+ * ```js
17
+ * const entries = await app.goboManager.loadLibrary( [
18
+ * { name: 'window', url: '/lightmasks/window_a.png' },
19
+ * { name: 'leaves', url: '/lightmasks/foliage_canopy_a.png' },
20
+ * ] );
21
+ * app.goboManager.setSpotLightGobo( spotLight.uuid, 'window', 1.0 );
22
+ * ```
23
+ */
24
+ export class GoboManager {
25
+
26
+ /**
27
+ * @param {import('../Stages/PathTracer.js').PathTracer} pathTracer
28
+ * @param {Object} [options]
29
+ * @param {Function} [options.onReset] - Called after gobo state changes so the host can reset accumulation
30
+ */
31
+ constructor( pathTracer, options = {} ) {
32
+
33
+ this.pathTracer = pathTracer;
34
+ this._onReset = options.onReset || null;
35
+
36
+ /** @type {DataArrayTexture | null} */
37
+ this.texture = null;
38
+
39
+ /** @type {Array<{ name: string, index: number }>} */
40
+ this.entries = [];
41
+
42
+ this._size = 256;
43
+
44
+ }
45
+
46
+ /**
47
+ * Load a list of gobo images into a single DataArrayTexture.
48
+ * Replaces any previously loaded library.
49
+ *
50
+ * @param {Array<{ name: string, url: string }>} items
51
+ * @param {Object} [options]
52
+ * @param {number} [options.size=256] - Per-layer square resolution
53
+ * @returns {Promise<Array<{ name: string, index: number }>>}
54
+ */
55
+ async loadLibrary( items, { size = 256 } = {} ) {
56
+
57
+ if ( ! Array.isArray( items ) || items.length === 0 ) return [];
58
+
59
+ this._size = size;
60
+ const images = await Promise.all( items.map( it => loadImage( it.url ) ) );
61
+
62
+ const width = size;
63
+ const height = size;
64
+ const depth = images.length;
65
+ const data = new Uint8Array( width * height * depth * 4 );
66
+
67
+ const cnv = document.createElement( 'canvas' );
68
+ cnv.width = width;
69
+ cnv.height = height;
70
+ const ctx = cnv.getContext( '2d', { willReadFrequently: true } );
71
+
72
+ for ( let i = 0; i < depth; i ++ ) {
73
+
74
+ ctx.clearRect( 0, 0, width, height );
75
+ ctx.drawImage( images[ i ], 0, 0, width, height );
76
+ const img = ctx.getImageData( 0, 0, width, height );
77
+ data.set( img.data, i * width * height * 4 );
78
+
79
+ }
80
+
81
+ const tex = new DataArrayTexture( data, width, height, depth );
82
+ tex.minFilter = LinearFilter;
83
+ tex.magFilter = LinearFilter;
84
+ tex.format = RGBAFormat;
85
+ tex.type = UnsignedByteType;
86
+ tex.generateMipmaps = false;
87
+ tex.needsUpdate = true;
88
+
89
+ const old = this.texture;
90
+ this.texture = tex;
91
+ this.entries = items.map( ( it, i ) => ( { name: it.name, index: i } ) );
92
+
93
+ // Hand the new texture to the shader graph and refresh light data.
94
+ this.pathTracer.goboMaps = tex;
95
+ this.pathTracer.shaderBuilder?.updateGoboMaps?.( tex );
96
+
97
+ // Free old GPU memory after the new one is bound.
98
+ old?.dispose?.();
99
+
100
+ return this.entries;
101
+
102
+ }
103
+
104
+ /**
105
+ * Returns the loaded library entries (name → index lookup).
106
+ * @returns {Array<{ name: string, index: number }>}
107
+ */
108
+ getEntries() {
109
+
110
+ return this.entries.slice();
111
+
112
+ }
113
+
114
+ /**
115
+ * Assign a gobo to a spot or directional light by uuid.
116
+ *
117
+ * @param {string} uuid - Light's uuid
118
+ * @param {string | null} name - Gobo entry name (or null to clear)
119
+ * @param {Object} [opts]
120
+ * @param {number} [opts.intensity=1.0] - Mask strength [0,1]
121
+ * @param {boolean} [opts.inverted=false] - If true, sample (1 - mask)
122
+ * @param {number} [opts.scale=5.0] - World units per gobo tile (directional only)
123
+ * @returns {boolean} True if the light was found and updated
124
+ */
125
+ setLightGobo( uuid, name, opts = {} ) {
126
+
127
+ const { intensity = 1.0, inverted = false, scale = 5.0 } = opts;
128
+ const light = this._findGoboLight( uuid );
129
+ if ( ! light ) return false;
130
+
131
+ light.userData = light.userData || {};
132
+
133
+ if ( name == null ) {
134
+
135
+ delete light.userData.gobo;
136
+
137
+ } else {
138
+
139
+ const entry = this.entries.find( e => e.name === name );
140
+ if ( ! entry ) {
141
+
142
+ console.warn( `GoboManager: unknown gobo "${name}"` );
143
+ return false;
144
+
145
+ }
146
+
147
+ light.userData.gobo = {
148
+ name: entry.name,
149
+ index: entry.index,
150
+ intensity: clamp01( intensity ),
151
+ inverted: !! inverted,
152
+ scale: Math.max( 1e-4, scale ),
153
+ };
154
+
155
+ }
156
+
157
+ this.pathTracer.updateLights();
158
+ this._onReset?.();
159
+ return true;
160
+
161
+ }
162
+
163
+ /**
164
+ * Toggle inversion on an existing gobo assignment without losing
165
+ * the chosen mask or intensity.
166
+ * @param {string} uuid
167
+ * @param {boolean} inverted
168
+ * @returns {boolean}
169
+ */
170
+ setLightGoboInverted( uuid, inverted ) {
171
+
172
+ const light = this._findGoboLight( uuid );
173
+ if ( ! light?.userData?.gobo ) return false;
174
+
175
+ light.userData.gobo.inverted = !! inverted;
176
+ this.pathTracer.updateLights();
177
+ this._onReset?.();
178
+ return true;
179
+
180
+ }
181
+
182
+ /**
183
+ * Update the gobo projection scale for a directional light without
184
+ * touching the chosen mask, intensity, or inverted flag.
185
+ * @param {string} uuid
186
+ * @param {number} scale
187
+ * @returns {boolean}
188
+ */
189
+ setLightGoboScale( uuid, scale ) {
190
+
191
+ const light = this._findGoboLight( uuid );
192
+ if ( ! light?.userData?.gobo ) return false;
193
+
194
+ light.userData.gobo.scale = Math.max( 1e-4, scale );
195
+ this.pathTracer.updateLights();
196
+ this._onReset?.();
197
+ return true;
198
+
199
+ }
200
+
201
+ /**
202
+ * Returns the gobo descriptor currently assigned to a light, or null.
203
+ * @param {string} uuid
204
+ */
205
+ getLightGobo( uuid ) {
206
+
207
+ const light = this._findGoboLight( uuid );
208
+ return light?.userData?.gobo || null;
209
+
210
+ }
211
+
212
+ // ── Back-compat thin wrappers ─────────────────────────────────────
213
+
214
+ setSpotLightGobo( uuid, name, intensity = 1.0, inverted = false ) {
215
+
216
+ return this.setLightGobo( uuid, name, { intensity, inverted } );
217
+
218
+ }
219
+
220
+ setSpotLightGoboInverted( uuid, inverted ) {
221
+
222
+ return this.setLightGoboInverted( uuid, inverted );
223
+
224
+ }
225
+
226
+ getSpotLightGobo( uuid ) {
227
+
228
+ return this.getLightGobo( uuid );
229
+
230
+ }
231
+
232
+ dispose() {
233
+
234
+ this.texture?.dispose?.();
235
+ this.texture = null;
236
+ this.entries = [];
237
+ this.pathTracer = null;
238
+ this._onReset = null;
239
+
240
+ }
241
+
242
+ _findGoboLight( uuid ) {
243
+
244
+ const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
245
+ if ( ! obj ) return null;
246
+ return ( obj.isSpotLight || obj.isDirectionalLight ) ? obj : null;
247
+
248
+ }
249
+
250
+ _findSpotLight( uuid ) {
251
+
252
+ const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
253
+ return obj && obj.isSpotLight ? obj : null;
254
+
255
+ }
256
+
257
+ }
258
+
259
+ function loadImage( url ) {
260
+
261
+ return new Promise( ( resolve, reject ) => {
262
+
263
+ const img = new Image();
264
+ img.crossOrigin = 'anonymous';
265
+ img.onload = () => resolve( img );
266
+ img.onerror = () => reject( new Error( `Failed to load gobo image: ${url}` ) );
267
+ img.src = url;
268
+
269
+ } );
270
+
271
+ }
272
+
273
+ function clamp01( v ) {
274
+
275
+ return Math.max( 0, Math.min( 1, v ) );
276
+
277
+ }
@@ -0,0 +1,268 @@
1
+ import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
2
+ import { parseIES, resampleIESToGrid, deriveIESBeamAngle, deriveIESPenumbra } from '../Processor/IESParser.js';
3
+
4
+ /**
5
+ * Manages IES photometric profiles for spot lights.
6
+ *
7
+ * Pulls .ies files over HTTP, parses each into an angular candela grid,
8
+ * resamples to a fixed-size 2D texture (U = horizontal angle, V = vertical
9
+ * angle), and stacks all profiles into a single `DataArrayTexture` referenced
10
+ * per-light through `light.userData.ies = { name, index, intensity }`.
11
+ *
12
+ * Shader sampling math lives in `LightsCore.js (sampleIESProfile)`.
13
+ *
14
+ * Usage:
15
+ * ```js
16
+ * const entries = await app.iesManager.loadLibrary([
17
+ * { name: 'parallel-beam', url: '/iesprofiles/parallel-beam.ies' },
18
+ * ]);
19
+ * app.iesManager.setSpotLightProfile(spotLight.uuid, 'parallel-beam', 1.0);
20
+ * ```
21
+ */
22
+ export class IESManager {
23
+
24
+ /**
25
+ * @param {import('../Stages/PathTracer.js').PathTracer} pathTracer
26
+ * @param {Object} [options]
27
+ * @param {Function} [options.onReset] - reset accumulation after assignment changes
28
+ */
29
+ constructor( pathTracer, options = {} ) {
30
+
31
+ this.pathTracer = pathTracer;
32
+ this._onReset = options.onReset || null;
33
+
34
+ /** @type {DataArrayTexture | null} */
35
+ this.texture = null;
36
+
37
+ /** @type {Array<{ name: string, index: number, maxCandela: number, photometricType: number }>} */
38
+ this.entries = [];
39
+
40
+ this._gridWidth = 128; // horizontal samples (U axis)
41
+ this._gridHeight = 128; // vertical samples (V axis)
42
+
43
+ }
44
+
45
+ /**
46
+ * Load a list of IES profiles. Replaces any previously loaded library.
47
+ *
48
+ * @param {Array<{ name: string, url: string }>} items
49
+ * @param {Object} [options]
50
+ * @param {number} [options.gridWidth=128]
51
+ * @param {number} [options.gridHeight=128]
52
+ * @returns {Promise<Array<{ name: string, index: number }>>}
53
+ */
54
+ async loadLibrary( items, { gridWidth = 128, gridHeight = 128 } = {} ) {
55
+
56
+ if ( ! Array.isArray( items ) || items.length === 0 ) return [];
57
+
58
+ this._gridWidth = gridWidth;
59
+ this._gridHeight = gridHeight;
60
+
61
+ // Fetch + parse in parallel; tolerate individual failures.
62
+ const results = await Promise.all( items.map( async ( it ) => {
63
+
64
+ try {
65
+
66
+ const res = await fetch( it.url );
67
+ if ( ! res.ok ) throw new Error( `HTTP ${res.status}` );
68
+ const text = await res.text();
69
+ const profile = parseIES( text, it.name );
70
+ const grid = resampleIESToGrid( profile, gridWidth, gridHeight );
71
+ return { it, profile, grid };
72
+
73
+ } catch ( err ) {
74
+
75
+ console.warn( `IESManager: failed to load "${it.name}": ${err.message}` );
76
+ return null;
77
+
78
+ }
79
+
80
+ } ) );
81
+
82
+ const okResults = results.filter( r => r !== null );
83
+ if ( okResults.length === 0 ) return [];
84
+
85
+ const depth = okResults.length;
86
+ const pixelsPerLayer = gridWidth * gridHeight;
87
+ // Expand single-channel grid to RGBA so the texture format matches the
88
+ // placeholder bound at shader-compile time (DataArrayTexture rebinds
89
+ // require matching format). R channel carries the value; G/B copy R for
90
+ // safer linear filtering, A=255.
91
+ const data = new Uint8Array( pixelsPerLayer * 4 * depth );
92
+
93
+ const entries = [];
94
+ for ( let i = 0; i < depth; i ++ ) {
95
+
96
+ const grid = okResults[ i ].grid;
97
+ const dst = i * pixelsPerLayer * 4;
98
+ for ( let p = 0; p < pixelsPerLayer; p ++ ) {
99
+
100
+ const v = grid[ p ];
101
+ data[ dst + p * 4 + 0 ] = v;
102
+ data[ dst + p * 4 + 1 ] = v;
103
+ data[ dst + p * 4 + 2 ] = v;
104
+ data[ dst + p * 4 + 3 ] = 255;
105
+
106
+ }
107
+
108
+ const profile = okResults[ i ].profile;
109
+ const suggestedAngle = deriveIESBeamAngle( profile );
110
+ entries.push( {
111
+ name: okResults[ i ].it.name,
112
+ index: i,
113
+ maxCandela: profile.maxCandela,
114
+ photometricType: profile.photometricType,
115
+ suggestedAngle,
116
+ suggestedPenumbra: deriveIESPenumbra( profile, suggestedAngle ),
117
+ lumens: profile.lumens,
118
+ } );
119
+
120
+ }
121
+
122
+ const tex = new DataArrayTexture( data, gridWidth, gridHeight, depth );
123
+ tex.format = RGBAFormat;
124
+ tex.type = UnsignedByteType;
125
+ tex.minFilter = LinearFilter;
126
+ tex.magFilter = LinearFilter;
127
+ tex.generateMipmaps = false;
128
+ tex.needsUpdate = true;
129
+
130
+ const old = this.texture;
131
+ this.texture = tex;
132
+ this.entries = entries;
133
+
134
+ // Hand the texture to the path tracer's shader graph.
135
+ this.pathTracer.iesProfiles = tex;
136
+ this.pathTracer.shaderBuilder?.updateIESProfiles?.( tex );
137
+
138
+ old?.dispose?.();
139
+
140
+ return this.entries.map( ( { name, index } ) => ( { name, index } ) );
141
+
142
+ }
143
+
144
+ /**
145
+ * Returns the loaded library entries.
146
+ */
147
+ getEntries() {
148
+
149
+ return this.entries.slice();
150
+
151
+ }
152
+
153
+ /**
154
+ * Assign or clear an IES profile on a spot light.
155
+ *
156
+ * When `applyAutoCone` is true (default), the spot light's photometrically
157
+ * meaningful parameters are derived from the profile and applied:
158
+ * - cone half-angle (snug clip outside the IES emission)
159
+ * - penumbra (transition band matching the IES soft edge)
160
+ * - decay (forced to 2 — physically correct inverse-square)
161
+ *
162
+ * @param {string} uuid
163
+ * @param {string | null} name - profile name (or null to clear)
164
+ * @param {number} [intensity=1.0] - blend [0,1] between flat (0) and full profile (1)
165
+ * @param {Object} [opts]
166
+ * @param {boolean} [opts.applyAutoCone=true]
167
+ * @returns {{ applied: boolean, suggestedAngle: number | null, suggestedPenumbra: number | null, suggestedDecay: number | null, fixtureLumens: number | null }}
168
+ * host can mirror the suggested values into UI state.
169
+ */
170
+ setSpotLightProfile( uuid, name, intensity = 1.0, { applyAutoCone = true } = {} ) {
171
+
172
+ const light = this._findSpotLight( uuid );
173
+ const empty = { applied: false, suggestedAngle: null, suggestedPenumbra: null, suggestedDecay: null, fixtureLumens: null };
174
+ if ( ! light ) return empty;
175
+
176
+ light.userData = light.userData || {};
177
+ let suggestedAngle = null;
178
+ let suggestedPenumbra = null;
179
+ let suggestedDecay = null;
180
+ let fixtureLumens = null;
181
+
182
+ if ( name == null ) {
183
+
184
+ delete light.userData.ies;
185
+
186
+ } else {
187
+
188
+ const entry = this.entries.find( e => e.name === name );
189
+ if ( ! entry ) {
190
+
191
+ console.warn( `IESManager: unknown profile "${name}"` );
192
+ return empty;
193
+
194
+ }
195
+
196
+ fixtureLumens = Number.isFinite( entry.lumens ) ? entry.lumens : null;
197
+
198
+ light.userData.ies = {
199
+ name: entry.name,
200
+ index: entry.index,
201
+ intensity: clamp01( intensity ),
202
+ fixtureLumens,
203
+ };
204
+
205
+ if ( applyAutoCone ) {
206
+
207
+ if ( Number.isFinite( entry.suggestedAngle ) ) {
208
+
209
+ light.angle = entry.suggestedAngle;
210
+ suggestedAngle = entry.suggestedAngle;
211
+
212
+ }
213
+
214
+ if ( Number.isFinite( entry.suggestedPenumbra ) ) {
215
+
216
+ light.penumbra = entry.suggestedPenumbra;
217
+ suggestedPenumbra = entry.suggestedPenumbra;
218
+
219
+ }
220
+
221
+ // IES is a photometric measurement that assumes inverse-square falloff.
222
+ light.decay = 2;
223
+ suggestedDecay = 2;
224
+
225
+ }
226
+
227
+ }
228
+
229
+ this.pathTracer.updateLights();
230
+ this._onReset?.();
231
+ return { applied: true, suggestedAngle, suggestedPenumbra, suggestedDecay, fixtureLumens };
232
+
233
+ }
234
+
235
+ /**
236
+ * Returns the current IES descriptor on a spot light (or null).
237
+ */
238
+ getSpotLightProfile( uuid ) {
239
+
240
+ const light = this._findSpotLight( uuid );
241
+ return light?.userData?.ies || null;
242
+
243
+ }
244
+
245
+ dispose() {
246
+
247
+ this.texture?.dispose?.();
248
+ this.texture = null;
249
+ this.entries = [];
250
+ this.pathTracer = null;
251
+ this._onReset = null;
252
+
253
+ }
254
+
255
+ _findSpotLight( uuid ) {
256
+
257
+ const obj = this.pathTracer?.scene?.getObjectByProperty?.( 'uuid', uuid );
258
+ return obj && obj.isSpotLight ? obj : null;
259
+
260
+ }
261
+
262
+ }
263
+
264
+ function clamp01( v ) {
265
+
266
+ return Math.max( 0, Math.min( 1, v ) );
267
+
268
+ }
@@ -129,11 +129,14 @@ export class LightManager extends EventDispatcher {
129
129
  }
130
130
 
131
131
  /**
132
- * Reprocesses all scene lights and updates the path tracer uniform buffers.
132
+ * Reprocesses all scene lights and updates the path tracer uniform buffers,
133
+ * and refreshes any visible helper gizmos so they reflect parameter changes
134
+ * (intensity, cone angle, position, target, distance) without rebuilding.
133
135
  */
134
136
  updateLights() {
135
137
 
136
138
  this.pathTracer?.updateLights();
139
+ if ( this.sceneHelpers?.visible ) this.sceneHelpers.update();
137
140
 
138
141
  }
139
142
 
@@ -332,6 +335,35 @@ export class LightManager extends EventDispatcher {
332
335
  } else if ( light.type === 'SpotLight' && light.target ) {
333
336
 
334
337
  descriptor.target = [ light.target.position.x, light.target.position.y, light.target.position.z ];
338
+ descriptor.distance = light.distance ?? 0;
339
+ descriptor.penumbra = light.penumbra ?? 0;
340
+ descriptor.decay = light.decay ?? 2;
341
+
342
+ } else if ( light.type === 'PointLight' ) {
343
+
344
+ descriptor.distance = light.distance ?? 0;
345
+ descriptor.decay = light.decay ?? 2;
346
+
347
+ }
348
+
349
+ if ( ( light.type === 'SpotLight' || light.type === 'DirectionalLight' ) && light.userData?.gobo ) {
350
+
351
+ descriptor.gobo = light.userData.gobo.name;
352
+ descriptor.goboIntensity = light.userData.gobo.intensity;
353
+ descriptor.goboInverted = !! light.userData.gobo.inverted;
354
+ if ( light.type === 'DirectionalLight' ) {
355
+
356
+ descriptor.goboScale = light.userData.gobo.scale ?? 5.0;
357
+
358
+ }
359
+
360
+ }
361
+
362
+ if ( light.type === 'SpotLight' && light.userData?.ies ) {
363
+
364
+ descriptor.ies = light.userData.ies.name;
365
+ descriptor.iesIntensity = light.userData.ies.intensity ?? 1.0;
366
+ descriptor.fixtureLumens = light.userData.ies.fixtureLumens ?? null;
335
367
 
336
368
  }
337
369
 
@@ -49,7 +49,7 @@ export class TransformManager {
49
49
  * Provide mesh data from SceneProcessor after scene load.
50
50
  * Required for position extraction during BVH refit.
51
51
  */
52
- setMeshData( meshes, triangleCount ) {
52
+ setMeshData( meshes ) {
53
53
 
54
54
  this._meshes = meshes;
55
55
  this._meshTriRanges = [];
@@ -72,8 +72,8 @@ export class TransformManager {
72
72
 
73
73
  }
74
74
 
75
- this._posBuffer = new Float32Array( triangleCount * 9 );
76
- this._normalBuffer = new Float32Array( triangleCount * 9 );
75
+ this._posBuffer = new Float32Array( offset * 9 );
76
+ this._normalBuffer = new Float32Array( offset * 9 );
77
77
 
78
78
  }
79
79
 
@@ -184,6 +184,9 @@ export class UniformManager {
184
184
  u( 'envTotalSum', 0.0, 'float' );
185
185
  u( 'envCompensationDelta', 0.0, 'float' );
186
186
  u( 'envResolution', new Vector2( 1, 1 ), 'vec2' );
187
+ ub( 'groundProjectionEnabled', DEFAULT_STATE.groundProjectionEnabled );
188
+ u( 'groundProjectionRadius', DEFAULT_STATE.groundProjectionRadius, 'float' );
189
+ u( 'groundProjectionHeight', DEFAULT_STATE.groundProjectionHeight, 'float' );
187
190
 
188
191
  // Sun parameters
189
192
  u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
@@ -202,10 +205,10 @@ export class UniformManager {
202
205
 
203
206
  // Light buffer nodes - pre-allocate for up to 16 lights per type (shader hard cap)
204
207
  this._lightBuffers = {
205
- directional: uniformArray( new Float32Array( 8 * 16 ), 'float' ),
208
+ directional: uniformArray( new Float32Array( 12 * 16 ), 'float' ),
206
209
  area: uniformArray( new Float32Array( 13 * 16 ), 'float' ),
207
210
  point: uniformArray( new Float32Array( 9 * 16 ), 'float' ),
208
- spot: uniformArray( new Float32Array( 14 * 16 ), 'float' ),
211
+ spot: uniformArray( new Float32Array( 20 * 16 ), 'float' ),
209
212
  };
210
213
 
211
214
  // Camera matrices
@@ -249,9 +252,6 @@ export class UniformManager {
249
252
  // Resolution (for RNG seeding)
250
253
  u( 'resolution', new Vector2( width, height ), 'vec2' );
251
254
 
252
- // Scene data
253
- u( 'totalTriangleCount', 0, 'int' );
254
-
255
255
  }
256
256
 
257
257
  /**