rayzee 6.5.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 +7554 -7014
- 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 +12 -9
- package/src/PathTracerApp.js +118 -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 +6 -0
- 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 -317
- package/src/Processor/StorageTexturePool.js +29 -15
- package/src/Processor/VRAMTracker.js +169 -0
- package/src/Processor/utils.js +11 -110
- package/src/RenderSettings.js +0 -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/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/PathTracerCore.js +43 -1039
- package/src/TSL/ShadeKernel.js +873 -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/OverlayManager.js +7 -22
- package/src/managers/UniformManager.js +0 -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
package/src/Stages/PathTracer.js
CHANGED
|
@@ -1,1105 +1,156 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 { TileManager } from '../managers/TileManager.js';
|
|
14
|
-
import { CameraOptimizer } from '../Processor/CameraOptimizer.js';
|
|
15
|
-
import { createPerformanceMonitor, calculateAccumulationAlpha, updateCompletionThreshold } from '../Processor/utils.js';
|
|
16
|
-
import { StorageTexturePool } from '../Processor/StorageTexturePool.js';
|
|
17
|
-
import { UniformManager } from '../managers/UniformManager.js';
|
|
18
|
-
import { MaterialDataManager } from '../managers/MaterialDataManager.js';
|
|
19
|
-
import { EnvironmentManager } from '../managers/EnvironmentManager.js';
|
|
20
|
-
import { ShaderBuilder } from '../Processor/ShaderBuilder.js';
|
|
21
|
-
|
|
22
|
-
// Scene building
|
|
23
|
-
import { SceneProcessor } from '../Processor/SceneProcessor.js';
|
|
24
|
-
import { LightSerializer } from '../Processor/LightSerializer';
|
|
25
|
-
|
|
26
|
-
// Constants
|
|
27
|
-
import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
|
|
28
|
-
import { getAssetConfig } from '../AssetConfig.js';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Data layout constants
|
|
32
|
-
*/
|
|
33
|
-
const BVH_VEC4_PER_NODE = 4;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Path Tracing Stage for WebGPU.
|
|
37
|
-
*
|
|
38
|
-
* Full-featured path tracing stage:
|
|
39
|
-
* - BVH-accelerated ray traversal
|
|
40
|
-
* - GGX/Diffuse BSDF sampling
|
|
41
|
-
* - Environment lighting with importance sampling
|
|
42
|
-
* - Progressive and tiled accumulation
|
|
43
|
-
* - MRT outputs for denoising (normal/depth, albedo)
|
|
44
|
-
* - Camera interaction mode optimization
|
|
45
|
-
* - Event-driven pipeline communication
|
|
46
|
-
*
|
|
47
|
-
* Events emitted:
|
|
48
|
-
* - pathtracer:frameComplete - When a frame finishes rendering
|
|
49
|
-
* - camera:moved - When camera position/orientation changes
|
|
50
|
-
* - tile:changed - When current tile changes (for OverlayManager TileHelper). @internal — payload carries internal tile coordinates and may change without notice.
|
|
51
|
-
* - asvgf:reset - Request ASVGF to reset temporal data
|
|
52
|
-
* - asvgf:updateParameters - Update ASVGF parameters
|
|
53
|
-
* - asvgf:setTemporal - Enable/disable ASVGF temporal accumulation
|
|
54
|
-
*
|
|
55
|
-
* Textures published to context:
|
|
56
|
-
* - pathtracer:color - Main color output
|
|
57
|
-
* - pathtracer:normalDepth - Normal/depth buffer
|
|
58
|
-
*/
|
|
59
|
-
export class PathTracer extends RenderStage {
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @param {WebGPURenderer} renderer - Three.js WebGPU renderer
|
|
63
|
-
* @param {Scene} scene - Three.js scene
|
|
64
|
-
* @param {PerspectiveCamera} camera - Three.js camera
|
|
65
|
-
* @param {Object} options - Configuration options
|
|
66
|
-
*/
|
|
67
|
-
constructor( renderer, scene, camera, options = {} ) {
|
|
68
|
-
|
|
69
|
-
super( 'PathTracer', {
|
|
70
|
-
...options,
|
|
71
|
-
executionMode: StageExecutionMode.ALWAYS
|
|
72
|
-
} );
|
|
73
|
-
|
|
74
|
-
const width = options.width || 1920;
|
|
75
|
-
const height = options.height || 1080;
|
|
76
|
-
|
|
77
|
-
this.camera = camera;
|
|
78
|
-
this.width = width;
|
|
79
|
-
this.height = height;
|
|
80
|
-
this.renderer = renderer;
|
|
81
|
-
this.scene = scene;
|
|
82
|
-
|
|
83
|
-
// Initialize managers
|
|
84
|
-
this.tileManager = new TileManager( width, height, DEFAULT_STATE.tiles );
|
|
85
|
-
|
|
86
|
-
// Scene building
|
|
87
|
-
this.sdfs = new SceneProcessor();
|
|
88
|
-
this.lightSerializer = new LightSerializer();
|
|
89
|
-
|
|
90
|
-
// State management
|
|
91
|
-
this.accumulationEnabled = true;
|
|
92
|
-
this.isComplete = false;
|
|
93
|
-
this.cameras = [];
|
|
94
|
-
// Performance monitoring
|
|
95
|
-
this.performanceMonitor = createPerformanceMonitor();
|
|
96
|
-
this.completionThreshold = 0;
|
|
97
|
-
this.renderLimitMode = 'frames';
|
|
98
|
-
|
|
99
|
-
// Initialize data textures
|
|
100
|
-
this._initDataTextures();
|
|
101
|
-
|
|
102
|
-
// Initialize storage texture pool (ping-pong compute output)
|
|
103
|
-
this.storageTextures = new StorageTexturePool( 0, 0 );
|
|
104
|
-
|
|
105
|
-
// Initialize uniforms via UniformManager
|
|
106
|
-
this.uniforms = new UniformManager( width, height );
|
|
107
|
-
|
|
108
|
-
// Define getters for every uniform so that this.maxBounces, this.frame, etc.
|
|
109
|
-
// return the uniform node (backward-compat with this.X.value pattern).
|
|
110
|
-
this._defineUniformGetters();
|
|
111
|
-
|
|
112
|
-
// Initialize material data manager
|
|
113
|
-
this.materialData = new MaterialDataManager( this.sdfs );
|
|
114
|
-
this.materialData.callbacks.onReset = () => this.reset();
|
|
115
|
-
// Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The
|
|
116
|
-
// authoritative CPU array is triangleStorageAttr.array (not sdfs.triangleData,
|
|
117
|
-
// which isn't populated on the PathTracerApp build path). The patch mutates
|
|
118
|
-
// the array in place — only a dirty flag is needed for GPU re-upload.
|
|
119
|
-
this.materialData.callbacks.getTriangleData = () => ( {
|
|
120
|
-
array: this.triangleStorageAttr?.array,
|
|
121
|
-
count: this.triangleCount,
|
|
122
|
-
} );
|
|
123
|
-
this.materialData.callbacks.onTriangleDataChanged = () => {
|
|
124
|
-
|
|
125
|
-
if ( this.triangleStorageAttr ) this.triangleStorageAttr.needsUpdate = true;
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Initialize environment manager
|
|
130
|
-
this.environment = new EnvironmentManager( this.scene, this.uniforms );
|
|
131
|
-
this.environment.callbacks.onReset = () => this.reset();
|
|
132
|
-
this.environment.callbacks.getSceneTextureNodes = () => this.shaderBuilder.getSceneTextureNodes();
|
|
133
|
-
|
|
134
|
-
// Initialize shader composer
|
|
135
|
-
this.shaderBuilder = new ShaderBuilder();
|
|
136
|
-
|
|
137
|
-
// Initialize rendering state
|
|
138
|
-
this._initRenderingState();
|
|
139
|
-
|
|
140
|
-
// Setup blue noise
|
|
141
|
-
this.setupBlueNoise();
|
|
142
|
-
|
|
143
|
-
// Cache frequently used objects
|
|
144
|
-
this.tempVector2 = new Vector2();
|
|
145
|
-
this.lastCameraMatrix = new Matrix4();
|
|
146
|
-
this.lastProjectionMatrix = new Matrix4();
|
|
147
|
-
|
|
148
|
-
// Denoising management state
|
|
149
|
-
this.lastRenderMode = - 1;
|
|
150
|
-
this.tileCompletionFrame = 0;
|
|
151
|
-
this.renderModeChangeTimeout = null;
|
|
152
|
-
this.renderModeChangeDelay = 50;
|
|
153
|
-
this.pendingRenderMode = null;
|
|
154
|
-
|
|
155
|
-
// Adaptive sampling state
|
|
156
|
-
this.adaptiveSamplingFrameToggle = false;
|
|
157
|
-
|
|
158
|
-
// Track interaction mode state for accumulation
|
|
159
|
-
this.lastInteractionModeState = false;
|
|
160
|
-
|
|
161
|
-
// Track changes for event emission
|
|
162
|
-
this.cameraChanged = false;
|
|
163
|
-
this.tileChanged = false;
|
|
164
|
-
|
|
165
|
-
// Update completion threshold
|
|
166
|
-
this.updateCompletionThreshold();
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Initialize data texture references and metadata
|
|
172
|
-
*/
|
|
173
|
-
_initDataTextures() {
|
|
174
|
-
|
|
175
|
-
// Triangle data (storage buffer for WebGPU)
|
|
176
|
-
this.triangleStorageAttr = null;
|
|
177
|
-
this.triangleStorageNode = null;
|
|
178
|
-
this.triangleCount = 0;
|
|
179
|
-
|
|
180
|
-
// BVH data (storage buffer for WebGPU)
|
|
181
|
-
this.bvhStorageAttr = null;
|
|
182
|
-
this.bvhStorageNode = null;
|
|
183
|
-
this.bvhNodeCount = 0;
|
|
184
|
-
|
|
185
|
-
// Lights
|
|
186
|
-
this.directionalLightsData = null;
|
|
187
|
-
this.pointLightsData = null;
|
|
188
|
-
this.spotLightsData = null;
|
|
189
|
-
this.areaLightsData = null;
|
|
190
|
-
|
|
191
|
-
// Spot light gobo (projection mask) DataArrayTexture. Owned externally
|
|
192
|
-
// (GoboManager); ShaderBuilder reads via this property at graph build time
|
|
193
|
-
// and refreshes the bound TextureNode in-place when it changes.
|
|
194
|
-
this.goboMaps = null;
|
|
195
|
-
|
|
196
|
-
// Spot light IES photometric profiles DataArrayTexture. Owned externally
|
|
197
|
-
// (IESManager); ShaderBuilder reads via this property at graph build time
|
|
198
|
-
// and refreshes the bound TextureNode in-place when it changes.
|
|
199
|
-
this.iesProfiles = null;
|
|
200
|
-
|
|
201
|
-
// STBN noise textures
|
|
202
|
-
this.stbnScalarTexture = null;
|
|
203
|
-
this.stbnVec2Texture = null;
|
|
204
|
-
|
|
205
|
-
// Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
|
|
206
|
-
// emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
|
|
207
|
-
// Initialized with dummy data so TSL compilation never sees null.
|
|
208
|
-
this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
|
|
209
|
-
this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
|
|
210
|
-
|
|
211
|
-
// Cached CPU-side data — rebuilt into the packed buffer whenever either source changes.
|
|
212
|
-
this._lbvhDataCache = null;
|
|
213
|
-
this._emissiveDataCache = null;
|
|
214
|
-
|
|
215
|
-
// Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
|
|
216
|
-
// (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
|
|
217
|
-
// tlasLeafIndex for each mesh so we can patch visibility in place.
|
|
218
|
-
this._instanceTable = null;
|
|
219
|
-
|
|
220
|
-
// Adaptive sampling
|
|
221
|
-
this.adaptiveSamplingTexture = null;
|
|
222
|
-
|
|
223
|
-
// Spheres
|
|
224
|
-
this.spheres = [];
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Dynamically defines getters for all uniform names so that
|
|
230
|
-
* this.maxBounces, this.frame, etc. return the uniform node.
|
|
231
|
-
* Also defines light buffer node getters.
|
|
232
|
-
* @private
|
|
233
|
-
*/
|
|
234
|
-
_defineUniformGetters() {
|
|
235
|
-
|
|
236
|
-
const uniforms = this.uniforms;
|
|
237
|
-
|
|
238
|
-
for ( const name of uniforms.keys() ) {
|
|
239
|
-
|
|
240
|
-
Object.defineProperty( this, name, {
|
|
241
|
-
get: () => uniforms.get( name ),
|
|
242
|
-
configurable: true,
|
|
243
|
-
} );
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Light buffer node getters
|
|
248
|
-
const lightBuffers = uniforms.getLightBufferNodes();
|
|
249
|
-
for ( const [ suffix, node ] of Object.entries( lightBuffers ) ) {
|
|
250
|
-
|
|
251
|
-
Object.defineProperty( this, `${suffix}LightsBufferNode`, {
|
|
252
|
-
get: () => node,
|
|
253
|
-
configurable: true,
|
|
254
|
-
} );
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Initialize rendering state
|
|
262
|
-
*/
|
|
263
|
-
_initRenderingState() {
|
|
264
|
-
|
|
265
|
-
// State flags
|
|
266
|
-
this.isReady = false;
|
|
267
|
-
this.frameCount = 0;
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Initialize camera movement optimizer
|
|
273
|
-
*/
|
|
274
|
-
_initCameraOptimizer() {
|
|
275
|
-
|
|
276
|
-
// Create adapter interface for TSL uniforms
|
|
277
|
-
const self = this;
|
|
278
|
-
const materialInterface = {
|
|
279
|
-
uniforms: {
|
|
280
|
-
maxBounceCount: {
|
|
281
|
-
get value() {
|
|
282
|
-
|
|
283
|
-
return self.maxBounces.value;
|
|
284
|
-
|
|
285
|
-
},
|
|
286
|
-
set value( v ) {
|
|
287
|
-
|
|
288
|
-
self.maxBounces.value = v;
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
},
|
|
292
|
-
numRaysPerPixel: {
|
|
293
|
-
get value() {
|
|
294
|
-
|
|
295
|
-
return self.samplesPerPixel.value;
|
|
296
|
-
|
|
297
|
-
},
|
|
298
|
-
set value( v ) {
|
|
299
|
-
|
|
300
|
-
self.samplesPerPixel.value = v;
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
},
|
|
304
|
-
useAdaptiveSampling: {
|
|
305
|
-
get value() {
|
|
306
|
-
|
|
307
|
-
return self.useAdaptiveSampling.value;
|
|
308
|
-
|
|
309
|
-
},
|
|
310
|
-
set value( v ) {
|
|
311
|
-
|
|
312
|
-
self.useAdaptiveSampling.value = v;
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
useEnvMapIS: {
|
|
317
|
-
get value() {
|
|
318
|
-
|
|
319
|
-
return self.useEnvMapIS.value;
|
|
320
|
-
|
|
321
|
-
},
|
|
322
|
-
set value( v ) {
|
|
323
|
-
|
|
324
|
-
self.useEnvMapIS.value = v;
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
enableAccumulation: {
|
|
329
|
-
get value() {
|
|
330
|
-
|
|
331
|
-
return self.enableAccumulation.value;
|
|
332
|
-
|
|
333
|
-
},
|
|
334
|
-
set value( v ) {
|
|
335
|
-
|
|
336
|
-
self.enableAccumulation.value = v;
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
enableEmissiveTriangleSampling: {
|
|
341
|
-
get value() {
|
|
342
|
-
|
|
343
|
-
return self.enableEmissiveTriangleSampling.value;
|
|
344
|
-
|
|
345
|
-
},
|
|
346
|
-
set value( v ) {
|
|
347
|
-
|
|
348
|
-
self.enableEmissiveTriangleSampling.value = v;
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
cameraIsMoving: {
|
|
353
|
-
get value() {
|
|
354
|
-
|
|
355
|
-
return self.cameraIsMoving.value;
|
|
356
|
-
|
|
357
|
-
},
|
|
358
|
-
set value( v ) {
|
|
359
|
-
|
|
360
|
-
self.cameraIsMoving.value = v;
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
this.cameraOptimizer = new CameraOptimizer( this.renderer, materialInterface, {
|
|
368
|
-
enabled: DEFAULT_STATE.interactionModeEnabled,
|
|
369
|
-
qualitySettings: {
|
|
370
|
-
maxBounceCount: 1,
|
|
371
|
-
numRaysPerPixel: 1,
|
|
372
|
-
useAdaptiveSampling: false,
|
|
373
|
-
useEnvMapIS: false,
|
|
374
|
-
enableAccumulation: false,
|
|
375
|
-
enableEmissiveTriangleSampling: false,
|
|
376
|
-
},
|
|
377
|
-
onReset: () => {
|
|
378
|
-
|
|
379
|
-
this.reset();
|
|
380
|
-
this.emit( 'pathtracer:viewpointChanged' );
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
} );
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Load STBN (Spatiotemporal Blue Noise) atlas textures.
|
|
389
|
-
* Each atlas is 1024×1024: 8×8 grid of 128×128 tiles, 64 temporal slices.
|
|
390
|
-
*/
|
|
391
|
-
setupBlueNoise() {
|
|
392
|
-
|
|
393
|
-
const loader = new TextureLoader();
|
|
394
|
-
loader.setCrossOrigin( 'anonymous' );
|
|
395
|
-
|
|
396
|
-
const configure = ( tex ) => {
|
|
397
|
-
|
|
398
|
-
tex.minFilter = NearestFilter;
|
|
399
|
-
tex.magFilter = NearestFilter;
|
|
400
|
-
tex.wrapS = RepeatWrapping;
|
|
401
|
-
tex.wrapT = RepeatWrapping;
|
|
402
|
-
tex.generateMipmaps = false;
|
|
403
|
-
return tex;
|
|
404
|
-
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const { stbnScalarAtlas, stbnVec2Atlas } = getAssetConfig();
|
|
408
|
-
|
|
409
|
-
loader.load( stbnScalarAtlas, ( tex ) => {
|
|
410
|
-
|
|
411
|
-
this.stbnScalarTexture = configure( tex );
|
|
412
|
-
stbnScalarTextureNode.value = tex;
|
|
413
|
-
console.log( `PathTracer: STBN scalar atlas loaded ${tex.image.width}x${tex.image.height}` );
|
|
414
|
-
|
|
415
|
-
} );
|
|
416
|
-
|
|
417
|
-
loader.load( stbnVec2Atlas, ( tex ) => {
|
|
418
|
-
|
|
419
|
-
this.stbnVec2Texture = configure( tex );
|
|
420
|
-
stbnVec2TextureNode.value = tex;
|
|
421
|
-
console.log( `PathTracer: STBN vec2 atlas loaded ${tex.image.width}x${tex.image.height}` );
|
|
422
|
-
|
|
423
|
-
} );
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Setup event listeners for pipeline events
|
|
429
|
-
*/
|
|
430
|
-
setupEventListeners() {
|
|
431
|
-
|
|
432
|
-
this.on( 'pipeline:reset', () => {
|
|
433
|
-
|
|
434
|
-
this.reset();
|
|
435
|
-
|
|
436
|
-
} );
|
|
437
|
-
|
|
438
|
-
this.on( 'pipeline:resize', ( data ) => {
|
|
439
|
-
|
|
440
|
-
if ( data && data.width && data.height ) {
|
|
441
|
-
|
|
442
|
-
this.setSize( data.width, data.height );
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
} );
|
|
447
|
-
|
|
448
|
-
this.on( 'pathtracer:setCompletionThreshold', ( data ) => {
|
|
449
|
-
|
|
450
|
-
if ( data && data.threshold !== undefined ) {
|
|
451
|
-
|
|
452
|
-
this.completionThreshold = data.threshold;
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
} );
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// ===== PUBLIC API METHODS =====
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Build scene data (BVH, geometry, materials)
|
|
464
|
-
* @param {Object3D} scene - Three.js scene or object
|
|
465
|
-
*/
|
|
466
|
-
async build( scene ) {
|
|
467
|
-
|
|
468
|
-
this.dispose();
|
|
469
|
-
this.scene = scene;
|
|
470
|
-
|
|
471
|
-
await this.sdfs.buildBVH( scene );
|
|
472
|
-
this.cameras = this.sdfs.cameras;
|
|
473
|
-
|
|
474
|
-
// Inject shader defines based on detected material features
|
|
475
|
-
this.materialData.injectMaterialFeatureDefines();
|
|
476
|
-
|
|
477
|
-
// Update uniforms with scene data
|
|
478
|
-
this.updateSceneUniforms();
|
|
479
|
-
this.updateLights();
|
|
480
|
-
|
|
481
|
-
// Initialize camera optimizer after scene is built
|
|
482
|
-
this._initCameraOptimizer();
|
|
483
|
-
|
|
484
|
-
// Setup material now that we have scene data
|
|
485
|
-
this.setupMaterial();
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Update scene uniforms from SceneProcessor data
|
|
491
|
-
*/
|
|
492
|
-
updateSceneUniforms() {
|
|
493
|
-
|
|
494
|
-
// Set data references
|
|
495
|
-
this.setTriangleData( this.sdfs.triangleData, this.sdfs.triangleCount );
|
|
496
|
-
this.setBVHData( this.sdfs.bvhData );
|
|
497
|
-
this.setInstanceTable( this.sdfs.instanceTable );
|
|
498
|
-
this.materialData.setMaterialData( this.sdfs.materialData );
|
|
499
|
-
|
|
500
|
-
// Material texture arrays
|
|
501
|
-
this.materialData.loadTexturesFromSdfs();
|
|
502
|
-
|
|
503
|
-
// Emissive triangles (storage buffer)
|
|
504
|
-
if ( this.sdfs.emissiveTriangleData ) {
|
|
505
|
-
|
|
506
|
-
this.setEmissiveTriangleData( this.sdfs.emissiveTriangleData, this.sdfs.emissiveTriangleCount || 0 );
|
|
507
|
-
|
|
508
|
-
} else {
|
|
509
|
-
|
|
510
|
-
this.emissiveTriangleCount.value = 0;
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Light BVH
|
|
515
|
-
if ( this.sdfs.lightBVHNodeData ) {
|
|
516
|
-
|
|
517
|
-
this.setLightBVHData( this.sdfs.lightBVHNodeData, this.sdfs.lightBVHNodeCount || 0 );
|
|
518
|
-
|
|
519
|
-
} else {
|
|
520
|
-
|
|
521
|
-
this.lightBVHNodeCount.value = 0;
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Per-mesh visibility — collect meshes from scene ordered by meshIndex
|
|
526
|
-
this._meshRefs = this._collectMeshRefs( this.scene );
|
|
527
|
-
this.setMeshVisibilityData( this._meshRefs );
|
|
528
|
-
|
|
529
|
-
// Spheres
|
|
530
|
-
this.spheres = this.sdfs.spheres || [];
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Update lights from scene
|
|
536
|
-
*/
|
|
537
|
-
updateLights() {
|
|
538
|
-
|
|
539
|
-
// Process scene lights
|
|
540
|
-
const mockMaterial = {
|
|
541
|
-
uniforms: {
|
|
542
|
-
directionalLights: { value: null },
|
|
543
|
-
pointLights: { value: null },
|
|
544
|
-
spotLights: { value: null },
|
|
545
|
-
areaLights: { value: null }
|
|
546
|
-
},
|
|
547
|
-
defines: {}
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
this.lightSerializer.processSceneLights( this.scene, mockMaterial );
|
|
551
|
-
|
|
552
|
-
// Store light data
|
|
553
|
-
this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
|
|
554
|
-
this.pointLightsData = mockMaterial.uniforms.pointLights.value;
|
|
555
|
-
this.spotLightsData = mockMaterial.uniforms.spotLights.value;
|
|
556
|
-
this.areaLightsData = mockMaterial.uniforms.areaLights.value;
|
|
557
|
-
|
|
558
|
-
// Add sun as directional light if procedural sky is active
|
|
559
|
-
if ( this.hasSun.value ) {
|
|
560
|
-
|
|
561
|
-
const scaledSunIntensity = this.environment.envParams.skySunIntensity * 950.0;
|
|
562
|
-
|
|
563
|
-
const sunLight = {
|
|
564
|
-
intensity: scaledSunIntensity,
|
|
565
|
-
color: { r: 1.0, g: 1.0, b: 1.0 },
|
|
566
|
-
userData: {
|
|
567
|
-
angle: this.sunAngularSize.value
|
|
568
|
-
},
|
|
569
|
-
updateMatrixWorld: () => {},
|
|
570
|
-
getWorldPosition: ( target ) => {
|
|
571
|
-
|
|
572
|
-
const sunDir = this.sunDirection.value;
|
|
573
|
-
return target.set( sunDir.x, sunDir.y, sunDir.z ).multiplyScalar( 1e10 );
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
this.lightSerializer.addDirectionalLight( sunLight );
|
|
579
|
-
this.lightSerializer.preprocessLights();
|
|
580
|
-
this.lightSerializer.updateShaderUniforms( mockMaterial );
|
|
581
|
-
|
|
582
|
-
this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
|
|
583
|
-
|
|
584
|
-
console.log( `Sun added as directional light (intensity: ${scaledSunIntensity.toFixed( 2 )})` );
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Update TSL uniform buffer nodes from raw Float32Array data
|
|
589
|
-
this._updateLightBufferNodes();
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Update TSL uniformArray nodes with current light Float32Array data
|
|
595
|
-
*/
|
|
596
|
-
_updateLightBufferNodes() {
|
|
597
|
-
|
|
598
|
-
// Directional lights (12 floats per light — 8 light fields + gobo {index, signed intensity, scale, pad})
|
|
599
|
-
if ( this.directionalLightsData && this.directionalLightsData.length > 0 ) {
|
|
600
|
-
|
|
601
|
-
this.directionalLightsBufferNode.array = Array.from( this.directionalLightsData );
|
|
602
|
-
this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 12 );
|
|
603
|
-
|
|
604
|
-
} else {
|
|
605
|
-
|
|
606
|
-
this.numDirectionalLights.value = 0;
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Area lights (13 floats per light)
|
|
611
|
-
if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
|
|
612
|
-
|
|
613
|
-
this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
|
|
614
|
-
this.numAreaLights.value = Math.floor( this.areaLightsData.length / 13 );
|
|
615
|
-
|
|
616
|
-
} else {
|
|
617
|
-
|
|
618
|
-
this.numAreaLights.value = 0;
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Point lights (9 floats per light)
|
|
623
|
-
if ( this.pointLightsData && this.pointLightsData.length > 0 ) {
|
|
624
|
-
|
|
625
|
-
this.pointLightsBufferNode.array = Array.from( this.pointLightsData );
|
|
626
|
-
this.numPointLights.value = Math.floor( this.pointLightsData.length / 9 );
|
|
627
|
-
|
|
628
|
-
} else {
|
|
629
|
-
|
|
630
|
-
this.numPointLights.value = 0;
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Spot lights (20 floats per light — 14 light fields + gobo {idx, signed intensity} + IES {idx, intensity} + 2 reserved)
|
|
635
|
-
if ( this.spotLightsData && this.spotLightsData.length > 0 ) {
|
|
636
|
-
|
|
637
|
-
this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
|
|
638
|
-
this.numSpotLights.value = Math.floor( this.spotLightsData.length / 20 );
|
|
639
|
-
|
|
640
|
-
} else {
|
|
641
|
-
|
|
642
|
-
this.numSpotLights.value = 0;
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Reset accumulation
|
|
650
|
-
*/
|
|
651
|
-
reset() {
|
|
652
|
-
|
|
653
|
-
this.frameCount = 0;
|
|
654
|
-
this.frame.value = 0;
|
|
655
|
-
this.hasPreviousAccumulated.value = 0;
|
|
656
|
-
this.storageTextures.currentTarget = 0;
|
|
657
|
-
|
|
658
|
-
// Reset tile manager
|
|
659
|
-
this.tileManager.spiralOrder = this.tileManager.generateSpiralOrder( this.tileManager.tiles );
|
|
660
|
-
|
|
661
|
-
// Update completion threshold
|
|
662
|
-
this.updateCompletionThreshold();
|
|
663
|
-
this.isComplete = false;
|
|
664
|
-
this.performanceMonitor?.reset();
|
|
665
|
-
|
|
666
|
-
this.lastRenderMode = - 1;
|
|
667
|
-
this.tileCompletionFrame = 0;
|
|
668
|
-
|
|
669
|
-
this.lastInteractionModeState = false;
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Set tile count for tiled rendering
|
|
675
|
-
* @param {number} newTileCount
|
|
676
|
-
*/
|
|
677
|
-
setTileCount( newTileCount ) {
|
|
678
|
-
|
|
679
|
-
this.tileManager.setTileCount( newTileCount );
|
|
680
|
-
this.updateCompletionThreshold();
|
|
681
|
-
this.reset();
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Set render size
|
|
687
|
-
* @param {number} width
|
|
688
|
-
* @param {number} height
|
|
689
|
-
*/
|
|
690
|
-
setSize( width, height ) {
|
|
691
|
-
|
|
692
|
-
this.width = width;
|
|
693
|
-
this.height = height;
|
|
694
|
-
|
|
695
|
-
this.resolution.value.set( width, height );
|
|
696
|
-
this.tileManager.setSize( width, height );
|
|
697
|
-
this.createStorageTextures( width, height );
|
|
698
|
-
this.shaderBuilder.setSize( width, height );
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Set accumulation enabled state
|
|
704
|
-
* @param {boolean} enabled
|
|
705
|
-
*/
|
|
706
|
-
setAccumulationEnabled( enabled ) {
|
|
707
|
-
|
|
708
|
-
this.accumulationEnabled = enabled;
|
|
709
|
-
this.enableAccumulation.value = enabled ? 1 : 0;
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// ===== MANAGER DELEGATION METHODS =====
|
|
714
|
-
|
|
715
|
-
enterInteractionMode() {
|
|
716
|
-
|
|
717
|
-
this.cameraOptimizer?.enterInteractionMode();
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
setInteractionModeEnabled( enabled ) {
|
|
722
|
-
|
|
723
|
-
this.cameraOptimizer?.setInteractionModeEnabled( enabled );
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// ===== PROPERTY GETTERS =====
|
|
728
|
-
|
|
729
|
-
get tiles() {
|
|
730
|
-
|
|
731
|
-
return this.tileManager.tiles;
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
get interactionMode() {
|
|
736
|
-
|
|
737
|
-
return this.cameraOptimizer?.isInInteractionMode() ?? false;
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// ===== TEXTURE SETTERS =====
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Sets the triangle data from raw Float32Array via storage buffer.
|
|
745
|
-
* On first call, creates the storage buffer and node.
|
|
746
|
-
* On subsequent calls, creates a new attribute with the correct size
|
|
747
|
-
* and updates the storage node's value to preserve shader graph references.
|
|
748
|
-
* @param {Float32Array} triangleData - Raw triangle data
|
|
749
|
-
* @param {number} triangleCount - Number of triangles
|
|
750
|
-
*/
|
|
751
|
-
setTriangleData( triangleData, triangleCount ) {
|
|
752
|
-
|
|
753
|
-
if ( ! triangleData ) return;
|
|
754
|
-
|
|
755
|
-
const vec4Count = triangleData.length / 4;
|
|
756
|
-
|
|
757
|
-
if ( this.triangleStorageNode ) {
|
|
758
|
-
|
|
759
|
-
// Create new attribute with correct size (old one is GC'd, backend WeakMap cleans up GPU buffer)
|
|
760
|
-
this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
|
|
761
|
-
|
|
762
|
-
// Update storage node references (preserves compiled shader graph)
|
|
763
|
-
this.triangleStorageNode.value = this.triangleStorageAttr;
|
|
764
|
-
this.triangleStorageNode.bufferCount = vec4Count;
|
|
765
|
-
|
|
766
|
-
} else {
|
|
767
|
-
|
|
768
|
-
// First time: create storage buffer and node
|
|
769
|
-
this.triangleStorageAttr = new StorageInstancedBufferAttribute( triangleData, 4 );
|
|
770
|
-
this.triangleStorageNode = storage( this.triangleStorageAttr, 'vec4', vec4Count ).toReadOnly();
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
this.triangleCount = triangleCount;
|
|
775
|
-
|
|
776
|
-
console.log( `PathTracer: ${this.triangleCount} triangles (storage buffer)` );
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Sets the BVH data from raw Float32Array via storage buffer.
|
|
782
|
-
* @param {Float32Array} bvhImageData - Raw BVH data from DataTexture.image.data
|
|
783
|
-
*/
|
|
784
|
-
setBVHData( bvhImageData ) {
|
|
785
|
-
|
|
786
|
-
if ( ! bvhImageData ) return;
|
|
787
|
-
|
|
788
|
-
const vec4Count = bvhImageData.length / 4;
|
|
789
|
-
|
|
790
|
-
if ( this.bvhStorageNode ) {
|
|
791
|
-
|
|
792
|
-
this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
|
|
793
|
-
this.bvhStorageNode.value = this.bvhStorageAttr;
|
|
794
|
-
this.bvhStorageNode.bufferCount = vec4Count;
|
|
795
|
-
|
|
796
|
-
} else {
|
|
797
|
-
|
|
798
|
-
this.bvhStorageAttr = new StorageInstancedBufferAttribute( bvhImageData, 4 );
|
|
799
|
-
this.bvhStorageNode = storage( this.bvhStorageAttr, 'vec4', vec4Count ).toReadOnly();
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
|
|
804
|
-
console.log( `PathTracer: ${this.bvhNodeCount} BVH nodes (storage buffer)` );
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Bind the InstanceTable used to locate each mesh's TLAS leaf for in-place
|
|
810
|
-
* visibility patching. Called by SceneProcessor during upload.
|
|
811
|
-
* @param {import('../Processor/InstanceTable.js').InstanceTable} instanceTable
|
|
812
|
-
*/
|
|
813
|
-
setInstanceTable( instanceTable ) {
|
|
814
|
-
|
|
815
|
-
this._instanceTable = instanceTable;
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Initialize packed visibility for each mesh from current world-visibility.
|
|
821
|
-
* Patches the TLAS leaf slots in the combined BVH buffer that was just uploaded.
|
|
822
|
-
* @param {Array} meshes - Array of Three.js mesh objects, ordered by meshIndex
|
|
823
|
-
*/
|
|
824
|
-
setMeshVisibilityData( meshes ) {
|
|
825
|
-
|
|
826
|
-
if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
|
|
827
|
-
|
|
828
|
-
for ( let i = 0; i < meshes.length; i ++ ) {
|
|
829
|
-
|
|
830
|
-
this._patchTLASLeafVisibility( i, this._isWorldVisible( meshes[ i ] ) );
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Update visibility for a single mesh by patching its TLAS leaf slot [2].
|
|
840
|
-
* @param {number} meshIndex
|
|
841
|
-
* @param {boolean} visible
|
|
842
|
-
*/
|
|
843
|
-
updateMeshVisibility( meshIndex, visible ) {
|
|
1
|
+
/**
|
|
2
|
+
* Wavefront path tracer — decomposed kernel dispatch (Extend → [Sort] → Shade → Compact per
|
|
3
|
+
* bounce, bookended by Generate + FinalWrite; DebugKernel for visMode). Extends PathTracerStage
|
|
4
|
+
* for shared engine/scene infrastructure (managers, uniforms, camera, lights, BVH, accumulation).
|
|
5
|
+
*/
|
|
844
6
|
|
|
845
|
-
|
|
846
|
-
|
|
7
|
+
import { uniform, texture, storage } from 'three/tsl';
|
|
8
|
+
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
9
|
+
import { PathTracerStage } from './PathTracerStage.js';
|
|
10
|
+
import { PackedRayBuffer, GBUFFER_STRIDE } from '../Processor/PackedRayBuffer.js';
|
|
11
|
+
import { QueueManager, COUNTER } from '../Processor/QueueManager.js';
|
|
12
|
+
import { VRAMTracker } from '../Processor/VRAMTracker.js';
|
|
13
|
+
import { KernelManager } from '../Processor/KernelManager.js';
|
|
14
|
+
import { buildGenerateKernel, GENERATE_WG_SIZE } from '../TSL/GenerateKernel.js';
|
|
15
|
+
import { buildExtendKernel, EXTEND_WG_SIZE } from '../TSL/ExtendKernel.js';
|
|
16
|
+
import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
|
|
17
|
+
import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
|
|
18
|
+
import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
|
|
19
|
+
import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
|
|
20
|
+
import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
|
|
21
|
+
import {
|
|
22
|
+
Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
|
|
23
|
+
} from 'three/tsl';
|
|
847
24
|
|
|
848
|
-
|
|
25
|
+
export class PathTracer extends PathTracerStage {
|
|
849
26
|
|
|
850
|
-
|
|
851
|
-
* Recompute world-visibility for all meshes and patch TLAS leaves in place.
|
|
852
|
-
* Call this when group visibility changes at runtime.
|
|
853
|
-
*/
|
|
854
|
-
updateAllMeshVisibility() {
|
|
27
|
+
constructor( renderer, scene, camera, options = {} ) {
|
|
855
28
|
|
|
856
|
-
|
|
29
|
+
super( renderer, scene, camera, options );
|
|
30
|
+
this.name = 'PathTracer';
|
|
857
31
|
|
|
858
|
-
|
|
32
|
+
this._packedBuffers = null;
|
|
33
|
+
this._queueManager = null;
|
|
34
|
+
this._kernelManager = null;
|
|
35
|
+
this._gBufferAttr = null; // per-pixel first-hit MRT (ND + albedo); see _buildWavefrontKernels
|
|
36
|
+
this._wavefrontReady = false;
|
|
859
37
|
|
|
860
|
-
|
|
38
|
+
// CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
|
|
39
|
+
this._useDynamicDispatch = true;
|
|
861
40
|
|
|
862
|
-
|
|
41
|
+
// Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
|
|
42
|
+
this._useSubgroupCompact = false;
|
|
863
43
|
|
|
864
|
-
|
|
44
|
+
// Multi-sample pool: S=samplesPerPixel primary rays/pixel/frame (interactive-only, ≤ the pixel cap; else S=1). FinalWrite averages the S slots. Baked into kernels; _ensureSamplesPerPass() rebuilds on change.
|
|
45
|
+
this._multiSampleMaxPixels = ENGINE_DEFAULTS.wavefrontMultiSampleMaxPixels ?? 589824; // 768²
|
|
46
|
+
this._samplesPerPass = 1;
|
|
865
47
|
|
|
866
|
-
|
|
48
|
+
this._lastBounceCounts = null;
|
|
49
|
+
// maxBounces the curve was measured at; the curve is ignored once this no longer matches (-1 = none).
|
|
50
|
+
this._lastBounceCountsBudget = - 1;
|
|
51
|
+
this._readbackPending = false;
|
|
52
|
+
this._readbackEveryNFrames = 4;
|
|
53
|
+
this._readbackFrameCounter = 0;
|
|
54
|
+
// Bumped on resolution change; a readback that resolves with a stale generation is dropped.
|
|
55
|
+
this._readbackGeneration = 0;
|
|
56
|
+
// 0.1% of primary ray count, floored at 100; -1 to disable. Updated per-scene in _buildWavefrontKernels.
|
|
57
|
+
this._bounceEarlyExitThreshold = 100;
|
|
867
58
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
*/
|
|
873
|
-
_patchTLASLeafVisibility( meshIndex, visible ) {
|
|
59
|
+
this._wfRenderWidth = uniform( 1920, 'int' );
|
|
60
|
+
this._wfRenderHeight = uniform( 1080, 'int' );
|
|
61
|
+
this._wfMaxRayCount = uniform( 0, 'uint' );
|
|
62
|
+
this._wfCurrentBounce = uniform( 0, 'int' );
|
|
874
63
|
|
|
875
|
-
|
|
876
|
-
|
|
64
|
+
// VRAM accounting — providers are thunks reading CURRENT live resources,
|
|
65
|
+
// so they survive buffer/texture reallocation (resize, scene/material reload).
|
|
66
|
+
this.vramTracker = new VRAMTracker();
|
|
67
|
+
this._registerVRAMProviders();
|
|
877
68
|
|
|
878
|
-
|
|
879
|
-
this.bvhStorageAttr.array[ entry.tlasLeafIndex * 16 + 2 ] = visible ? 1.0 : 0.0;
|
|
880
|
-
return true;
|
|
69
|
+
console.log( 'PathTracer: initialized (wavefront)' );
|
|
881
70
|
|
|
882
71
|
}
|
|
883
72
|
|
|
884
|
-
|
|
885
|
-
* Collect mesh references from scene, ordered by meshIndex (assigned during extraction).
|
|
886
|
-
* @param {Object3D} scene
|
|
887
|
-
* @returns {Array}
|
|
888
|
-
* @private
|
|
889
|
-
*/
|
|
890
|
-
_collectMeshRefs( scene ) {
|
|
73
|
+
_registerVRAMProviders() {
|
|
891
74
|
|
|
892
|
-
|
|
75
|
+
const t = this.vramTracker;
|
|
893
76
|
|
|
894
|
-
|
|
895
|
-
|
|
77
|
+
// Wavefront ray-state SoA buffers (rw/ro nodes share one GPU buffer per attr)
|
|
78
|
+
t.register( 'rays', () => {
|
|
896
79
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
meshes[ obj.userData.meshIndex ] = obj;
|
|
900
|
-
|
|
901
|
-
}
|
|
80
|
+
const a = this._packedBuffers?._attrs;
|
|
81
|
+
return a ? [ a.ray, a.rng, a.hit ] : null;
|
|
902
82
|
|
|
903
83
|
} );
|
|
904
84
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Walk the parent chain to determine world-space visibility.
|
|
911
|
-
* @param {Object3D} object
|
|
912
|
-
* @returns {boolean}
|
|
913
|
-
* @private
|
|
914
|
-
*/
|
|
915
|
-
_isWorldVisible( object ) {
|
|
916
|
-
|
|
917
|
-
while ( object ) {
|
|
918
|
-
|
|
919
|
-
if ( ! object.visible ) return false;
|
|
920
|
-
object = object.parent;
|
|
921
|
-
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
return true;
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// ===== FAST BUFFER UPDATES (BVH Refit / Animation) =====
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Update an existing GPU storage buffer in-place (no reallocation).
|
|
932
|
-
* @param {StorageInstancedBufferAttribute} attr
|
|
933
|
-
* @param {Float32Array} data
|
|
934
|
-
* @private
|
|
935
|
-
*/
|
|
936
|
-
_updateStorageBuffer( attr, data ) {
|
|
937
|
-
|
|
938
|
-
if ( ! attr ) return;
|
|
939
|
-
attr.array.set( data );
|
|
940
|
-
attr.needsUpdate = true;
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/** Update triangle positions in the existing GPU buffer (full). */
|
|
945
|
-
updateTriangleData( triangleData ) {
|
|
946
|
-
|
|
947
|
-
this._updateStorageBuffer( this.triangleStorageAttr, triangleData );
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/** Update BVH node data in the existing GPU buffer (full). */
|
|
952
|
-
updateBVHData( bvhData ) {
|
|
953
|
-
|
|
954
|
-
this._updateStorageBuffer( this.bvhStorageAttr, bvhData );
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Update only specific ranges of the GPU storage buffers.
|
|
960
|
-
* Uses addUpdateRange for partial GPU upload instead of full buffer copy.
|
|
961
|
-
*
|
|
962
|
-
* @param {Array<{offset: number, count: number}>} triRanges - Dirty triangle ranges (element index + count)
|
|
963
|
-
* @param {Array<{offset: number, count: number}>} bvhRanges - Dirty BVH node ranges (element index + count)
|
|
964
|
-
*/
|
|
965
|
-
updateBufferRanges( triRanges, bvhRanges ) {
|
|
966
|
-
|
|
967
|
-
if ( this.triangleStorageAttr && triRanges.length > 0 ) {
|
|
968
|
-
|
|
969
|
-
this.triangleStorageAttr.clearUpdateRanges();
|
|
970
|
-
|
|
971
|
-
for ( const r of triRanges ) {
|
|
972
|
-
|
|
973
|
-
this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
|
|
85
|
+
// Queue indices + atomic counters
|
|
86
|
+
t.register( 'queues', () => {
|
|
974
87
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if ( this.bvhStorageAttr && bvhRanges.length > 0 ) {
|
|
982
|
-
|
|
983
|
-
this.bvhStorageAttr.clearUpdateRanges();
|
|
984
|
-
|
|
985
|
-
for ( const r of bvhRanges ) {
|
|
986
|
-
|
|
987
|
-
this.bvhStorageAttr.addUpdateRange( r.offset, r.count );
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
this.bvhStorageAttr.version ++;
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// ===== STORAGE TEXTURES =====
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Creates storage textures for compute accumulation.
|
|
1001
|
-
* @param {number} width
|
|
1002
|
-
* @param {number} height
|
|
1003
|
-
*/
|
|
1004
|
-
createStorageTextures( width, height ) {
|
|
1005
|
-
|
|
1006
|
-
if ( this.storageTextures.writeColor ) {
|
|
1007
|
-
|
|
1008
|
-
// Resize existing textures — preserves JS object references
|
|
1009
|
-
// so the compiled compute node's bindings remain valid
|
|
1010
|
-
this.storageTextures.setSize( width, height );
|
|
1011
|
-
|
|
1012
|
-
} else {
|
|
1013
|
-
|
|
1014
|
-
// Initial creation
|
|
1015
|
-
this.storageTextures.create( width, height );
|
|
1016
|
-
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// Update resolution uniform
|
|
1020
|
-
this.resolution.value.set( width, height );
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// ===== MATERIAL SETUP =====
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Creates the path tracing material and quad.
|
|
1028
|
-
* On subsequent calls (after the first), updates texture node values
|
|
1029
|
-
* in-place instead of rebuilding the entire shader to avoid TSL/WGSL
|
|
1030
|
-
* compilation failures from duplicate variable names.
|
|
1031
|
-
*/
|
|
1032
|
-
setupMaterial() {
|
|
1033
|
-
|
|
1034
|
-
// Ensure camera optimizer exists (build() creates it, but loadSceneData() skips build())
|
|
1035
|
-
if ( ! this.cameraOptimizer ) {
|
|
1036
|
-
|
|
1037
|
-
this._initCameraOptimizer();
|
|
88
|
+
const qm = this._queueManager;
|
|
89
|
+
if ( ! qm ) return null;
|
|
90
|
+
return [
|
|
91
|
+
qm._countersAttr, qm._bounceCountsAttr,
|
|
92
|
+
qm._attrA, qm._attrB,
|
|
93
|
+
];
|
|
1038
94
|
|
|
1039
|
-
}
|
|
95
|
+
} );
|
|
1040
96
|
|
|
1041
|
-
|
|
97
|
+
// Per-pixel first-hit G-buffer (normal/depth + albedo)
|
|
98
|
+
t.register( 'gbuffer', () => this._gBufferAttr ? [ this._gBufferAttr ] : null );
|
|
1042
99
|
|
|
1043
|
-
|
|
1044
|
-
|
|
100
|
+
// Accumulation pool: 3 write StorageTextures (2048²) + readable MRT RenderTarget
|
|
101
|
+
t.register( 'accum', () => {
|
|
1045
102
|
|
|
1046
|
-
|
|
103
|
+
const sp = this.storageTextures;
|
|
104
|
+
return sp ? [ sp.writeColor, sp.writeNormalDepth, sp.writeAlbedo, sp.readTarget ] : null;
|
|
1047
105
|
|
|
1048
|
-
|
|
106
|
+
} );
|
|
1049
107
|
|
|
1050
|
-
|
|
1051
|
-
|
|
108
|
+
// Scene geometry (triangle data, two-level BVH, light BVH + emissive)
|
|
109
|
+
t.register( 'geometry', () => [ this.triangleStorageAttr, this.bvhStorageAttr, this.lightStorageAttr ] );
|
|
1052
110
|
|
|
1053
|
-
|
|
111
|
+
// Material storage buffer + per-property texture arrays
|
|
112
|
+
t.register( 'materials', () => {
|
|
1054
113
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
114
|
+
const m = this.materialData;
|
|
115
|
+
if ( ! m ) return null;
|
|
116
|
+
return [
|
|
117
|
+
m.materialStorageAttr,
|
|
118
|
+
m.albedoMaps, m.emissiveMaps, m.normalMaps, m.bumpMaps,
|
|
119
|
+
m.roughnessMaps, m.metalnessMaps, m.displacementMaps,
|
|
120
|
+
];
|
|
1058
121
|
|
|
1059
|
-
|
|
1060
|
-
return;
|
|
122
|
+
} );
|
|
1061
123
|
|
|
1062
|
-
|
|
124
|
+
// Environment map + importance-sampling CDF
|
|
125
|
+
t.register( 'environment', () => {
|
|
1063
126
|
|
|
1064
|
-
|
|
127
|
+
const e = this.environment;
|
|
128
|
+
return e ? [ e.environmentTexture, e.envCDFTexture ] : null;
|
|
1065
129
|
|
|
1066
|
-
this.shaderBuilder.setupCompute( {
|
|
1067
|
-
stage: this,
|
|
1068
|
-
storageTextures: this.storageTextures,
|
|
1069
130
|
} );
|
|
1070
131
|
|
|
1071
|
-
this.isReady = true;
|
|
1072
|
-
|
|
1073
132
|
}
|
|
1074
133
|
|
|
1075
|
-
|
|
1076
|
-
* Ensure storage textures exist at correct size
|
|
1077
|
-
*/
|
|
1078
|
-
_ensureStorageTextures() {
|
|
134
|
+
setupMaterial() {
|
|
1079
135
|
|
|
1080
|
-
|
|
1081
|
-
const width = Math.max( 1, canvas.width || this.width );
|
|
1082
|
-
const height = Math.max( 1, canvas.height || this.height );
|
|
136
|
+
super.setupMaterial();
|
|
1083
137
|
|
|
1084
|
-
|
|
138
|
+
// First setupMaterial call has 0 triangles/materials — skip it.
|
|
139
|
+
if ( this.materialData?.materialCount > 0 ) {
|
|
1085
140
|
|
|
1086
|
-
this.
|
|
141
|
+
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
142
|
+
this._wavefrontReady = false;
|
|
143
|
+
this._buildWavefrontKernels();
|
|
1087
144
|
|
|
1088
145
|
}
|
|
1089
146
|
|
|
1090
147
|
}
|
|
1091
148
|
|
|
1092
|
-
// ===== CORE RENDER METHOD =====
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Renders the path tracing pass with accumulation.
|
|
1096
|
-
* @param {PipelineContext} context - Pipeline context
|
|
1097
|
-
*/
|
|
1098
149
|
render( context ) {
|
|
1099
150
|
|
|
1100
|
-
|
|
151
|
+
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
152
|
+
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
1101
153
|
|
|
1102
|
-
// Early exit conditions
|
|
1103
154
|
if ( this.isComplete || this.frameCount >= this.completionThreshold ) {
|
|
1104
155
|
|
|
1105
156
|
if ( ! this.isComplete ) this.isComplete = true;
|
|
@@ -1109,26 +160,12 @@ export class PathTracer extends RenderStage {
|
|
|
1109
160
|
|
|
1110
161
|
this.performanceMonitor?.start();
|
|
1111
162
|
|
|
1112
|
-
// Read adaptive sampling guidance from pipeline context (produced by AdaptiveSampling)
|
|
1113
|
-
if ( context && this.shaderBuilder.adaptiveSamplingTexNode ) {
|
|
1114
|
-
|
|
1115
|
-
const asTex = context.getTexture( 'adaptiveSampling:output' );
|
|
1116
|
-
if ( asTex ) {
|
|
1117
|
-
|
|
1118
|
-
this.shaderBuilder.adaptiveSamplingTexNode.value = asTex;
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
163
|
const frameValue = this.frameCount;
|
|
1125
164
|
const renderMode = this.renderMode.value;
|
|
1126
165
|
|
|
1127
|
-
// Store original rendering parameters for first frame override in tile mode
|
|
1128
166
|
let originalMaxBounces = null;
|
|
1129
167
|
let originalSamplesPerPixel = null;
|
|
1130
168
|
|
|
1131
|
-
// In tile rendering mode, cap the first frame at 1spp and 1 bounce
|
|
1132
169
|
if ( renderMode === 1 && frameValue === 0 ) {
|
|
1133
170
|
|
|
1134
171
|
originalMaxBounces = this.maxBounces.value;
|
|
@@ -1138,75 +175,20 @@ export class PathTracer extends RenderStage {
|
|
|
1138
175
|
|
|
1139
176
|
}
|
|
1140
177
|
|
|
1141
|
-
// Handle resize
|
|
1142
178
|
this._handleResize();
|
|
179
|
+
this._ensureSamplesPerPass();
|
|
180
|
+
this.manageASVGFForRenderMode( renderMode );
|
|
1143
181
|
|
|
1144
|
-
//
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// Handle tile rendering
|
|
1148
|
-
const tileInfo = this.tileManager.handleTileRendering(
|
|
1149
|
-
this.renderer,
|
|
1150
|
-
renderMode,
|
|
1151
|
-
frameValue,
|
|
1152
|
-
null
|
|
1153
|
-
);
|
|
1154
|
-
|
|
1155
|
-
// Publish tile state to context
|
|
1156
|
-
if ( context ) {
|
|
1157
|
-
|
|
1158
|
-
context.setState( 'tileRenderingComplete', tileInfo.isCompleteCycle );
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Emit tile:changed event
|
|
1163
|
-
if ( tileInfo.tileIndex >= 0 ) {
|
|
1164
|
-
|
|
1165
|
-
const tileBounds = this.tileManager.calculateTileBounds(
|
|
1166
|
-
tileInfo.tileIndex,
|
|
1167
|
-
this.tileManager.tiles,
|
|
1168
|
-
this.width,
|
|
1169
|
-
this.height
|
|
1170
|
-
);
|
|
1171
|
-
|
|
1172
|
-
this.emit( 'tile:changed', {
|
|
1173
|
-
tileIndex: tileInfo.tileIndex,
|
|
1174
|
-
tileBounds: tileBounds,
|
|
1175
|
-
renderMode: renderMode
|
|
1176
|
-
} );
|
|
1177
|
-
|
|
1178
|
-
this.tileChanged = true;
|
|
1179
|
-
|
|
1180
|
-
}
|
|
182
|
+
// Full-frame render is always a complete cycle (PER_CYCLE stages gate on this).
|
|
183
|
+
if ( context ) context.setState( 'tileRenderingComplete', true );
|
|
1181
184
|
|
|
1182
|
-
// Update camera and movement optimization
|
|
1183
185
|
this.cameraChanged = this._updateCameraUniforms();
|
|
1184
186
|
this.cameraOptimizer?.updateInteractionMode( this.cameraChanged );
|
|
1185
|
-
|
|
1186
|
-
// Update accumulation state
|
|
1187
187
|
this._updateAccumulationUniforms( frameValue, renderMode );
|
|
1188
|
-
|
|
1189
|
-
// Update frame uniform
|
|
1190
188
|
this.frame.value = frameValue;
|
|
1191
189
|
|
|
1192
|
-
|
|
1193
|
-
if ( tileInfo.tileIndex >= 0 && tileInfo.tileBounds ) {
|
|
1194
|
-
|
|
1195
|
-
// Dispatch only the workgroups covering this tile
|
|
1196
|
-
this.shaderBuilder.setTileDispatch(
|
|
1197
|
-
tileInfo.tileBounds.x, tileInfo.tileBounds.y,
|
|
1198
|
-
tileInfo.tileBounds.width, tileInfo.tileBounds.height
|
|
1199
|
-
);
|
|
1200
|
-
|
|
1201
|
-
} else {
|
|
1202
|
-
|
|
1203
|
-
// Full-screen render — dispatch all workgroups
|
|
1204
|
-
this.shaderBuilder.setFullScreenDispatch();
|
|
1205
|
-
|
|
1206
|
-
}
|
|
190
|
+
this._setWfDispatch();
|
|
1207
191
|
|
|
1208
|
-
// Update previous-frame texture node values from readTarget
|
|
1209
|
-
// (these sample the last frame's results via texture())
|
|
1210
192
|
const readTextures = this.storageTextures.getReadTextures();
|
|
1211
193
|
if ( this.shaderBuilder.prevColorTexNode ) {
|
|
1212
194
|
|
|
@@ -1216,498 +198,731 @@ export class PathTracer extends RenderStage {
|
|
|
1216
198
|
|
|
1217
199
|
}
|
|
1218
200
|
|
|
1219
|
-
//
|
|
1220
|
-
this.
|
|
1221
|
-
|
|
1222
|
-
// Copy StorageTextures → RenderTarget textures for downstream reads
|
|
1223
|
-
this.storageTextures.copyToReadTargets( this.renderer );
|
|
1224
|
-
|
|
1225
|
-
// Publish readable textures to context for downstream stages
|
|
1226
|
-
const readTex = this.storageTextures.getReadTextures();
|
|
1227
|
-
if ( context ) {
|
|
1228
|
-
|
|
1229
|
-
this._publishTexturesToContext( context, readTex );
|
|
201
|
+
// Wavefront's texture nodes are independent; monolithic's updateSceneTextures doesn't reach them.
|
|
202
|
+
this._refreshWfTextureNodes();
|
|
1230
203
|
|
|
1231
|
-
|
|
204
|
+
const km = this._kernelManager;
|
|
1232
205
|
|
|
1233
|
-
//
|
|
1234
|
-
|
|
206
|
+
// Debug visualization (visMode 1-10): single-pass primary-ray kernel — no bounce loop or
|
|
207
|
+
// accumulation. Mode 11 (NaN/Inf) flows through the normal pipeline below; FinalWrite flags it.
|
|
208
|
+
if ( ( this.visMode?.value | 0 ) > 0 && this.visMode.value !== 11 ) {
|
|
1235
209
|
|
|
1236
|
-
|
|
1237
|
-
// Interaction-mode frames provide visual feedback but should not
|
|
1238
|
-
// consume the sample budget — otherwise the render "completes"
|
|
1239
|
-
// with N frames of 1-SPP noise before the timeout exits.
|
|
1240
|
-
if ( ! ( this.cameraOptimizer?.isInInteractionMode() ) ) {
|
|
210
|
+
km.dispatch( 'debug' );
|
|
1241
211
|
|
|
1242
|
-
this.
|
|
212
|
+
this.storageTextures.copyToReadTargets( this.renderer );
|
|
213
|
+
const dbgReadTex = this.storageTextures.getReadTextures();
|
|
214
|
+
if ( context ) this._publishTexturesToContext( context, dbgReadTex );
|
|
1243
215
|
|
|
1244
|
-
|
|
216
|
+
this._emitStateEvents();
|
|
217
|
+
// Don't count interaction-mode (1-SPP feedback) frames toward completion (megakernel parity Stages/PathTracer.js:1240) — else a continuous orbit "completes" on noise.
|
|
218
|
+
if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
|
|
1245
219
|
|
|
1246
|
-
|
|
1247
|
-
|
|
220
|
+
if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
|
|
221
|
+
if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
|
|
1248
222
|
|
|
1249
|
-
this.
|
|
223
|
+
this.performanceMonitor?.end();
|
|
224
|
+
return;
|
|
1250
225
|
|
|
1251
226
|
}
|
|
1252
227
|
|
|
1253
|
-
|
|
228
|
+
km.dispatch( 'resetCounters' );
|
|
229
|
+
km.dispatch( 'generate' );
|
|
230
|
+
// Generate traces every pixel; seed ENTERING_COUNT from the full identity active list.
|
|
231
|
+
km.dispatch( 'initActiveIndices' );
|
|
1254
232
|
|
|
1255
|
-
|
|
233
|
+
const maxBounces = this.maxBounces.value;
|
|
234
|
+
// Transmissive/SSS steps consume iterations without advancing camera-bounce depth, so the loop must run far enough for deep glass/subsurface walks (mirror PathTracerCore); the survivor curve + early-exit break it early on non-SSS scenes.
|
|
235
|
+
const loopBound = maxBounces + this.transmissiveBounces.value + this.maxSubsurfaceSteps.value;
|
|
236
|
+
const maxRays = this._wfMaxRayCount.value;
|
|
1256
237
|
|
|
1257
|
-
|
|
238
|
+
// The survivor curve survives a maxBounces change (reset() preserves it), and is reusable
|
|
239
|
+
// across one: for loop iterations below BOTH the old and new camera-bounce caps, no ray has
|
|
240
|
+
// been killed by either cap, so the counts are cap-independent. Trust the curve up to that
|
|
241
|
+
// cutoff — the whole curve when same-budget or decreasing (old counts only over-estimate, so
|
|
242
|
+
// sizing over-sizes and early-exit fires later — both safe); only the overlap [0, oldBudget)
|
|
243
|
+
// when increasing (beyond it the old cap already culled rays → under-estimate → would drop
|
|
244
|
+
// rays). Past the cutoff, full dispatch + no early-exit. Avoids the full-work spike (a visible
|
|
245
|
+
// hitch) on every bounce-count change while a fresh curve is read back. budget=-1 (no curve,
|
|
246
|
+
// e.g. cold start / post-resize) → cutoff 0 → full dispatch everywhere, matching prior behavior.
|
|
247
|
+
const curve = this._lastBounceCounts;
|
|
248
|
+
const curveReliableUpto = curve
|
|
249
|
+
? ( maxBounces <= this._lastBounceCountsBudget ? loopBound + 1 : this._lastBounceCountsBudget )
|
|
250
|
+
: 0;
|
|
1258
251
|
|
|
1259
|
-
|
|
252
|
+
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
1260
253
|
|
|
1261
|
-
|
|
254
|
+
this._wfCurrentBounce.value = bounce;
|
|
1262
255
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
_handleResize() {
|
|
256
|
+
// Functional-compaction path (dynamic dispatch): copyback keeps the read buffer dense, kernels sized to live survivors. Dynamic-off uses the full path (ENTERING=maxRays, identity buffer).
|
|
257
|
+
const useFunctionalCompaction = this._useDynamicDispatch;
|
|
258
|
+
if ( useFunctionalCompaction ) {
|
|
1267
259
|
|
|
1268
|
-
|
|
1269
|
-
|
|
260
|
+
// ENTERING_COUNT already set (bounce 0 by initActiveIndices, N>0 by snapshotBounceCount); size from last frame's survivor curve with a 1.5×+1024 margin.
|
|
261
|
+
let entering = maxRays;
|
|
262
|
+
if ( bounce > 0 ) {
|
|
1270
263
|
|
|
1271
|
-
|
|
264
|
+
const idx = bounce - 1;
|
|
265
|
+
let prev;
|
|
266
|
+
if ( idx < curveReliableUpto && curve[ idx ] !== undefined ) {
|
|
1272
267
|
|
|
1273
|
-
|
|
1274
|
-
this.shaderBuilder.setSize( width, height );
|
|
1275
|
-
this.frameCount = 0;
|
|
268
|
+
prev = curve[ idx ]; // trusted exact count
|
|
1276
269
|
|
|
1277
|
-
|
|
270
|
+
} else if ( curveReliableUpto > 0 ) {
|
|
1278
271
|
|
|
1279
|
-
|
|
272
|
+
// Untrusted tail after a maxBounces increase: survivor counts are monotonically
|
|
273
|
+
// non-increasing across bounces (rays only terminate), so the last trusted count is
|
|
274
|
+
// a safe upper bound — far below maxRays, so no full-dispatch spike. Safe even if it
|
|
275
|
+
// reads low: a count <= threshold would have tripped the early-exit before this bounce.
|
|
276
|
+
prev = curve[ curveReliableUpto - 1 ];
|
|
1280
277
|
|
|
1281
|
-
|
|
278
|
+
}
|
|
1282
279
|
|
|
1283
|
-
|
|
1284
|
-
* Compare two Matrix4 with tolerance to avoid false positives from
|
|
1285
|
-
* floating-point drift (e.g. OrbitControls spherical↔cartesian round-trips).
|
|
1286
|
-
* @param {Matrix4} a
|
|
1287
|
-
* @param {Matrix4} b
|
|
1288
|
-
* @param {number} epsilon
|
|
1289
|
-
* @returns {boolean} True if matrices are approximately equal
|
|
1290
|
-
*/
|
|
1291
|
-
_matricesApproxEqual( a, b, epsilon = 1e-10 ) {
|
|
280
|
+
entering = prev > 0 ? prev : maxRays;
|
|
1292
281
|
|
|
1293
|
-
|
|
1294
|
-
const be = b.elements;
|
|
1295
|
-
for ( let i = 0; i < 16; i ++ ) {
|
|
282
|
+
}
|
|
1296
283
|
|
|
1297
|
-
|
|
284
|
+
const sized = Math.min( maxRays, Math.ceil( entering * 1.5 ) + 1024 );
|
|
285
|
+
const wg = [ Math.ceil( sized / 256 ), 1, 1 ];
|
|
286
|
+
km.setDispatchCount( 'extend', wg );
|
|
287
|
+
km.setDispatchCount( 'shade', wg );
|
|
288
|
+
km.setDispatchCount( 'compact', wg );
|
|
289
|
+
km.setDispatchCount( 'compactCopyback', wg );
|
|
1298
290
|
|
|
1299
|
-
|
|
291
|
+
} else {
|
|
1300
292
|
|
|
1301
|
-
|
|
293
|
+
km.dispatch( 'enterFull' );
|
|
294
|
+
const full = [ Math.ceil( maxRays / 256 ), 1, 1 ];
|
|
295
|
+
km.setDispatchCount( 'extend', full );
|
|
296
|
+
km.setDispatchCount( 'shade', full );
|
|
297
|
+
km.setDispatchCount( 'compact', full );
|
|
1302
298
|
|
|
1303
|
-
|
|
299
|
+
}
|
|
1304
300
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
*/
|
|
1309
|
-
_updateCameraUniforms() {
|
|
301
|
+
// Extend/Shade kept separate (not fused): a fused kernel's register pressure drops occupancy more than fusion saves.
|
|
302
|
+
km.dispatch( 'extend' );
|
|
303
|
+
km.dispatch( 'shade' );
|
|
1310
304
|
|
|
1311
|
-
|
|
1312
|
-
|
|
305
|
+
km.dispatch( 'resetActiveCounter' );
|
|
306
|
+
km.dispatch( 'compact' );
|
|
307
|
+
if ( useFunctionalCompaction ) km.dispatch( 'compactCopyback' );
|
|
308
|
+
km.dispatch( 'snapshotBounceCount' );
|
|
309
|
+
// No swap: pingPong stays 0 (kernels are build-time-bound to buffer A).
|
|
1313
310
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
311
|
+
// Early-exit on last frame's per-bounce snapshot (stale via async readback, fine for a heuristic).
|
|
312
|
+
if (
|
|
313
|
+
bounce < curveReliableUpto
|
|
314
|
+
&& bounce < loopBound
|
|
315
|
+
&& curve[ bounce ] !== undefined
|
|
316
|
+
&& curve[ bounce ] <= this._bounceEarlyExitThreshold
|
|
317
|
+
) {
|
|
1318
318
|
|
|
1319
|
-
|
|
1320
|
-
this.lastProjectionMatrix.copy( this.camera.projectionMatrixInverse );
|
|
319
|
+
break;
|
|
1321
320
|
|
|
1322
|
-
|
|
321
|
+
}
|
|
1323
322
|
|
|
1324
323
|
}
|
|
1325
324
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* Update accumulation uniforms
|
|
1332
|
-
* @param {number} frameValue
|
|
1333
|
-
* @param {number} renderMode
|
|
1334
|
-
*/
|
|
1335
|
-
_updateAccumulationUniforms( frameValue, renderMode ) {
|
|
325
|
+
km.dispatch( 'finalWrite' );
|
|
1336
326
|
|
|
1337
|
-
|
|
1338
|
-
this.lastInteractionModeState = currentInteractionMode;
|
|
327
|
+
this._maybeReadbackCounters();
|
|
1339
328
|
|
|
1340
|
-
|
|
329
|
+
this.storageTextures.copyToReadTargets( this.renderer );
|
|
1341
330
|
|
|
1342
|
-
|
|
331
|
+
const readTex = this.storageTextures.getReadTextures();
|
|
332
|
+
if ( context ) this._publishTexturesToContext( context, readTex );
|
|
1343
333
|
|
|
1344
|
-
|
|
1345
|
-
|
|
334
|
+
this._emitStateEvents();
|
|
335
|
+
// Don't count interaction-mode (1-SPP feedback) frames toward completion (megakernel parity Stages/PathTracer.js:1240) — else a continuous orbit "completes" on noise.
|
|
336
|
+
if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
|
|
1346
337
|
|
|
1347
|
-
|
|
338
|
+
if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
|
|
339
|
+
if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
|
|
1348
340
|
|
|
1349
|
-
|
|
1350
|
-
frameValue,
|
|
1351
|
-
renderMode,
|
|
1352
|
-
this.tileManager.totalTilesCache,
|
|
1353
|
-
false
|
|
1354
|
-
);
|
|
341
|
+
this.performanceMonitor?.end();
|
|
1355
342
|
|
|
1356
|
-
|
|
343
|
+
}
|
|
1357
344
|
|
|
1358
|
-
|
|
345
|
+
// Parent resizes storageTextures/shaderBuilder; wavefront also needs its buffers/uniforms/kernels rebuilt.
|
|
346
|
+
_handleResize() {
|
|
1359
347
|
|
|
1360
|
-
|
|
348
|
+
const oldW = this.storageTextures.renderWidth;
|
|
349
|
+
const oldH = this.storageTextures.renderHeight;
|
|
1361
350
|
|
|
1362
|
-
|
|
1363
|
-
this.hasPreviousAccumulated.value = 0;
|
|
351
|
+
super._handleResize();
|
|
1364
352
|
|
|
1365
|
-
|
|
353
|
+
this._rebuildKernelsIfResized( oldW, oldH );
|
|
1366
354
|
|
|
1367
355
|
}
|
|
1368
356
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
* @param {PipelineContext} context
|
|
1372
|
-
* @param {Object} writeTex - The just-written StorageTexture set { color, normalDepth, albedo }
|
|
1373
|
-
*/
|
|
1374
|
-
_publishTexturesToContext( context, writeTex ) {
|
|
357
|
+
// S=samplesPerPixel for interactive within the pixel cap; production/tiled and high-res get S=1.
|
|
358
|
+
_resolveSamplesPerPass( w, h ) {
|
|
1375
359
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
context.setState( 'interactionMode', this.cameraOptimizer?.isInInteractionMode() ?? false );
|
|
1381
|
-
context.setState( 'renderMode', this.renderMode.value );
|
|
1382
|
-
context.setState( 'tiles', this.tileManager.tiles );
|
|
360
|
+
const interactive = this.renderMode.value === 0;
|
|
361
|
+
const within = ( w * h ) <= this._multiSampleMaxPixels;
|
|
362
|
+
return ( interactive && within ) ? Math.max( 1, this.samplesPerPixel.value | 0 ) : 1;
|
|
1383
363
|
|
|
1384
364
|
}
|
|
1385
365
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
*/
|
|
1389
|
-
_emitStateEvents() {
|
|
1390
|
-
|
|
1391
|
-
this.emit( 'pathtracer:frameComplete', {
|
|
1392
|
-
frame: this.frameCount,
|
|
1393
|
-
isComplete: this.isComplete
|
|
1394
|
-
} );
|
|
366
|
+
// S is baked at build but samplesPerPixel/mode can change without a resize; rebuild when the implied S differs.
|
|
367
|
+
_ensureSamplesPerPass() {
|
|
1395
368
|
|
|
1396
|
-
if ( this.
|
|
369
|
+
if ( ! this._wavefrontReady ) return;
|
|
370
|
+
const w = this.storageTextures.renderWidth;
|
|
371
|
+
const h = this.storageTextures.renderHeight;
|
|
372
|
+
if ( this._resolveSamplesPerPass( w, h ) !== this._samplesPerPass ) {
|
|
1397
373
|
|
|
1398
|
-
this.
|
|
1399
|
-
this.
|
|
374
|
+
this._wavefrontReady = false;
|
|
375
|
+
this._buildWavefrontKernels();
|
|
1400
376
|
|
|
1401
377
|
}
|
|
1402
378
|
|
|
1403
379
|
}
|
|
1404
380
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
*/
|
|
1408
|
-
updateCompletionThreshold() {
|
|
1409
|
-
|
|
1410
|
-
const renderMode = this.renderMode.value;
|
|
1411
|
-
const maxFrames = this.maxSamples.value;
|
|
1412
|
-
|
|
1413
|
-
if ( this.renderLimitMode === 'time' ) {
|
|
1414
|
-
|
|
1415
|
-
this.completionThreshold = Infinity;
|
|
381
|
+
// UI-driven resize (Resolution dropdown) — parent bypasses _handleResize(), so hook here too.
|
|
382
|
+
setSize( width, height ) {
|
|
1416
383
|
|
|
1417
|
-
|
|
384
|
+
const oldW = this.storageTextures.renderWidth;
|
|
385
|
+
const oldH = this.storageTextures.renderHeight;
|
|
1418
386
|
|
|
1419
|
-
|
|
1420
|
-
renderMode,
|
|
1421
|
-
maxFrames,
|
|
1422
|
-
this.tileManager.totalTilesCache
|
|
1423
|
-
);
|
|
387
|
+
super.setSize( width, height );
|
|
1424
388
|
|
|
1425
|
-
|
|
389
|
+
this._rebuildKernelsIfResized( oldW, oldH );
|
|
1426
390
|
|
|
1427
391
|
}
|
|
1428
392
|
|
|
1429
|
-
|
|
393
|
+
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
394
|
+
_maybeReadbackCounters() {
|
|
1430
395
|
|
|
1431
|
-
this.
|
|
1432
|
-
this.updateCompletionThreshold();
|
|
1433
|
-
|
|
1434
|
-
}
|
|
396
|
+
if ( this._readbackPending ) return;
|
|
1435
397
|
|
|
1436
|
-
|
|
398
|
+
this._readbackFrameCounter ++;
|
|
399
|
+
if ( this._readbackFrameCounter < this._readbackEveryNFrames ) return;
|
|
400
|
+
this._readbackFrameCounter = 0;
|
|
1437
401
|
|
|
1438
|
-
|
|
402
|
+
const attr = this._queueManager?.getBounceCountsAttribute();
|
|
403
|
+
if ( ! attr ) return;
|
|
1439
404
|
|
|
1440
|
-
|
|
405
|
+
this._readbackPending = true;
|
|
406
|
+
const gen = this._readbackGeneration;
|
|
407
|
+
const budget = this.maxBounces.value;
|
|
408
|
+
this.renderer.getArrayBufferAsync( attr ).then( ( buf ) => {
|
|
1441
409
|
|
|
1442
|
-
|
|
410
|
+
// Drop counts measured at a now-stale resolution (a resize happened mid-flight).
|
|
411
|
+
if ( gen === this._readbackGeneration ) {
|
|
1443
412
|
|
|
1444
|
-
|
|
413
|
+
this._lastBounceCounts = new Uint32Array( buf.slice( 0 ) );
|
|
414
|
+
this._lastBounceCountsBudget = budget;
|
|
1445
415
|
|
|
1446
416
|
}
|
|
1447
417
|
|
|
1448
|
-
this.
|
|
1449
|
-
|
|
1450
|
-
this.renderModeChangeTimeout = setTimeout( () => {
|
|
418
|
+
this._readbackPending = false;
|
|
1451
419
|
|
|
1452
|
-
|
|
420
|
+
} ).catch( ( e ) => {
|
|
1453
421
|
|
|
1454
|
-
|
|
1455
|
-
|
|
422
|
+
console.warn( 'Wavefront bounceCounts readback failed:', e );
|
|
423
|
+
this._readbackPending = false;
|
|
1456
424
|
|
|
1457
|
-
|
|
425
|
+
} );
|
|
1458
426
|
|
|
1459
|
-
|
|
1460
|
-
this.pendingRenderMode = null;
|
|
427
|
+
}
|
|
1461
428
|
|
|
1462
|
-
|
|
429
|
+
// Sync wavefront's texture nodes with current env/material textures; only a changed ref triggers GPU rebind.
|
|
430
|
+
_refreshWfTextureNodes() {
|
|
1463
431
|
|
|
1464
|
-
|
|
432
|
+
const t = this._wfTexNodes;
|
|
433
|
+
if ( ! t ) return;
|
|
1465
434
|
|
|
1466
|
-
|
|
435
|
+
const env = this.environment?.environmentTexture;
|
|
436
|
+
if ( env && t.envTex ) t.envTex.value = env;
|
|
437
|
+
// CDF texture is replaced (new DataTexture) on each HDRI/env build — repoint the node.
|
|
438
|
+
if ( this.environment?.envCDFTexture && t.envCDFTex ) t.envCDFTex.value = this.environment.envCDFTexture;
|
|
1467
439
|
|
|
1468
|
-
|
|
440
|
+
const mat = this.materialData;
|
|
441
|
+
if ( ! mat ) return;
|
|
442
|
+
if ( mat.albedoMaps && t.albedoMaps ) t.albedoMaps.value = mat.albedoMaps;
|
|
443
|
+
if ( mat.normalMaps && t.normalMaps ) t.normalMaps.value = mat.normalMaps;
|
|
444
|
+
if ( mat.bumpMaps && t.bumpMaps ) t.bumpMaps.value = mat.bumpMaps;
|
|
445
|
+
if ( mat.metalnessMaps && t.metalnessMaps ) t.metalnessMaps.value = mat.metalnessMaps;
|
|
446
|
+
if ( mat.roughnessMaps && t.roughnessMaps ) t.roughnessMaps.value = mat.roughnessMaps;
|
|
447
|
+
if ( mat.emissiveMaps && t.emissiveMaps ) t.emissiveMaps.value = mat.emissiveMaps;
|
|
448
|
+
if ( mat.displacementMaps && t.displacementMaps ) t.displacementMaps.value = mat.displacementMaps;
|
|
1469
449
|
|
|
1470
|
-
|
|
450
|
+
}
|
|
1471
451
|
|
|
1472
|
-
|
|
452
|
+
_rebuildKernelsIfResized( oldW, oldH ) {
|
|
1473
453
|
|
|
1474
|
-
|
|
454
|
+
const newW = this.storageTextures.renderWidth;
|
|
455
|
+
const newH = this.storageTextures.renderHeight;
|
|
456
|
+
if ( ( newW === oldW && newH === oldH ) || ! ( this.materialData?.materialCount > 0 ) ) return;
|
|
1475
457
|
|
|
1476
|
-
|
|
458
|
+
// A survivor curve from the old resolution mis-sizes the per-bounce dispatch at the new one
|
|
459
|
+
// (row-major active list → under-coverage of the lower rows → GI band). Force full coverage
|
|
460
|
+
// until the readback re-measures at the new size; bump the generation so any readback already
|
|
461
|
+
// in flight (carrying the old-resolution counts) is discarded when it resolves.
|
|
462
|
+
this._lastBounceCounts = null;
|
|
463
|
+
this._lastBounceCountsBudget = - 1;
|
|
464
|
+
this._readbackFrameCounter = 0;
|
|
465
|
+
this._readbackGeneration ++;
|
|
1477
466
|
|
|
1478
|
-
|
|
467
|
+
// Recompile only when buffers reallocate (capacity grows) or S changes; otherwise resize uniforms in place.
|
|
468
|
+
const newS = this._resolveSamplesPerPass( newW, newH );
|
|
469
|
+
const neededCap = PackedRayBuffer.requiredCapacity( newW * newH * newS );
|
|
470
|
+
const mustRebuild = ! this._packedBuffers
|
|
471
|
+
|| neededCap > this._packedBuffers.capacity
|
|
472
|
+
|| newS !== this._samplesPerPass;
|
|
1479
473
|
|
|
1480
|
-
if (
|
|
474
|
+
if ( mustRebuild ) {
|
|
1481
475
|
|
|
1482
|
-
this.
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
} );
|
|
476
|
+
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
477
|
+
this._wavefrontReady = false;
|
|
478
|
+
this._buildWavefrontKernels();
|
|
1486
479
|
|
|
1487
480
|
} else {
|
|
1488
481
|
|
|
1489
|
-
this.
|
|
1490
|
-
temporalAlpha: 0.1,
|
|
1491
|
-
} );
|
|
482
|
+
this._resizeWavefrontInPlace( newW, newH );
|
|
1492
483
|
|
|
1493
484
|
}
|
|
1494
485
|
|
|
1495
|
-
this.emit( 'asvgf:reset' );
|
|
1496
|
-
|
|
1497
486
|
}
|
|
1498
487
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
const isFirstFrame = frameValue === 0;
|
|
1502
|
-
const currentTileIndex = isFirstFrame ? - 1 : ( ( frameValue - 1 ) % this.tileManager.totalTilesCache );
|
|
1503
|
-
const isLastTileInSample = currentTileIndex === this.tileManager.totalTilesCache - 1;
|
|
1504
|
-
|
|
1505
|
-
if ( isFirstFrame ) {
|
|
1506
|
-
|
|
1507
|
-
this.emit( 'asvgf:setTemporal', { enabled: true } );
|
|
488
|
+
// Same-capacity, same-S resize: update render-size uniforms + early-exit threshold, no recompile.
|
|
489
|
+
_resizeWavefrontInPlace( w, h ) {
|
|
1508
490
|
|
|
1509
|
-
|
|
491
|
+
const maxRays = w * h * this._samplesPerPass;
|
|
492
|
+
this._wfRenderWidth.value = w;
|
|
493
|
+
this._wfRenderHeight.value = h;
|
|
494
|
+
this._wfMaxRayCount.value = maxRays;
|
|
495
|
+
// initActiveIndices is dispatched bare (not re-sized per frame); its grid must cover the grown range or the identity buffer is left unseeded over [oldMaxRays, maxRays).
|
|
496
|
+
this._kernelManager?.setDispatchCount( 'initActiveIndices', [ Math.ceil( maxRays / 256 ), 1, 1 ] );
|
|
497
|
+
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
1510
498
|
|
|
1511
|
-
this.
|
|
1512
|
-
this.tileCompletionFrame = frameValue;
|
|
1513
|
-
|
|
1514
|
-
} else {
|
|
1515
|
-
|
|
1516
|
-
this.emit( 'asvgf:setTemporal', { enabled: false } );
|
|
499
|
+
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( maxRays / 1000 ) );
|
|
1517
500
|
|
|
1518
501
|
}
|
|
1519
502
|
|
|
1520
503
|
}
|
|
1521
504
|
|
|
1522
|
-
|
|
505
|
+
_buildWavefrontKernels() {
|
|
1523
506
|
|
|
1524
|
-
this.
|
|
507
|
+
const texNodes = this.shaderBuilder.getSceneTextureNodes();
|
|
508
|
+
if ( ! texNodes ) return;
|
|
1525
509
|
|
|
1526
|
-
|
|
510
|
+
const w = this.storageTextures.renderWidth;
|
|
511
|
+
const h = this.storageTextures.renderHeight;
|
|
512
|
+
// maxRays = pool capacity (pixels × S); all downstream sizing scales off it, so S propagates for free.
|
|
513
|
+
this._samplesPerPass = this._resolveSamplesPerPass( w, h );
|
|
514
|
+
const S = this._samplesPerPass | 0;
|
|
515
|
+
const maxRaysPerSample = w * h;
|
|
516
|
+
const maxRays = maxRaysPerSample * S;
|
|
1527
517
|
|
|
1528
|
-
|
|
518
|
+
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
1529
519
|
|
|
1530
|
-
|
|
1531
|
-
* Generic uniform setter. Handles booleans (→ int 0/1),
|
|
1532
|
-
* vectors/matrices (→ .copy()), and plain scalars automatically.
|
|
1533
|
-
* @param {string} name - Uniform name (e.g. 'maxBounces', 'showBackground')
|
|
1534
|
-
* @param {*} value
|
|
1535
|
-
*/
|
|
1536
|
-
setUniform( name, value ) {
|
|
520
|
+
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( maxRays / 1000 ) );
|
|
1537
521
|
|
|
1538
|
-
|
|
522
|
+
}
|
|
1539
523
|
|
|
1540
|
-
|
|
524
|
+
if ( ! this._packedBuffers ) {
|
|
1541
525
|
|
|
1542
|
-
|
|
526
|
+
this._packedBuffers = new PackedRayBuffer( maxRays );
|
|
1543
527
|
|
|
1544
|
-
|
|
1545
|
-
this.stbnScalarTexture = tex;
|
|
1546
|
-
if ( tex ) stbnScalarTextureNode.value = tex;
|
|
528
|
+
} else {
|
|
1547
529
|
|
|
1548
|
-
|
|
530
|
+
this._packedBuffers.resize( maxRays );
|
|
1549
531
|
|
|
1550
|
-
|
|
1551
|
-
* Rebuild the packed light buffer from cached lightBVH + emissive data.
|
|
1552
|
-
* Layout: [ lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE vec4s per entry) ].
|
|
1553
|
-
* Also updates `emissiveVec4Offset` uniform (in vec4 elements).
|
|
1554
|
-
* @private
|
|
1555
|
-
*/
|
|
1556
|
-
_rebuildLightBuffer() {
|
|
1557
|
-
|
|
1558
|
-
const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
|
|
1559
|
-
const lbvh = this._lbvhDataCache;
|
|
1560
|
-
const emis = this._emissiveDataCache;
|
|
1561
|
-
const lbvhLen = lbvh ? lbvh.length : 0;
|
|
1562
|
-
const emisLen = emis ? emis.length : 0;
|
|
1563
|
-
|
|
1564
|
-
// Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
|
|
1565
|
-
const totalLen = Math.max( lbvhLen + emisLen, 4 );
|
|
1566
|
-
const combined = new Float32Array( totalLen );
|
|
1567
|
-
if ( lbvh ) combined.set( lbvh, 0 );
|
|
1568
|
-
if ( emis ) combined.set( emis, lbvhLen );
|
|
1569
|
-
|
|
1570
|
-
this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
|
|
1571
|
-
this.lightStorageNode.value = this.lightStorageAttr;
|
|
1572
|
-
this.lightStorageNode.bufferCount = combined.length / 4;
|
|
1573
|
-
|
|
1574
|
-
// Offset (in vec4 elements) where emissive data starts.
|
|
1575
|
-
this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
|
|
532
|
+
}
|
|
1576
533
|
|
|
1577
|
-
|
|
534
|
+
// Per-pixel G-buffer (first-hit MRT: ND + albedo), 1 uvec4/pixel — half-precision packed (pack2x16).
|
|
535
|
+
// uint (not f32) buffer: packed lanes can hit the NaN exponent range (e.g. snorm 1.0 → 0x7FFF), which a
|
|
536
|
+
// GPU may canonicalize through f32 storage; u32 stores the bits verbatim. Separate from RAY — it's
|
|
537
|
+
// per-pixel (not per-ray×S), written by Generate/Shade bounce-0 and read only by FinalWrite.
|
|
538
|
+
// 1.25× margin (same as the per-ray buffers) so it survives the in-place-resize range.
|
|
539
|
+
const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRaysPerSample ) * GBUFFER_STRIDE;
|
|
540
|
+
this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
|
|
541
|
+
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
542
|
+
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
1578
543
|
|
|
1579
|
-
|
|
544
|
+
if ( ! this._queueManager ) {
|
|
1580
545
|
|
|
1581
|
-
|
|
546
|
+
this._queueManager = new QueueManager( this._packedBuffers.capacity );
|
|
1582
547
|
|
|
1583
|
-
|
|
1584
|
-
this.emissiveTriangleCount.value = count;
|
|
1585
|
-
this.emissiveTotalPower.value = totalPower;
|
|
1586
|
-
this._rebuildLightBuffer();
|
|
1587
|
-
console.log( `PathTracer: ${count} emissive triangles, totalPower=${totalPower.toFixed( 4 )} (storage buffer)` );
|
|
548
|
+
} else {
|
|
1588
549
|
|
|
1589
|
-
|
|
550
|
+
this._queueManager.resize( this._packedBuffers.capacity );
|
|
1590
551
|
|
|
1591
|
-
|
|
552
|
+
}
|
|
1592
553
|
|
|
1593
|
-
if ( !
|
|
554
|
+
if ( ! this._kernelManager ) {
|
|
1594
555
|
|
|
1595
|
-
|
|
1596
|
-
this.lightBVHNodeCount.value = nodeCount;
|
|
1597
|
-
this._rebuildLightBuffer();
|
|
1598
|
-
console.log( `PathTracer: Light BVH ${nodeCount} nodes` );
|
|
556
|
+
this._kernelManager = new KernelManager( this.renderer );
|
|
1599
557
|
|
|
1600
|
-
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const pb = this._packedBuffers;
|
|
561
|
+
const qm = this._queueManager;
|
|
1601
562
|
|
|
1602
|
-
|
|
563
|
+
this._wfRenderWidth.value = w;
|
|
564
|
+
this._wfRenderHeight.value = h;
|
|
565
|
+
this._wfMaxRayCount.value = maxRays;
|
|
1603
566
|
|
|
1604
|
-
|
|
567
|
+
const prevColor = this.shaderBuilder.prevColorTexNode;
|
|
568
|
+
const prevND = this.shaderBuilder.prevNormalDepthTexNode;
|
|
569
|
+
const prevAlbedo = this.shaderBuilder.prevAlbedoTexNode;
|
|
570
|
+
const writeTex = this.storageTextures.getWriteTextures();
|
|
1605
571
|
|
|
1606
|
-
|
|
572
|
+
const counters = qm.getCounters();
|
|
573
|
+
const resetFn = Fn( () => {
|
|
1607
574
|
|
|
1608
|
-
|
|
575
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
1609
576
|
|
|
1610
|
-
|
|
577
|
+
} );
|
|
578
|
+
this._kernelManager.register( 'resetCounters',
|
|
579
|
+
resetFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
580
|
+
);
|
|
1611
581
|
|
|
1612
|
-
|
|
582
|
+
const resetActiveFn = Fn( () => {
|
|
1613
583
|
|
|
1614
|
-
|
|
1615
|
-
hasChanges = true;
|
|
584
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
1616
585
|
|
|
1617
|
-
|
|
586
|
+
} );
|
|
587
|
+
this._kernelManager.register( 'resetActiveCounter',
|
|
588
|
+
resetActiveFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
589
|
+
);
|
|
1618
590
|
|
|
1619
|
-
|
|
591
|
+
// Copy ACTIVE_RAY_COUNT into bounceCounts[currentBounce] for the readback survivor curve.
|
|
592
|
+
const bounceCountsBuf = qm.getBounceCounts();
|
|
593
|
+
const wfCurrentBounce = this._wfCurrentBounce;
|
|
594
|
+
const snapshotFn = Fn( () => {
|
|
1620
595
|
|
|
1621
|
-
|
|
596
|
+
const cnt = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
597
|
+
const slot = uint( wfCurrentBounce ).clamp( uint( 0 ), uint( qm.MAX_BOUNCE_SNAPSHOTS - 1 ) );
|
|
598
|
+
bounceCountsBuf.element( slot ).assign( cnt );
|
|
599
|
+
// Also set ENTERING_COUNT for the next bounce; the full-dispatch path's enterFull overrides it.
|
|
600
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), cnt );
|
|
601
|
+
|
|
602
|
+
} );
|
|
603
|
+
this._kernelManager.register( 'snapshotBounceCount',
|
|
604
|
+
snapshotFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
605
|
+
);
|
|
1622
606
|
|
|
1623
|
-
|
|
607
|
+
const activeWriteA = qm.activeIndices.a;
|
|
608
|
+
const initFn = Fn( () => {
|
|
1624
609
|
|
|
1625
|
-
|
|
610
|
+
const tid = instanceIndex;
|
|
611
|
+
activeWriteA.element( tid ).assign( tid );
|
|
612
|
+
// Seed ACTIVE_RAY_COUNT + ENTERING_COUNT from the _wfMaxRayCount uniform (not a literal) so in-place resize works.
|
|
613
|
+
If( tid.equal( uint( 0 ) ), () => {
|
|
1626
614
|
|
|
1627
|
-
|
|
615
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), this._wfMaxRayCount );
|
|
616
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), this._wfMaxRayCount );
|
|
1628
617
|
|
|
1629
|
-
|
|
618
|
+
} );
|
|
1630
619
|
|
|
1631
|
-
|
|
620
|
+
} );
|
|
621
|
+
this._kernelManager.register( 'initActiveIndices',
|
|
622
|
+
initFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
623
|
+
);
|
|
1632
624
|
|
|
1633
|
-
|
|
625
|
+
const genFn = buildGenerateKernel( {
|
|
626
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
627
|
+
rngBufferRW: pb.rngBuffer.rw,
|
|
628
|
+
gBufferRW,
|
|
629
|
+
resolution: this.resolution,
|
|
630
|
+
frame: this.frame,
|
|
631
|
+
cameraWorldMatrix: this.cameraWorldMatrix,
|
|
632
|
+
cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
|
|
633
|
+
enableDOF: this.enableDOF,
|
|
634
|
+
focalLength: this.focalLength,
|
|
635
|
+
aperture: this.aperture,
|
|
636
|
+
focusDistance: this.focusDistance,
|
|
637
|
+
sceneScale: this.sceneScale,
|
|
638
|
+
apertureScale: this.apertureScale,
|
|
639
|
+
anamorphicRatio: this.anamorphicRatio,
|
|
640
|
+
renderWidth: this._wfRenderWidth,
|
|
641
|
+
renderHeight: this._wfRenderHeight,
|
|
642
|
+
samplesPerPass: S,
|
|
643
|
+
transmissiveBounces: this.transmissiveBounces,
|
|
644
|
+
transparentBackground: this.transparentBackground,
|
|
645
|
+
} );
|
|
646
|
+
this._kernelManager.register( 'generate',
|
|
647
|
+
genFn().compute(
|
|
648
|
+
// Multi-sample: dispatch covers h·S rows (each sub-sample is a row band).
|
|
649
|
+
[ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1 ],
|
|
650
|
+
[ GENERATE_WG_SIZE, GENERATE_WG_SIZE, 1 ]
|
|
651
|
+
)
|
|
652
|
+
);
|
|
1634
653
|
|
|
1635
|
-
|
|
654
|
+
const freshBvh = this.bvhStorageNode;
|
|
655
|
+
const freshTri = this.triangleStorageNode;
|
|
656
|
+
const freshMat = this.materialData.materialStorageNode;
|
|
657
|
+
const freshEnvCDF = texture( this.environment.envCDFTexture ); // independent CDF texture node; refreshed in _refreshWfTextureNodes
|
|
658
|
+
const freshLight = this.lightStorageNode;
|
|
659
|
+
// Independent texture nodes (never compiled elsewhere) avoid Three.js TextureNode caching across pipelines; refreshed via _refreshWfTextureNodes.
|
|
660
|
+
const _mat = this.materialData;
|
|
661
|
+
const _env = this.environment;
|
|
662
|
+
const _placeholder = texNodes.albedoMapsTex;
|
|
663
|
+
const freshAlbedoMaps = _mat.albedoMaps ? texture( _mat.albedoMaps ) : _placeholder;
|
|
664
|
+
const freshNormalMaps = _mat.normalMaps ? texture( _mat.normalMaps ) : texNodes.normalMapsTex;
|
|
665
|
+
const freshBumpMaps = _mat.bumpMaps ? texture( _mat.bumpMaps ) : texNodes.bumpMapsTex;
|
|
666
|
+
const freshMetalnessMaps = _mat.metalnessMaps ? texture( _mat.metalnessMaps ) : texNodes.metalnessMapsTex;
|
|
667
|
+
const freshRoughnessMaps = _mat.roughnessMaps ? texture( _mat.roughnessMaps ) : texNodes.roughnessMapsTex;
|
|
668
|
+
const freshEmissiveMaps = _mat.emissiveMaps ? texture( _mat.emissiveMaps ) : texNodes.emissiveMapsTex;
|
|
669
|
+
const freshDisplacementMaps = _mat.displacementMaps ? texture( _mat.displacementMaps ) : texNodes.displacementMapsTex;
|
|
670
|
+
const freshEnvTex = _env.environmentTexture ? texture( _env.environmentTexture ) : texNodes.envTex;
|
|
671
|
+
|
|
672
|
+
this._wfTexNodes = {
|
|
673
|
+
envTex: freshEnvTex,
|
|
674
|
+
envCDFTex: freshEnvCDF,
|
|
675
|
+
albedoMaps: freshAlbedoMaps,
|
|
676
|
+
normalMaps: freshNormalMaps,
|
|
677
|
+
bumpMaps: freshBumpMaps,
|
|
678
|
+
metalnessMaps: freshMetalnessMaps,
|
|
679
|
+
roughnessMaps: freshRoughnessMaps,
|
|
680
|
+
emissiveMaps: freshEmissiveMaps,
|
|
681
|
+
displacementMaps: freshDisplacementMaps,
|
|
682
|
+
};
|
|
1636
683
|
|
|
1637
|
-
|
|
684
|
+
const extFn = buildExtendKernel( {
|
|
685
|
+
bvhBuffer: freshBvh,
|
|
686
|
+
triangleBuffer: freshTri,
|
|
687
|
+
materialBuffer: freshMat,
|
|
688
|
+
rayBufferRO: pb.rayBuffer.ro,
|
|
689
|
+
hitBufferRW: pb.hitBuffer.rw,
|
|
690
|
+
activeIndicesRO: qm.getActiveReadRO(),
|
|
691
|
+
counters,
|
|
692
|
+
maxRayCount: this._wfMaxRayCount,
|
|
693
|
+
} );
|
|
694
|
+
this._kernelManager.register( 'extend',
|
|
695
|
+
extFn().compute(
|
|
696
|
+
[ Math.ceil( maxRays / EXTEND_WG_SIZE ), 1, 1 ],
|
|
697
|
+
[ EXTEND_WG_SIZE, 1, 1 ]
|
|
698
|
+
)
|
|
699
|
+
);
|
|
1638
700
|
|
|
1639
|
-
|
|
701
|
+
const shadeFn = buildShadeKernel( {
|
|
702
|
+
gBufferRW,
|
|
703
|
+
envCompensationDelta: this.envCompensationDelta,
|
|
704
|
+
bvhBuffer: freshBvh,
|
|
705
|
+
triangleBuffer: freshTri,
|
|
706
|
+
materialBuffer: freshMat,
|
|
707
|
+
envCDFTexture: freshEnvCDF,
|
|
708
|
+
lightBuffer: freshLight,
|
|
709
|
+
rayBufferRW: pb.rayBuffer.rw,
|
|
710
|
+
rngBufferRW: pb.rngBuffer.rw,
|
|
711
|
+
hitBufferRO: pb.hitBuffer.ro,
|
|
712
|
+
counters,
|
|
713
|
+
activeIndicesRO: qm.getActiveReadRO(),
|
|
714
|
+
albedoMaps: freshAlbedoMaps,
|
|
715
|
+
normalMaps: freshNormalMaps,
|
|
716
|
+
bumpMaps: freshBumpMaps,
|
|
717
|
+
metalnessMaps: freshMetalnessMaps,
|
|
718
|
+
roughnessMaps: freshRoughnessMaps,
|
|
719
|
+
emissiveMaps: freshEmissiveMaps,
|
|
720
|
+
displacementMaps: freshDisplacementMaps,
|
|
721
|
+
envTexture: freshEnvTex,
|
|
722
|
+
environmentIntensity: this.environmentIntensity,
|
|
723
|
+
envMatrix: this.environmentMatrix,
|
|
724
|
+
enableEnvironmentLight: this.enableEnvironment,
|
|
725
|
+
useEnvMapIS: this.useEnvMapIS,
|
|
726
|
+
groundProjectionEnabled: this.groundProjectionEnabled,
|
|
727
|
+
groundProjectionRadius: this.groundProjectionRadius,
|
|
728
|
+
groundProjectionHeight: this.groundProjectionHeight,
|
|
729
|
+
envTotalSum: this.envTotalSum,
|
|
730
|
+
envResolution: this.envResolution,
|
|
731
|
+
directionalLightsBuffer: this.directionalLightsBufferNode,
|
|
732
|
+
numDirectionalLights: this.numDirectionalLights,
|
|
733
|
+
areaLightsBuffer: this.areaLightsBufferNode,
|
|
734
|
+
numAreaLights: this.numAreaLights,
|
|
735
|
+
pointLightsBuffer: this.pointLightsBufferNode,
|
|
736
|
+
numPointLights: this.numPointLights,
|
|
737
|
+
spotLightsBuffer: this.spotLightsBufferNode,
|
|
738
|
+
numSpotLights: this.numSpotLights,
|
|
739
|
+
maxBounceCount: this.maxBounces,
|
|
740
|
+
maxSubsurfaceSteps: this.maxSubsurfaceSteps,
|
|
741
|
+
transparentBackground: this.transparentBackground,
|
|
742
|
+
backgroundIntensity: this.backgroundIntensity,
|
|
743
|
+
showBackground: this.showBackground,
|
|
744
|
+
globalIlluminationIntensity: this.globalIlluminationIntensity,
|
|
745
|
+
cameraProjectionMatrix: this.cameraProjectionMatrix,
|
|
746
|
+
cameraViewMatrix: this.cameraViewMatrix,
|
|
747
|
+
fireflyThreshold: this.fireflyThreshold,
|
|
748
|
+
frame: this.frame,
|
|
749
|
+
resolution: this.resolution,
|
|
750
|
+
emissiveTriangleCount: this.emissiveTriangleCount,
|
|
751
|
+
emissiveVec4Offset: this.emissiveVec4Offset,
|
|
752
|
+
emissiveTotalPower: this.emissiveTotalPower,
|
|
753
|
+
emissiveBoost: this.emissiveBoost,
|
|
754
|
+
totalTriangleCount: this.totalTriangleCount,
|
|
755
|
+
enableEmissiveTriangleSampling: this.enableEmissiveTriangleSampling,
|
|
756
|
+
lightBVHNodeCount: this.lightBVHNodeCount,
|
|
757
|
+
currentBounce: this._wfCurrentBounce,
|
|
758
|
+
maxRayCount: this._wfMaxRayCount,
|
|
759
|
+
} );
|
|
760
|
+
this._kernelManager.register( 'shade',
|
|
761
|
+
shadeFn().compute(
|
|
762
|
+
[ Math.ceil( maxRays / SHADE_WG_SIZE ), 1, 1 ],
|
|
763
|
+
[ SHADE_WG_SIZE, 1, 1 ]
|
|
764
|
+
)
|
|
765
|
+
);
|
|
1640
766
|
|
|
1641
|
-
|
|
767
|
+
// Subgroup prefix-sum variant when supported.
|
|
768
|
+
const subgroupsOK = this._useSubgroupCompact
|
|
769
|
+
&& ( this.renderer.hasFeature ? this.renderer.hasFeature( 'subgroups' ) : false );
|
|
770
|
+
this._compactIsSubgroup = subgroupsOK;
|
|
771
|
+
const compactBuilder = subgroupsOK ? buildCompactSubgroupKernel : buildCompactKernel;
|
|
772
|
+
const compactFn = compactBuilder( {
|
|
773
|
+
rayBufferRO: pb.rayBuffer.ro,
|
|
774
|
+
activeIndicesReadRO: qm.getActiveReadRO(),
|
|
775
|
+
activeIndicesWriteRW: qm.getActiveWrite(),
|
|
776
|
+
counters,
|
|
777
|
+
currentActiveCount: this._wfMaxRayCount,
|
|
778
|
+
} );
|
|
779
|
+
this._kernelManager.register( 'compact',
|
|
780
|
+
compactFn().compute(
|
|
781
|
+
[ Math.ceil( maxRays / COMPACT_WG_SIZE ), 1, 1 ],
|
|
782
|
+
[ COMPACT_WG_SIZE, 1, 1 ]
|
|
783
|
+
)
|
|
784
|
+
);
|
|
1642
785
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
this.updateLights();
|
|
1647
|
-
this.reset();
|
|
786
|
+
// Storage nodes bind buffer A at build time, so compactCopyback copies the dense survivor list B→A for the next bounce.
|
|
787
|
+
// Full-dispatch path: ENTERING_COUNT = maxRays, kernels read the identity buffer over [0,maxRays).
|
|
788
|
+
const enterFullFn = Fn( () => {
|
|
1648
789
|
|
|
1649
|
-
|
|
790
|
+
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), this._wfMaxRayCount );
|
|
1650
791
|
|
|
1651
|
-
}
|
|
792
|
+
} );
|
|
793
|
+
this._kernelManager.register( 'enterFull',
|
|
794
|
+
enterFullFn().compute( [ 1, 1, 1 ], [ 1, 1, 1 ] )
|
|
795
|
+
);
|
|
1652
796
|
|
|
1653
|
-
|
|
797
|
+
const copyReadB = qm.activeIndicesRO.b; // compact writes B (pingPong fixed at 0)
|
|
798
|
+
const copyWriteA = qm.activeIndices.a;
|
|
799
|
+
const copyFn = Fn( () => {
|
|
1654
800
|
|
|
1655
|
-
|
|
801
|
+
const tid = instanceIndex;
|
|
802
|
+
If( tid.greaterThanEqual( atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) ) ), () => {
|
|
1656
803
|
|
|
1657
|
-
|
|
1658
|
-
this.reset();
|
|
804
|
+
Return();
|
|
1659
805
|
|
|
1660
|
-
}
|
|
806
|
+
} );
|
|
807
|
+
copyWriteA.element( tid ).assign( copyReadB.element( tid ) );
|
|
1661
808
|
|
|
1662
|
-
|
|
809
|
+
} );
|
|
810
|
+
this._kernelManager.register( 'compactCopyback',
|
|
811
|
+
copyFn().compute( [ Math.ceil( maxRays / 256 ), 1, 1 ], [ 256, 1, 1 ] )
|
|
812
|
+
);
|
|
1663
813
|
|
|
1664
|
-
|
|
814
|
+
const fwFn = buildFinalWriteKernel( {
|
|
815
|
+
rayBufferRO: pb.rayBuffer.ro,
|
|
816
|
+
gBufferRO,
|
|
817
|
+
writeColorTex: writeTex.color,
|
|
818
|
+
writeNDTex: writeTex.normalDepth,
|
|
819
|
+
writeAlbedoTex: writeTex.albedo,
|
|
820
|
+
resolution: this.resolution,
|
|
821
|
+
frame: this.frame,
|
|
822
|
+
enableAccumulation: this.enableAccumulation,
|
|
823
|
+
hasPreviousAccumulated: this.hasPreviousAccumulated,
|
|
824
|
+
accumulationAlpha: this.accumulationAlpha,
|
|
825
|
+
cameraIsMoving: this.cameraIsMoving,
|
|
826
|
+
transparentBackground: this.transparentBackground,
|
|
827
|
+
prevAccumTexture: prevColor,
|
|
828
|
+
prevNormalDepthTexture: prevND,
|
|
829
|
+
prevAlbedoTexture: prevAlbedo,
|
|
830
|
+
renderWidth: this._wfRenderWidth,
|
|
831
|
+
renderHeight: this._wfRenderHeight,
|
|
832
|
+
samplesPerPass: S,
|
|
833
|
+
visMode: this.visMode,
|
|
834
|
+
} );
|
|
835
|
+
this._kernelManager.register( 'finalWrite',
|
|
836
|
+
// Per-pixel (w×h) — kernel averages the S sample-slots internally.
|
|
837
|
+
fwFn().compute(
|
|
838
|
+
[ Math.ceil( w / FINALWRITE_WG_SIZE ), Math.ceil( h / FINALWRITE_WG_SIZE ), 1 ],
|
|
839
|
+
[ FINALWRITE_WG_SIZE, FINALWRITE_WG_SIZE, 1 ]
|
|
840
|
+
)
|
|
841
|
+
);
|
|
1665
842
|
|
|
1666
|
-
|
|
843
|
+
// Debug visualization (visMode 1-10): single-pass primary-ray kernel. Reuses the same fresh*
|
|
844
|
+
// scene nodes so _refreshWfTextureNodes keeps it current; mode 11 (NaN/Inf) is FinalWrite's branch.
|
|
845
|
+
const debugFn = buildDebugKernel( {
|
|
846
|
+
writeColorTex: writeTex.color,
|
|
847
|
+
writeNDTex: writeTex.normalDepth,
|
|
848
|
+
writeAlbedoTex: writeTex.albedo,
|
|
849
|
+
resolution: this.resolution,
|
|
850
|
+
renderWidth: this._wfRenderWidth,
|
|
851
|
+
renderHeight: this._wfRenderHeight,
|
|
852
|
+
cameraWorldMatrix: this.cameraWorldMatrix,
|
|
853
|
+
cameraProjectionMatrixInverse: this.cameraProjectionMatrixInverse,
|
|
854
|
+
cameraProjectionMatrix: this.cameraProjectionMatrix,
|
|
855
|
+
cameraViewMatrix: this.cameraViewMatrix,
|
|
856
|
+
enableDOF: this.enableDOF,
|
|
857
|
+
focalLength: this.focalLength,
|
|
858
|
+
aperture: this.aperture,
|
|
859
|
+
focusDistance: this.focusDistance,
|
|
860
|
+
sceneScale: this.sceneScale,
|
|
861
|
+
apertureScale: this.apertureScale,
|
|
862
|
+
anamorphicRatio: this.anamorphicRatio,
|
|
863
|
+
bvhBuffer: freshBvh,
|
|
864
|
+
triangleBuffer: freshTri,
|
|
865
|
+
materialBuffer: freshMat,
|
|
866
|
+
envTexture: freshEnvTex,
|
|
867
|
+
environmentMatrix: this.environmentMatrix,
|
|
868
|
+
environmentIntensity: this.environmentIntensity,
|
|
869
|
+
enableEnvironmentLight: this.enableEnvironment,
|
|
870
|
+
visMode: this.visMode,
|
|
871
|
+
debugVisScale: this.debugVisScale,
|
|
872
|
+
samplesPerPass: this._samplesPerPass,
|
|
873
|
+
albedoMaps: freshAlbedoMaps,
|
|
874
|
+
normalMaps: freshNormalMaps,
|
|
875
|
+
bumpMaps: freshBumpMaps,
|
|
876
|
+
metalnessMaps: freshMetalnessMaps,
|
|
877
|
+
roughnessMaps: freshRoughnessMaps,
|
|
878
|
+
emissiveMaps: freshEmissiveMaps,
|
|
879
|
+
frame: this.frame,
|
|
880
|
+
} );
|
|
881
|
+
this._kernelManager.register( 'debug',
|
|
882
|
+
debugFn().compute(
|
|
883
|
+
[ Math.ceil( w / DEBUG_WG_SIZE ), Math.ceil( h / DEBUG_WG_SIZE ), 1 ],
|
|
884
|
+
[ DEBUG_WG_SIZE, DEBUG_WG_SIZE, 1 ]
|
|
885
|
+
)
|
|
886
|
+
);
|
|
1667
887
|
|
|
1668
|
-
|
|
888
|
+
this._wavefrontReady = true;
|
|
889
|
+
console.log( `PathTracer: all wavefront kernels built (${w}×${h}, ${maxRays} rays)` );
|
|
1669
890
|
|
|
1670
891
|
}
|
|
1671
892
|
|
|
1672
|
-
|
|
893
|
+
_setWfDispatch() {
|
|
1673
894
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
dispose() {
|
|
895
|
+
const w = this._wfRenderWidth.value;
|
|
896
|
+
const h = this._wfRenderHeight.value;
|
|
897
|
+
const S = this._samplesPerPass | 0;
|
|
1678
898
|
|
|
1679
|
-
|
|
1680
|
-
|
|
899
|
+
this._kernelManager.setDispatchCount( 'generate', [
|
|
900
|
+
Math.ceil( w / GENERATE_WG_SIZE ),
|
|
901
|
+
Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1
|
|
902
|
+
] );
|
|
903
|
+
this._kernelManager.setDispatchCount( 'finalWrite', [
|
|
904
|
+
Math.ceil( w / FINALWRITE_WG_SIZE ),
|
|
905
|
+
Math.ceil( h / FINALWRITE_WG_SIZE ), 1
|
|
906
|
+
] );
|
|
907
|
+
this._kernelManager.setDispatchCount( 'debug', [
|
|
908
|
+
Math.ceil( w / DEBUG_WG_SIZE ),
|
|
909
|
+
Math.ceil( h / DEBUG_WG_SIZE ), 1
|
|
910
|
+
] );
|
|
1681
911
|
|
|
1682
|
-
|
|
1683
|
-
this.renderModeChangeTimeout = null;
|
|
912
|
+
}
|
|
1684
913
|
|
|
1685
|
-
|
|
914
|
+
dispose() {
|
|
1686
915
|
|
|
1687
|
-
|
|
1688
|
-
this.
|
|
1689
|
-
this.
|
|
1690
|
-
this.
|
|
1691
|
-
this.
|
|
1692
|
-
this.
|
|
1693
|
-
this.
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
this.
|
|
1697
|
-
|
|
1698
|
-
// Dispose textures
|
|
1699
|
-
this.stbnScalarTexture?.dispose();
|
|
1700
|
-
this.stbnVec2Texture?.dispose();
|
|
1701
|
-
this.placeholderTexture?.dispose();
|
|
1702
|
-
|
|
1703
|
-
// Clear data references
|
|
1704
|
-
this.triangleStorageAttr = null;
|
|
1705
|
-
this.triangleStorageNode = null;
|
|
1706
|
-
this.bvhStorageAttr = null;
|
|
1707
|
-
this.bvhStorageNode = null;
|
|
1708
|
-
this.placeholderTexture = null;
|
|
1709
|
-
|
|
1710
|
-
this.isReady = false;
|
|
916
|
+
super.dispose();
|
|
917
|
+
this._packedBuffers?.dispose();
|
|
918
|
+
this._queueManager?.dispose();
|
|
919
|
+
this._kernelManager?.dispose();
|
|
920
|
+
this._gBufferAttr?.dispose?.();
|
|
921
|
+
this._packedBuffers = null;
|
|
922
|
+
this._queueManager = null;
|
|
923
|
+
this._kernelManager = null;
|
|
924
|
+
this._gBufferAttr = null;
|
|
925
|
+
this._wavefrontReady = false;
|
|
1711
926
|
|
|
1712
927
|
}
|
|
1713
928
|
|