rayzee 6.5.0 → 7.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 (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7624 -7063
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +26 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +291 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +151 -78
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +111 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +876 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
@@ -0,0 +1,1451 @@
1
+ import { storage } from 'three/tsl';
2
+ import { StorageInstancedBufferAttribute } from 'three/webgpu';
3
+ import {
4
+ NearestFilter, Vector2, Matrix4,
5
+ TextureLoader, RepeatWrapping
6
+ } from 'three';
7
+ import { stbnScalarTextureNode, stbnVec2TextureNode } from '../TSL/Random.js';
8
+
9
+ // Pipeline system
10
+ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
11
+
12
+ // Managers (renderer-agnostic)
13
+ import { CameraOptimizer } from '../Processor/CameraOptimizer.js';
14
+ import { createPerformanceMonitor, calculateAccumulationAlpha, updateCompletionThreshold } from '../Processor/utils.js';
15
+ import { StorageTexturePool } from '../Processor/StorageTexturePool.js';
16
+ import { UniformManager } from '../managers/UniformManager.js';
17
+ import { MaterialDataManager } from '../managers/MaterialDataManager.js';
18
+ import { EnvironmentManager } from '../managers/EnvironmentManager.js';
19
+ import { ShaderBuilder } from '../Processor/ShaderBuilder.js';
20
+
21
+ // Scene building
22
+ import { SceneProcessor } from '../Processor/SceneProcessor.js';
23
+ import { LightSerializer } from '../Processor/LightSerializer';
24
+
25
+ // Constants
26
+ import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
27
+ import { getAssetConfig } from '../AssetConfig.js';
28
+
29
+ /**
30
+ * Data layout constants
31
+ */
32
+ const BVH_VEC4_PER_NODE = 4;
33
+
34
+ /**
35
+ * Path Tracing Stage for WebGPU.
36
+ *
37
+ * Full-featured path tracing stage:
38
+ * - BVH-accelerated ray traversal
39
+ * - GGX/Diffuse BSDF sampling
40
+ * - Environment lighting with importance sampling
41
+ * - Progressive and tiled accumulation
42
+ * - MRT outputs for denoising (normal/depth, albedo)
43
+ * - Camera interaction mode optimization
44
+ * - Event-driven pipeline communication
45
+ *
46
+ * Events emitted:
47
+ * - pathtracer:frameComplete - When a frame finishes rendering
48
+ * - camera:moved - When camera position/orientation changes
49
+ * - asvgf:reset - Request ASVGF to reset temporal data
50
+ * - asvgf:updateParameters - Update ASVGF parameters
51
+ * - asvgf:setTemporal - Enable/disable ASVGF temporal accumulation
52
+ *
53
+ * Textures published to context:
54
+ * - pathtracer:color - Main color output
55
+ * - pathtracer:normalDepth - Normal/depth buffer
56
+ */
57
+ export class PathTracerStage extends RenderStage {
58
+
59
+ /**
60
+ * @param {WebGPURenderer} renderer - Three.js WebGPU renderer
61
+ * @param {Scene} scene - Three.js scene
62
+ * @param {PerspectiveCamera} camera - Three.js camera
63
+ * @param {Object} options - Configuration options
64
+ */
65
+ constructor( renderer, scene, camera, options = {} ) {
66
+
67
+ super( 'PathTracer', {
68
+ ...options,
69
+ executionMode: StageExecutionMode.ALWAYS
70
+ } );
71
+
72
+ const width = options.width || 1920;
73
+ const height = options.height || 1080;
74
+
75
+ this.camera = camera;
76
+ this.width = width;
77
+ this.height = height;
78
+ this.renderer = renderer;
79
+ this.scene = scene;
80
+
81
+ // Scene building
82
+ this.sdfs = new SceneProcessor();
83
+ this.lightSerializer = new LightSerializer();
84
+
85
+ // State management
86
+ this.accumulationEnabled = true;
87
+ this.isComplete = false;
88
+ this.cameras = [];
89
+ // Performance monitoring
90
+ this.performanceMonitor = createPerformanceMonitor();
91
+ this.completionThreshold = 0;
92
+ this.renderLimitMode = 'frames';
93
+
94
+ // Initialize data textures
95
+ this._initDataTextures();
96
+
97
+ // Initialize storage texture pool (ping-pong compute output)
98
+ this.storageTextures = new StorageTexturePool( 0, 0 );
99
+
100
+ // Initialize uniforms via UniformManager
101
+ this.uniforms = new UniformManager( width, height );
102
+
103
+ // Define getters for every uniform so that this.maxBounces, this.frame, etc.
104
+ // return the uniform node (backward-compat with this.X.value pattern).
105
+ this._defineUniformGetters();
106
+
107
+ // Initialize material data manager
108
+ this.materialData = new MaterialDataManager( this.sdfs );
109
+ this.materialData.callbacks.onReset = () => this.reset();
110
+ // Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The
111
+ // authoritative CPU array is triangleStorageAttr.array (not sdfs.triangleData,
112
+ // which isn't populated on the PathTracerApp build path). The patch mutates
113
+ // the array in place — only a dirty flag is needed for GPU re-upload.
114
+ this.materialData.callbacks.getTriangleData = () => ( {
115
+ array: this.triangleStorageAttr?.array,
116
+ count: this.triangleCount,
117
+ } );
118
+ this.materialData.callbacks.onTriangleDataChanged = () => {
119
+
120
+ if ( this.triangleStorageAttr ) this.triangleStorageAttr.needsUpdate = true;
121
+
122
+ };
123
+
124
+ // Initialize environment manager
125
+ this.environment = new EnvironmentManager( this.scene, this.uniforms );
126
+ this.environment.callbacks.onReset = () => this.reset();
127
+ this.environment.callbacks.getSceneTextureNodes = () => this.shaderBuilder.getSceneTextureNodes();
128
+
129
+ // Initialize shader composer
130
+ this.shaderBuilder = new ShaderBuilder();
131
+
132
+ // Initialize rendering state
133
+ this._initRenderingState();
134
+
135
+ // Setup blue noise
136
+ this.setupBlueNoise();
137
+
138
+ // Cache frequently used objects
139
+ this.tempVector2 = new Vector2();
140
+ this.lastCameraMatrix = new Matrix4();
141
+ this.lastProjectionMatrix = new Matrix4();
142
+
143
+ // Denoising management state
144
+ this.lastRenderMode = - 1;
145
+ this.renderModeChangeTimeout = null;
146
+ this.renderModeChangeDelay = 50;
147
+ this.pendingRenderMode = null;
148
+
149
+ // Track interaction mode state for accumulation
150
+ this.lastInteractionModeState = false;
151
+
152
+ // Track changes for event emission
153
+ this.cameraChanged = false;
154
+
155
+ // Update completion threshold
156
+ this.updateCompletionThreshold();
157
+
158
+ }
159
+
160
+ /**
161
+ * Initialize data texture references and metadata
162
+ */
163
+ _initDataTextures() {
164
+
165
+ // Triangle data (storage buffer for WebGPU)
166
+ this.triangleStorageAttr = null;
167
+ this.triangleStorageNode = null;
168
+ this.triangleCount = 0;
169
+
170
+ // BVH data (storage buffer for WebGPU)
171
+ this.bvhStorageAttr = null;
172
+ this.bvhStorageNode = null;
173
+ this.bvhNodeCount = 0;
174
+
175
+ // Lights
176
+ this.directionalLightsData = null;
177
+ this.pointLightsData = null;
178
+ this.spotLightsData = null;
179
+ this.areaLightsData = null;
180
+
181
+ // Spot light gobo (projection mask) DataArrayTexture. Owned externally
182
+ // (GoboManager); ShaderBuilder reads via this property at graph build time
183
+ // and refreshes the bound TextureNode in-place when it changes.
184
+ this.goboMaps = null;
185
+
186
+ // Spot light IES photometric profiles DataArrayTexture. Owned externally
187
+ // (IESManager); ShaderBuilder reads via this property at graph build time
188
+ // and refreshes the bound TextureNode in-place when it changes.
189
+ this.iesProfiles = null;
190
+
191
+ // STBN noise textures
192
+ this.stbnScalarTexture = null;
193
+ this.stbnVec2Texture = null;
194
+
195
+ // Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
196
+ // emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
197
+ // Initialized with dummy data so TSL compilation never sees null.
198
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
199
+ this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
200
+
201
+ // Cached CPU-side data — rebuilt into the packed buffer whenever either source changes.
202
+ this._lbvhDataCache = null;
203
+ this._emissiveDataCache = null;
204
+
205
+ // Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
206
+ // (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
207
+ // tlasLeafIndex for each mesh so we can patch visibility in place.
208
+ this._instanceTable = null;
209
+
210
+ // Spheres
211
+ this.spheres = [];
212
+
213
+ }
214
+
215
+ /**
216
+ * Dynamically defines getters for all uniform names so that
217
+ * this.maxBounces, this.frame, etc. return the uniform node.
218
+ * Also defines light buffer node getters.
219
+ * @private
220
+ */
221
+ _defineUniformGetters() {
222
+
223
+ const uniforms = this.uniforms;
224
+
225
+ for ( const name of uniforms.keys() ) {
226
+
227
+ Object.defineProperty( this, name, {
228
+ get: () => uniforms.get( name ),
229
+ configurable: true,
230
+ } );
231
+
232
+ }
233
+
234
+ // Light buffer node getters
235
+ const lightBuffers = uniforms.getLightBufferNodes();
236
+ for ( const [ suffix, node ] of Object.entries( lightBuffers ) ) {
237
+
238
+ Object.defineProperty( this, `${suffix}LightsBufferNode`, {
239
+ get: () => node,
240
+ configurable: true,
241
+ } );
242
+
243
+ }
244
+
245
+ }
246
+
247
+ /**
248
+ * Initialize rendering state
249
+ */
250
+ _initRenderingState() {
251
+
252
+ // State flags
253
+ this.isReady = false;
254
+ this.frameCount = 0;
255
+
256
+ }
257
+
258
+ /**
259
+ * Initialize camera movement optimizer
260
+ */
261
+ _initCameraOptimizer() {
262
+
263
+ // Create adapter interface for TSL uniforms
264
+ const self = this;
265
+ const materialInterface = {
266
+ uniforms: {
267
+ maxBounceCount: {
268
+ get value() {
269
+
270
+ return self.maxBounces.value;
271
+
272
+ },
273
+ set value( v ) {
274
+
275
+ self.maxBounces.value = v;
276
+
277
+ }
278
+ },
279
+ numRaysPerPixel: {
280
+ get value() {
281
+
282
+ return self.samplesPerPixel.value;
283
+
284
+ },
285
+ set value( v ) {
286
+
287
+ self.samplesPerPixel.value = v;
288
+
289
+ }
290
+ },
291
+ useEnvMapIS: {
292
+ get value() {
293
+
294
+ return self.useEnvMapIS.value;
295
+
296
+ },
297
+ set value( v ) {
298
+
299
+ self.useEnvMapIS.value = v;
300
+
301
+ }
302
+ },
303
+ enableAccumulation: {
304
+ get value() {
305
+
306
+ return self.enableAccumulation.value;
307
+
308
+ },
309
+ set value( v ) {
310
+
311
+ self.enableAccumulation.value = v;
312
+
313
+ }
314
+ },
315
+ enableEmissiveTriangleSampling: {
316
+ get value() {
317
+
318
+ return self.enableEmissiveTriangleSampling.value;
319
+
320
+ },
321
+ set value( v ) {
322
+
323
+ self.enableEmissiveTriangleSampling.value = v;
324
+
325
+ }
326
+ },
327
+ cameraIsMoving: {
328
+ get value() {
329
+
330
+ return self.cameraIsMoving.value;
331
+
332
+ },
333
+ set value( v ) {
334
+
335
+ self.cameraIsMoving.value = v;
336
+
337
+ }
338
+ }
339
+ }
340
+ };
341
+
342
+ this.cameraOptimizer = new CameraOptimizer( this.renderer, materialInterface, {
343
+ enabled: DEFAULT_STATE.interactionModeEnabled,
344
+ qualitySettings: {
345
+ maxBounceCount: 1,
346
+ numRaysPerPixel: 1,
347
+ useEnvMapIS: false,
348
+ enableAccumulation: false,
349
+ enableEmissiveTriangleSampling: false,
350
+ },
351
+ onReset: () => {
352
+
353
+ this.reset();
354
+ this.emit( 'pathtracer:viewpointChanged' );
355
+
356
+ }
357
+ } );
358
+
359
+ }
360
+
361
+ /**
362
+ * Load STBN (Spatiotemporal Blue Noise) atlas textures.
363
+ * Each atlas is 1024×1024: 8×8 grid of 128×128 tiles, 64 temporal slices.
364
+ */
365
+ setupBlueNoise() {
366
+
367
+ const loader = new TextureLoader();
368
+ loader.setCrossOrigin( 'anonymous' );
369
+
370
+ const configure = ( tex ) => {
371
+
372
+ tex.minFilter = NearestFilter;
373
+ tex.magFilter = NearestFilter;
374
+ tex.wrapS = RepeatWrapping;
375
+ tex.wrapT = RepeatWrapping;
376
+ tex.generateMipmaps = false;
377
+ return tex;
378
+
379
+ };
380
+
381
+ const { stbnScalarAtlas, stbnVec2Atlas } = getAssetConfig();
382
+
383
+ loader.load( stbnScalarAtlas, ( tex ) => {
384
+
385
+ this.stbnScalarTexture = configure( tex );
386
+ stbnScalarTextureNode.value = tex;
387
+ console.log( `PathTracer: STBN scalar atlas loaded ${tex.image.width}x${tex.image.height}` );
388
+
389
+ } );
390
+
391
+ loader.load( stbnVec2Atlas, ( tex ) => {
392
+
393
+ this.stbnVec2Texture = configure( tex );
394
+ stbnVec2TextureNode.value = tex;
395
+ console.log( `PathTracer: STBN vec2 atlas loaded ${tex.image.width}x${tex.image.height}` );
396
+
397
+ } );
398
+
399
+ }
400
+
401
+ /**
402
+ * Setup event listeners for pipeline events
403
+ */
404
+ setupEventListeners() {
405
+
406
+ this.on( 'pipeline:reset', () => {
407
+
408
+ this.reset();
409
+
410
+ } );
411
+
412
+ this.on( 'pipeline:resize', ( data ) => {
413
+
414
+ if ( data && data.width && data.height ) {
415
+
416
+ this.setSize( data.width, data.height );
417
+
418
+ }
419
+
420
+ } );
421
+
422
+ this.on( 'pathtracer:setCompletionThreshold', ( data ) => {
423
+
424
+ if ( data && data.threshold !== undefined ) {
425
+
426
+ this.completionThreshold = data.threshold;
427
+
428
+ }
429
+
430
+ } );
431
+
432
+ }
433
+
434
+ // ===== PUBLIC API METHODS =====
435
+
436
+ /**
437
+ * Build scene data (BVH, geometry, materials)
438
+ * @param {Object3D} scene - Three.js scene or object
439
+ */
440
+ async build( scene ) {
441
+
442
+ this.dispose();
443
+ this.scene = scene;
444
+
445
+ await this.sdfs.buildBVH( scene );
446
+ this.cameras = this.sdfs.cameras;
447
+
448
+ // Inject shader defines based on detected material features
449
+ this.materialData.injectMaterialFeatureDefines();
450
+
451
+ // Update uniforms with scene data
452
+ this.updateSceneUniforms();
453
+ this.updateLights();
454
+
455
+ // Initialize camera optimizer after scene is built
456
+ this._initCameraOptimizer();
457
+
458
+ // Setup material now that we have scene data
459
+ this.setupMaterial();
460
+
461
+ }
462
+
463
+ /**
464
+ * Update scene uniforms from SceneProcessor data
465
+ */
466
+ updateSceneUniforms() {
467
+
468
+ // Set data references
469
+ this.setTriangleData( this.sdfs.triangleData, this.sdfs.triangleCount );
470
+ this.setBVHData( this.sdfs.bvhData );
471
+ this.setInstanceTable( this.sdfs.instanceTable );
472
+ this.materialData.setMaterialData( this.sdfs.materialData );
473
+
474
+ // Material texture arrays
475
+ this.materialData.loadTexturesFromSdfs();
476
+
477
+ // Emissive triangles (storage buffer)
478
+ if ( this.sdfs.emissiveTriangleData ) {
479
+
480
+ this.setEmissiveTriangleData( this.sdfs.emissiveTriangleData, this.sdfs.emissiveTriangleCount || 0 );
481
+
482
+ } else {
483
+
484
+ this.emissiveTriangleCount.value = 0;
485
+
486
+ }
487
+
488
+ // Light BVH
489
+ if ( this.sdfs.lightBVHNodeData ) {
490
+
491
+ this.setLightBVHData( this.sdfs.lightBVHNodeData, this.sdfs.lightBVHNodeCount || 0 );
492
+
493
+ } else {
494
+
495
+ this.lightBVHNodeCount.value = 0;
496
+
497
+ }
498
+
499
+ // Per-mesh visibility — collect meshes from scene ordered by meshIndex
500
+ this._meshRefs = this._collectMeshRefs( this.scene );
501
+ this.setMeshVisibilityData( this._meshRefs );
502
+
503
+ // Spheres
504
+ this.spheres = this.sdfs.spheres || [];
505
+
506
+ }
507
+
508
+ /**
509
+ * Update lights from scene
510
+ */
511
+ updateLights() {
512
+
513
+ // Process scene lights
514
+ const mockMaterial = {
515
+ uniforms: {
516
+ directionalLights: { value: null },
517
+ pointLights: { value: null },
518
+ spotLights: { value: null },
519
+ areaLights: { value: null }
520
+ },
521
+ defines: {}
522
+ };
523
+
524
+ this.lightSerializer.processSceneLights( this.scene, mockMaterial );
525
+
526
+ // Store light data
527
+ this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
528
+ this.pointLightsData = mockMaterial.uniforms.pointLights.value;
529
+ this.spotLightsData = mockMaterial.uniforms.spotLights.value;
530
+ this.areaLightsData = mockMaterial.uniforms.areaLights.value;
531
+
532
+ // Add sun as directional light if procedural sky is active
533
+ if ( this.hasSun.value ) {
534
+
535
+ const scaledSunIntensity = this.environment.envParams.skySunIntensity * 950.0;
536
+
537
+ const sunLight = {
538
+ intensity: scaledSunIntensity,
539
+ color: { r: 1.0, g: 1.0, b: 1.0 },
540
+ userData: {
541
+ angle: this.sunAngularSize.value
542
+ },
543
+ updateMatrixWorld: () => {},
544
+ getWorldPosition: ( target ) => {
545
+
546
+ const sunDir = this.sunDirection.value;
547
+ return target.set( sunDir.x, sunDir.y, sunDir.z ).multiplyScalar( 1e10 );
548
+
549
+ }
550
+ };
551
+
552
+ this.lightSerializer.addDirectionalLight( sunLight );
553
+ this.lightSerializer.preprocessLights();
554
+ this.lightSerializer.updateShaderUniforms( mockMaterial );
555
+
556
+ this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
557
+
558
+ console.log( `Sun added as directional light (intensity: ${scaledSunIntensity.toFixed( 2 )})` );
559
+
560
+ }
561
+
562
+ // Update TSL uniform buffer nodes from raw Float32Array data
563
+ this._updateLightBufferNodes();
564
+
565
+ }
566
+
567
+ /**
568
+ * Update TSL uniformArray nodes with current light Float32Array data
569
+ */
570
+ _updateLightBufferNodes() {
571
+
572
+ // Directional lights (12 floats per light — 8 light fields + gobo {index, signed intensity, scale, pad})
573
+ if ( this.directionalLightsData && this.directionalLightsData.length > 0 ) {
574
+
575
+ this.directionalLightsBufferNode.array = Array.from( this.directionalLightsData );
576
+ this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 12 );
577
+
578
+ } else {
579
+
580
+ this.numDirectionalLights.value = 0;
581
+
582
+ }
583
+
584
+ // Area lights (13 floats per light)
585
+ if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
586
+
587
+ this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
588
+ this.numAreaLights.value = Math.floor( this.areaLightsData.length / 13 );
589
+
590
+ } else {
591
+
592
+ this.numAreaLights.value = 0;
593
+
594
+ }
595
+
596
+ // Point lights (9 floats per light)
597
+ if ( this.pointLightsData && this.pointLightsData.length > 0 ) {
598
+
599
+ this.pointLightsBufferNode.array = Array.from( this.pointLightsData );
600
+ this.numPointLights.value = Math.floor( this.pointLightsData.length / 9 );
601
+
602
+ } else {
603
+
604
+ this.numPointLights.value = 0;
605
+
606
+ }
607
+
608
+ // Spot lights (20 floats per light — 14 light fields + gobo {idx, signed intensity} + IES {idx, intensity} + 2 reserved)
609
+ if ( this.spotLightsData && this.spotLightsData.length > 0 ) {
610
+
611
+ this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
612
+ this.numSpotLights.value = Math.floor( this.spotLightsData.length / 20 );
613
+
614
+ } else {
615
+
616
+ this.numSpotLights.value = 0;
617
+
618
+ }
619
+
620
+ }
621
+
622
+ /**
623
+ * Reset accumulation
624
+ */
625
+ reset() {
626
+
627
+ this.frameCount = 0;
628
+ this.frame.value = 0;
629
+ this.hasPreviousAccumulated.value = 0;
630
+ this.storageTextures.currentTarget = 0;
631
+
632
+ // Update completion threshold
633
+ this.updateCompletionThreshold();
634
+ this.isComplete = false;
635
+ this.performanceMonitor?.reset();
636
+
637
+ this.lastRenderMode = - 1;
638
+
639
+ this.lastInteractionModeState = false;
640
+
641
+ }
642
+
643
+ /**
644
+ * Set render size
645
+ * @param {number} width
646
+ * @param {number} height
647
+ */
648
+ setSize( width, height ) {
649
+
650
+ this.width = width;
651
+ this.height = height;
652
+
653
+ this.resolution.value.set( width, height );
654
+ this.createStorageTextures( width, height );
655
+
656
+ }
657
+
658
+ /**
659
+ * Set accumulation enabled state
660
+ * @param {boolean} enabled
661
+ */
662
+ setAccumulationEnabled( enabled ) {
663
+
664
+ this.accumulationEnabled = enabled;
665
+ this.enableAccumulation.value = enabled ? 1 : 0;
666
+
667
+ }
668
+
669
+ // ===== MANAGER DELEGATION METHODS =====
670
+
671
+ enterInteractionMode() {
672
+
673
+ this.cameraOptimizer?.enterInteractionMode();
674
+
675
+ }
676
+
677
+ setInteractionModeEnabled( enabled ) {
678
+
679
+ this.cameraOptimizer?.setInteractionModeEnabled( enabled );
680
+
681
+ }
682
+
683
+ // ===== PROPERTY GETTERS =====
684
+
685
+ get interactionMode() {
686
+
687
+ return this.cameraOptimizer?.isInInteractionMode() ?? false;
688
+
689
+ }
690
+
691
+ // ===== TEXTURE SETTERS =====
692
+
693
+ /**
694
+ * Sets the triangle data from raw Float32Array via storage buffer.
695
+ * On first call, creates the storage buffer and node.
696
+ * On subsequent calls, creates a new attribute with the correct size
697
+ * and updates the storage node's value to preserve shader graph references.
698
+ * @param {Float32Array} triangleData - Raw triangle data
699
+ * @param {number} triangleCount - Number of triangles
700
+ */
701
+ setTriangleData( triangleData, triangleCount ) {
702
+
703
+ if ( ! triangleData ) return;
704
+
705
+ const vec4Count = triangleData.length / 4;
706
+
707
+ if ( this.triangleStorageNode ) {
708
+
709
+ // Create new attribute with correct size (old one is GC'd, backend WeakMap cleans up GPU buffer)
710
+ this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
711
+
712
+ // Update storage node references (preserves compiled shader graph)
713
+ this.triangleStorageNode.value = this.triangleStorageAttr;
714
+ this.triangleStorageNode.bufferCount = vec4Count;
715
+
716
+ } else {
717
+
718
+ // First time: create storage buffer and node
719
+ this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
720
+ this.triangleStorageNode = storage( this.triangleStorageAttr, 'vec4', vec4Count ).toReadOnly();
721
+
722
+ }
723
+
724
+ this.triangleCount = triangleCount;
725
+
726
+ console.log( `PathTracer: ${this.triangleCount} triangles (storage buffer)` );
727
+
728
+ }
729
+
730
+ /**
731
+ * Sets the BVH data from raw Float32Array via storage buffer.
732
+ * @param {Float32Array} bvhImageData - Raw BVH data from DataTexture.image.data
733
+ */
734
+ setBVHData( bvhImageData ) {
735
+
736
+ if ( ! bvhImageData ) return;
737
+
738
+ const vec4Count = bvhImageData.length / 4;
739
+
740
+ if ( this.bvhStorageNode ) {
741
+
742
+ this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
743
+ this.bvhStorageNode.value = this.bvhStorageAttr;
744
+ this.bvhStorageNode.bufferCount = vec4Count;
745
+
746
+ } else {
747
+
748
+ this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
749
+ this.bvhStorageNode = storage( this.bvhStorageAttr, 'vec4', vec4Count ).toReadOnly();
750
+
751
+ }
752
+
753
+ this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
754
+ console.log( `PathTracer: ${this.bvhNodeCount} BVH nodes (storage buffer)` );
755
+
756
+ }
757
+
758
+ /**
759
+ * Bind the InstanceTable used to locate each mesh's TLAS leaf for in-place
760
+ * visibility patching. Called by SceneProcessor during upload.
761
+ * @param {import('../Processor/InstanceTable.js').InstanceTable} instanceTable
762
+ */
763
+ setInstanceTable( instanceTable ) {
764
+
765
+ this._instanceTable = instanceTable;
766
+
767
+ }
768
+
769
+ /**
770
+ * Initialize packed visibility for each mesh from current world-visibility.
771
+ * Patches the TLAS leaf slots in the combined BVH buffer that was just uploaded.
772
+ * @param {Array} meshes - Array of Three.js mesh objects, ordered by meshIndex
773
+ */
774
+ setMeshVisibilityData( meshes ) {
775
+
776
+ if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
777
+
778
+ for ( let i = 0; i < meshes.length; i ++ ) {
779
+
780
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( meshes[ i ] ) );
781
+
782
+ }
783
+
784
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
785
+
786
+ }
787
+
788
+ /**
789
+ * Update visibility for a single mesh by patching its TLAS leaf slot [2].
790
+ * @param {number} meshIndex
791
+ * @param {boolean} visible
792
+ */
793
+ updateMeshVisibility( meshIndex, visible ) {
794
+
795
+ if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
796
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
797
+
798
+ }
799
+
800
+ /**
801
+ * Recompute world-visibility for all meshes and patch TLAS leaves in place.
802
+ * Call this when group visibility changes at runtime.
803
+ */
804
+ updateAllMeshVisibility() {
805
+
806
+ if ( ! this._meshRefs || ! this._instanceTable ) return;
807
+
808
+ for ( let i = 0; i < this._meshRefs.length; i ++ ) {
809
+
810
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( this._meshRefs[ i ] ) );
811
+
812
+ }
813
+
814
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
815
+
816
+ }
817
+
818
+ /**
819
+ * Patch a single TLAS leaf's visibility flag in the combined BVH buffer.
820
+ * Returns true if the patch was applied.
821
+ * @private
822
+ */
823
+ _patchTLASLeafVisibility( meshIndex, visible ) {
824
+
825
+ const entry = this._instanceTable?.entries?.[ meshIndex ];
826
+ if ( ! entry || entry.tlasLeafIndex < 0 || ! this.bvhStorageAttr ) return false;
827
+
828
+ entry.visible = visible;
829
+ this.bvhStorageAttr.array[ entry.tlasLeafIndex * 16 + 2 ] = visible ? 1.0 : 0.0;
830
+ return true;
831
+
832
+ }
833
+
834
+ /**
835
+ * Collect mesh references from scene, ordered by meshIndex (assigned during extraction).
836
+ * @param {Object3D} scene
837
+ * @returns {Array}
838
+ * @private
839
+ */
840
+ _collectMeshRefs( scene ) {
841
+
842
+ if ( ! scene ) return [];
843
+
844
+ const meshes = [];
845
+ scene.traverse( obj => {
846
+
847
+ if ( obj.isMesh && obj.userData.meshIndex !== undefined ) {
848
+
849
+ meshes[ obj.userData.meshIndex ] = obj;
850
+
851
+ }
852
+
853
+ } );
854
+
855
+ return meshes;
856
+
857
+ }
858
+
859
+ /**
860
+ * Walk the parent chain to determine world-space visibility.
861
+ * @param {Object3D} object
862
+ * @returns {boolean}
863
+ * @private
864
+ */
865
+ _isWorldVisible( object ) {
866
+
867
+ while ( object ) {
868
+
869
+ if ( ! object.visible ) return false;
870
+ object = object.parent;
871
+
872
+ }
873
+
874
+ return true;
875
+
876
+ }
877
+
878
+ // ===== FAST BUFFER UPDATES (BVH Refit / Animation) =====
879
+
880
+ /**
881
+ * Update an existing GPU storage buffer in-place (no reallocation).
882
+ * @param {StorageInstancedBufferAttribute} attr
883
+ * @param {Float32Array} data
884
+ * @private
885
+ */
886
+ _updateStorageBuffer( attr, data ) {
887
+
888
+ if ( ! attr ) return;
889
+ attr.array.set( data );
890
+ attr.needsUpdate = true;
891
+
892
+ }
893
+
894
+ /** Update triangle positions in the existing GPU buffer (full). */
895
+ updateTriangleData( triangleData ) {
896
+
897
+ this._updateStorageBuffer( this.triangleStorageAttr, triangleData );
898
+
899
+ }
900
+
901
+ /** Update BVH node data in the existing GPU buffer (full). */
902
+ updateBVHData( bvhData ) {
903
+
904
+ this._updateStorageBuffer( this.bvhStorageAttr, bvhData );
905
+
906
+ }
907
+
908
+ /**
909
+ * Update only specific ranges of the GPU storage buffers.
910
+ * Uses addUpdateRange for partial GPU upload instead of full buffer copy.
911
+ *
912
+ * @param {Array<{offset: number, count: number}>} triRanges - Dirty triangle ranges (element index + count)
913
+ * @param {Array<{offset: number, count: number}>} bvhRanges - Dirty BVH node ranges (element index + count)
914
+ */
915
+ updateBufferRanges( triRanges, bvhRanges ) {
916
+
917
+ if ( this.triangleStorageAttr && triRanges.length > 0 ) {
918
+
919
+ this.triangleStorageAttr.clearUpdateRanges();
920
+
921
+ for ( const r of triRanges ) {
922
+
923
+ this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
924
+
925
+ }
926
+
927
+ this.triangleStorageAttr.version ++;
928
+
929
+ }
930
+
931
+ if ( this.bvhStorageAttr && bvhRanges.length > 0 ) {
932
+
933
+ this.bvhStorageAttr.clearUpdateRanges();
934
+
935
+ for ( const r of bvhRanges ) {
936
+
937
+ this.bvhStorageAttr.addUpdateRange( r.offset, r.count );
938
+
939
+ }
940
+
941
+ this.bvhStorageAttr.version ++;
942
+
943
+ }
944
+
945
+ }
946
+
947
+ // ===== STORAGE TEXTURES =====
948
+
949
+ /**
950
+ * Creates storage textures for compute accumulation.
951
+ * @param {number} width
952
+ * @param {number} height
953
+ */
954
+ createStorageTextures( width, height ) {
955
+
956
+ if ( this.storageTextures.writeColor ) {
957
+
958
+ // Resize existing textures — preserves JS object references
959
+ // so the compiled compute node's bindings remain valid
960
+ this.storageTextures.setSize( width, height );
961
+
962
+ } else {
963
+
964
+ // Initial creation
965
+ this.storageTextures.create( width, height );
966
+
967
+ }
968
+
969
+ // Update resolution uniform
970
+ this.resolution.value.set( width, height );
971
+
972
+ }
973
+
974
+ // ===== MATERIAL SETUP =====
975
+
976
+ /**
977
+ * Creates the path tracing material and quad.
978
+ * On subsequent calls (after the first), updates texture node values
979
+ * in-place instead of rebuilding the entire shader to avoid TSL/WGSL
980
+ * compilation failures from duplicate variable names.
981
+ */
982
+ setupMaterial() {
983
+
984
+ // Ensure camera optimizer exists (build() creates it, but loadSceneData() skips build())
985
+ if ( ! this.cameraOptimizer ) {
986
+
987
+ this._initCameraOptimizer();
988
+
989
+ }
990
+
991
+ if ( ! this.triangleStorageNode ) {
992
+
993
+ console.error( 'PathTracer: Triangle data required' );
994
+ return;
995
+
996
+ }
997
+
998
+ if ( ! this.bvhStorageNode ) {
999
+
1000
+ console.error( 'PathTracer: BVH data required' );
1001
+ return;
1002
+
1003
+ }
1004
+
1005
+ // If compute nodes already exist, update texture nodes in-place
1006
+ // instead of rebuilding the shader (avoids TSL recompilation issues)
1007
+ if ( this.isReady && this.shaderBuilder.getSceneTextureNodes() ) {
1008
+
1009
+ this.shaderBuilder.updateSceneTextures( this );
1010
+ return;
1011
+
1012
+ }
1013
+
1014
+ this._ensureStorageTextures();
1015
+
1016
+ // Build the shared scene texture nodes (prev*, adaptive, scene textures, gobo/IES) the kernels read.
1017
+ this.shaderBuilder.createSceneTextureNodes( this, this.storageTextures );
1018
+
1019
+ this.isReady = true;
1020
+
1021
+ }
1022
+
1023
+ /**
1024
+ * Ensure storage textures exist at correct size
1025
+ */
1026
+ _ensureStorageTextures() {
1027
+
1028
+ const canvas = this.renderer.domElement;
1029
+ const width = Math.max( 1, canvas.width || this.width );
1030
+ const height = Math.max( 1, canvas.height || this.height );
1031
+
1032
+ if ( this.storageTextures.ensureSize( width, height ) ) {
1033
+
1034
+ this.resolution.value.set( width, height );
1035
+
1036
+ }
1037
+
1038
+ }
1039
+
1040
+ /**
1041
+ * Handle canvas resize
1042
+ */
1043
+ _handleResize() {
1044
+
1045
+ const canvas = this.renderer.domElement;
1046
+ const { width, height } = canvas;
1047
+
1048
+ if ( width !== this.storageTextures.renderWidth || height !== this.storageTextures.renderHeight ) {
1049
+
1050
+ this.createStorageTextures( width, height );
1051
+ this.frameCount = 0;
1052
+
1053
+ }
1054
+
1055
+ this.resolution.value.set( width, height );
1056
+
1057
+ }
1058
+
1059
+ /**
1060
+ * Compare two Matrix4 with tolerance to avoid false positives from
1061
+ * floating-point drift (e.g. OrbitControls spherical↔cartesian round-trips).
1062
+ * @param {Matrix4} a
1063
+ * @param {Matrix4} b
1064
+ * @param {number} epsilon
1065
+ * @returns {boolean} True if matrices are approximately equal
1066
+ */
1067
+ _matricesApproxEqual( a, b, epsilon = 1e-10 ) {
1068
+
1069
+ const ae = a.elements;
1070
+ const be = b.elements;
1071
+ for ( let i = 0; i < 16; i ++ ) {
1072
+
1073
+ if ( Math.abs( ae[ i ] - be[ i ] ) > epsilon ) return false;
1074
+
1075
+ }
1076
+
1077
+ return true;
1078
+
1079
+ }
1080
+
1081
+ /**
1082
+ * Update camera uniforms
1083
+ * @returns {boolean} True if camera changed
1084
+ */
1085
+ _updateCameraUniforms() {
1086
+
1087
+ if ( ! this._matricesApproxEqual( this.lastCameraMatrix, this.camera.matrixWorld ) ||
1088
+ ! this._matricesApproxEqual( this.lastProjectionMatrix, this.camera.projectionMatrixInverse ) ) {
1089
+
1090
+ this.cameraWorldMatrix.value.copy( this.camera.matrixWorld );
1091
+ this.cameraViewMatrix.value.copy( this.camera.matrixWorldInverse );
1092
+ this.cameraProjectionMatrix.value.copy( this.camera.projectionMatrix );
1093
+ this.cameraProjectionMatrixInverse.value.copy( this.camera.projectionMatrixInverse );
1094
+
1095
+ this.lastCameraMatrix.copy( this.camera.matrixWorld );
1096
+ this.lastProjectionMatrix.copy( this.camera.projectionMatrixInverse );
1097
+
1098
+ return true;
1099
+
1100
+ }
1101
+
1102
+ return false;
1103
+
1104
+ }
1105
+
1106
+ /**
1107
+ * Update accumulation uniforms
1108
+ * @param {number} frameValue
1109
+ * @param {number} renderMode
1110
+ */
1111
+ _updateAccumulationUniforms( frameValue, renderMode ) {
1112
+
1113
+ const currentInteractionMode = this.cameraOptimizer?.isInInteractionMode() ?? false;
1114
+ this.lastInteractionModeState = currentInteractionMode;
1115
+
1116
+ if ( this.accumulationEnabled ) {
1117
+
1118
+ if ( currentInteractionMode ) {
1119
+
1120
+ this.accumulationAlpha.value = 1.0;
1121
+ this.hasPreviousAccumulated.value = 0;
1122
+
1123
+ } else {
1124
+
1125
+ this.accumulationAlpha.value = calculateAccumulationAlpha( frameValue );
1126
+
1127
+ this.hasPreviousAccumulated.value = frameValue > 0 ? 1 : 0;
1128
+
1129
+ }
1130
+
1131
+ } else {
1132
+
1133
+ this.accumulationAlpha.value = 1.0;
1134
+ this.hasPreviousAccumulated.value = 0;
1135
+
1136
+ }
1137
+
1138
+ }
1139
+
1140
+ /**
1141
+ * Publish textures to pipeline context
1142
+ * @param {PipelineContext} context
1143
+ * @param {Object} writeTex - The just-written StorageTexture set { color, normalDepth, albedo }
1144
+ */
1145
+ _publishTexturesToContext( context, writeTex ) {
1146
+
1147
+ context.setTexture( 'pathtracer:color', writeTex.color );
1148
+ context.setTexture( 'pathtracer:normalDepth', writeTex.normalDepth );
1149
+ context.setTexture( 'pathtracer:albedo', writeTex.albedo );
1150
+
1151
+ context.setState( 'interactionMode', this.cameraOptimizer?.isInInteractionMode() ?? false );
1152
+ context.setState( 'renderMode', this.renderMode.value );
1153
+
1154
+ }
1155
+
1156
+ /**
1157
+ * Emit state change events
1158
+ */
1159
+ _emitStateEvents() {
1160
+
1161
+ this.emit( 'pathtracer:frameComplete', {
1162
+ frame: this.frameCount,
1163
+ isComplete: this.isComplete
1164
+ } );
1165
+
1166
+ if ( this.cameraChanged ) {
1167
+
1168
+ this.emit( 'camera:moved' );
1169
+ this.cameraChanged = false;
1170
+
1171
+ }
1172
+
1173
+ }
1174
+
1175
+ /**
1176
+ * Update completion threshold based on render mode
1177
+ */
1178
+ updateCompletionThreshold() {
1179
+
1180
+ const renderMode = this.renderMode.value;
1181
+ const maxFrames = this.maxSamples.value;
1182
+
1183
+ if ( this.renderLimitMode === 'time' ) {
1184
+
1185
+ this.completionThreshold = Infinity;
1186
+
1187
+ } else {
1188
+
1189
+ this.completionThreshold = updateCompletionThreshold(
1190
+ renderMode,
1191
+ maxFrames
1192
+ );
1193
+
1194
+ }
1195
+
1196
+ }
1197
+
1198
+ setRenderLimitMode( mode ) {
1199
+
1200
+ this.renderLimitMode = mode;
1201
+ this.updateCompletionThreshold();
1202
+
1203
+ }
1204
+
1205
+ // ===== ASVGF DENOISING MANAGEMENT =====
1206
+
1207
+ manageASVGFForRenderMode( renderMode ) {
1208
+
1209
+ if ( renderMode !== this.lastRenderMode ) {
1210
+
1211
+ if ( this.renderModeChangeTimeout ) {
1212
+
1213
+ clearTimeout( this.renderModeChangeTimeout );
1214
+
1215
+ }
1216
+
1217
+ this.pendingRenderMode = renderMode;
1218
+
1219
+ this.renderModeChangeTimeout = setTimeout( () => {
1220
+
1221
+ if ( this.pendingRenderMode !== null && this.pendingRenderMode !== this.lastRenderMode ) {
1222
+
1223
+ this.lastRenderMode = this.pendingRenderMode;
1224
+ this._onRenderModeChanged( this.pendingRenderMode );
1225
+
1226
+ }
1227
+
1228
+ this.renderModeChangeTimeout = null;
1229
+ this.pendingRenderMode = null;
1230
+
1231
+ }, this.renderModeChangeDelay );
1232
+
1233
+ }
1234
+
1235
+ this._handleFullQuadASVGF();
1236
+
1237
+ }
1238
+
1239
+ _onRenderModeChanged( newMode ) {
1240
+
1241
+ if ( newMode === 1 ) {
1242
+
1243
+ this.emit( 'asvgf:updateParameters', {
1244
+ enableDebug: false,
1245
+ temporalAlpha: 0.15
1246
+ } );
1247
+
1248
+ } else {
1249
+
1250
+ this.emit( 'asvgf:updateParameters', {
1251
+ temporalAlpha: 0.1,
1252
+ } );
1253
+
1254
+ }
1255
+
1256
+ this.emit( 'asvgf:reset' );
1257
+
1258
+ }
1259
+
1260
+ _handleFullQuadASVGF() {
1261
+
1262
+ this.emit( 'asvgf:setTemporal', { enabled: true } );
1263
+
1264
+ }
1265
+
1266
+ // ===== UNIFORM & DATA SETTERS =====
1267
+
1268
+ /**
1269
+ * Generic uniform setter. Handles booleans (→ int 0/1),
1270
+ * vectors/matrices (→ .copy()), and plain scalars automatically.
1271
+ * @param {string} name - Uniform name (e.g. 'maxBounces', 'showBackground')
1272
+ * @param {*} value
1273
+ */
1274
+ setUniform( name, value ) {
1275
+
1276
+ this.uniforms.set( name, value );
1277
+
1278
+ }
1279
+
1280
+ setBlueNoiseTexture( tex ) {
1281
+
1282
+ // Legacy API — sets the scalar STBN atlas texture
1283
+ this.stbnScalarTexture = tex;
1284
+ if ( tex ) stbnScalarTextureNode.value = tex;
1285
+
1286
+ }
1287
+
1288
+ /**
1289
+ * Rebuild the packed light buffer from cached lightBVH + emissive data.
1290
+ * Layout: [ lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE vec4s per entry) ].
1291
+ * Also updates `emissiveVec4Offset` uniform (in vec4 elements).
1292
+ * @private
1293
+ */
1294
+ _rebuildLightBuffer() {
1295
+
1296
+ const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
1297
+ const lbvh = this._lbvhDataCache;
1298
+ const emis = this._emissiveDataCache;
1299
+ const lbvhLen = lbvh ? lbvh.length : 0;
1300
+ const emisLen = emis ? emis.length : 0;
1301
+
1302
+ // Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
1303
+ const totalLen = Math.max( lbvhLen + emisLen, 4 );
1304
+ const combined = new Float32Array( totalLen );
1305
+ if ( lbvh ) combined.set( lbvh, 0 );
1306
+ if ( emis ) combined.set( emis, lbvhLen );
1307
+
1308
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
1309
+ this.lightStorageNode.value = this.lightStorageAttr;
1310
+ this.lightStorageNode.bufferCount = combined.length / 4;
1311
+
1312
+ // Offset (in vec4 elements) where emissive data starts.
1313
+ this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
1314
+
1315
+ }
1316
+
1317
+ setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
1318
+
1319
+ if ( ! emissiveData ) return;
1320
+
1321
+ this._emissiveDataCache = emissiveData;
1322
+ this.emissiveTriangleCount.value = count;
1323
+ this.emissiveTotalPower.value = totalPower;
1324
+ this._rebuildLightBuffer();
1325
+ console.log( `PathTracer: ${count} emissive triangles, totalPower=${totalPower.toFixed( 4 )} (storage buffer)` );
1326
+
1327
+ }
1328
+
1329
+ setLightBVHData( nodeData, nodeCount ) {
1330
+
1331
+ if ( ! nodeData ) return;
1332
+
1333
+ this._lbvhDataCache = nodeData;
1334
+ this.lightBVHNodeCount.value = nodeCount;
1335
+ this._rebuildLightBuffer();
1336
+ console.log( `PathTracer: Light BVH ${nodeCount} nodes` );
1337
+
1338
+ }
1339
+
1340
+ // ===== UTILITY METHODS =====
1341
+
1342
+ updateUniforms( updates ) {
1343
+
1344
+ let hasChanges = false;
1345
+
1346
+ for ( const [ key, value ] of Object.entries( updates ) ) {
1347
+
1348
+ if ( this[ key ] && this[ key ].value !== undefined ) {
1349
+
1350
+ if ( this[ key ].value !== value ) {
1351
+
1352
+ this[ key ].value = value;
1353
+ hasChanges = true;
1354
+
1355
+ }
1356
+
1357
+ }
1358
+
1359
+ }
1360
+
1361
+ if ( hasChanges ) {
1362
+
1363
+ this.reset();
1364
+
1365
+ }
1366
+
1367
+ }
1368
+
1369
+ async rebuildMaterials( scene ) {
1370
+
1371
+ if ( ! this.sdfs ) {
1372
+
1373
+ throw new Error( "Scene not built yet. Call build() first." );
1374
+
1375
+ }
1376
+
1377
+ try {
1378
+
1379
+ console.log( 'PathTracer: Starting material rebuild...' );
1380
+
1381
+ await this.sdfs.rebuildMaterials( scene );
1382
+ this.updateSceneUniforms();
1383
+ this.shaderBuilder.updateSceneTextures( this );
1384
+ this.updateLights();
1385
+ this.reset();
1386
+
1387
+ console.log( 'PathTracer materials rebuilt successfully' );
1388
+
1389
+ } catch ( error ) {
1390
+
1391
+ console.error( 'Error rebuilding PathTracer materials:', error );
1392
+
1393
+ try {
1394
+
1395
+ console.warn( 'Attempting recovery by resetting path tracer...' );
1396
+ this.reset();
1397
+
1398
+ } catch ( recoveryError ) {
1399
+
1400
+ console.error( 'Recovery failed:', recoveryError );
1401
+
1402
+ }
1403
+
1404
+ throw error;
1405
+
1406
+ }
1407
+
1408
+ }
1409
+
1410
+ // ===== DISPOSE =====
1411
+
1412
+ /**
1413
+ * Disposes of GPU resources.
1414
+ */
1415
+ dispose() {
1416
+
1417
+ // Clear timeouts
1418
+ if ( this.renderModeChangeTimeout ) {
1419
+
1420
+ clearTimeout( this.renderModeChangeTimeout );
1421
+ this.renderModeChangeTimeout = null;
1422
+
1423
+ }
1424
+
1425
+ // Dispose managers
1426
+ this.cameraOptimizer?.dispose();
1427
+ this.materialData?.dispose();
1428
+ this.environment?.dispose();
1429
+ this.shaderBuilder?.dispose();
1430
+ this.uniforms?.dispose();
1431
+
1432
+ // Dispose storage textures
1433
+ this.storageTextures?.dispose();
1434
+
1435
+ // Dispose textures
1436
+ this.stbnScalarTexture?.dispose();
1437
+ this.stbnVec2Texture?.dispose();
1438
+ this.placeholderTexture?.dispose();
1439
+
1440
+ // Clear data references
1441
+ this.triangleStorageAttr = null;
1442
+ this.triangleStorageNode = null;
1443
+ this.bvhStorageAttr = null;
1444
+ this.bvhStorageNode = null;
1445
+ this.placeholderTexture = null;
1446
+
1447
+ this.isReady = false;
1448
+
1449
+ }
1450
+
1451
+ }