rayzee 6.4.0 → 7.0.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.
- package/README.md +24 -5
- package/dist/rayzee.es.js +4953 -4225
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +157 -236
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +29 -13
- package/src/PathTracerApp.js +119 -26
- package/src/Pipeline/PipelineContext.js +1 -2
- package/src/Pipeline/RenderPipeline.js +1 -1
- package/src/Pipeline/RenderStage.js +1 -1
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/GeometryExtractor.js +22 -1
- package/src/Processor/KernelManager.js +277 -0
- package/src/Processor/PackedRayBuffer.js +265 -0
- package/src/Processor/QueueManager.js +173 -0
- package/src/Processor/SceneProcessor.js +1 -0
- package/src/Processor/ShaderBuilder.js +11 -316
- package/src/Processor/StorageTexturePool.js +29 -15
- package/src/Processor/TextureCreator.js +6 -0
- package/src/Processor/VRAMTracker.js +169 -0
- package/src/Processor/utils.js +11 -110
- package/src/RenderSettings.js +1 -3
- package/src/Stages/ASVGF.js +76 -20
- package/src/Stages/BilateralFilter.js +34 -10
- package/src/Stages/EdgeFilter.js +2 -3
- package/src/Stages/MotionVector.js +16 -9
- package/src/Stages/NormalDepth.js +17 -5
- package/src/Stages/PathTracer.js +671 -1456
- package/src/Stages/PathTracerStage.js +1451 -0
- package/src/Stages/SSRC.js +32 -15
- package/src/Stages/Variance.js +35 -12
- package/src/TSL/BVHTraversal.js +7 -1
- package/src/TSL/Common.js +12 -2
- package/src/TSL/CompactKernel.js +110 -0
- package/src/TSL/DebugKernel.js +98 -0
- package/src/TSL/Environment.js +13 -11
- package/src/TSL/ExtendKernel.js +75 -0
- package/src/TSL/FinalWriteKernel.js +121 -0
- package/src/TSL/GenerateKernel.js +109 -0
- package/src/TSL/LightsSampling.js +2 -2
- package/src/TSL/MaterialTransmission.js +32 -2
- package/src/TSL/PathTracerCore.js +43 -912
- package/src/TSL/ShadeKernel.js +873 -0
- package/src/TSL/Struct.js +5 -0
- package/src/TSL/Subsurface.js +232 -0
- package/src/TSL/patches.js +81 -4
- package/src/index.js +3 -0
- package/src/managers/CameraManager.js +1 -1
- package/src/managers/DenoisingManager.js +40 -75
- package/src/managers/EnvironmentManager.js +30 -39
- package/src/managers/MaterialDataManager.js +60 -1
- package/src/managers/OverlayManager.js +7 -22
- package/src/managers/UniformManager.js +1 -3
- package/src/managers/helpers/TileHelper.js +2 -2
- package/src/Stages/AdaptiveSampling.js +0 -483
- package/src/TSL/PathTracer.js +0 -384
- 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
|
+
}
|