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.
Files changed (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7554 -7014
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +12 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +76 -20
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +109 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +873 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
@@ -1,483 +0,0 @@
1
- import { Fn, wgslFn, uniform, int, uint, ivec2, uvec2, If,
2
- textureLoad, textureStore, localId, workgroupId } from 'three/tsl';
3
- import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
4
- import { NearestFilter, LinearFilter, RGBAFormat, HalfFloatType, FloatType } from 'three';
5
- import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
- import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
7
-
8
- // ── wgslFn helpers ──────────────────────────────────────────
9
-
10
- /**
11
- * Map temporal variance to normalised sample count with convergence logic.
12
- *
13
- * Uses temporal variance (frame-to-frame pixel change from Variance)
14
- * as the primary convergence signal, with a small spatial variance boost for noisy
15
- * neighbourhoods where the temporal EMA may underestimate.
16
- *
17
- * Returns vec4f(normalizedSamples, varianceRatio, converged, 1.0).
18
- */
19
- const computeSamplingGuidance = /*@__PURE__*/ wgslFn( `
20
- fn computeSamplingGuidance(
21
- temporalVariance: f32,
22
- spatialVariance: f32,
23
- meanLuminance: f32,
24
- threshold: f32,
25
- frame: i32,
26
- minFrames: i32,
27
- convThreshold: f32,
28
- sensitivity: f32,
29
- convSpeedScale: f32
30
- ) -> vec4f {
31
-
32
- // The path tracer accumulates via alpha = 1/(frame+1), so temporal variance
33
- // of the accumulated output shrinks as ~sigma²/(frame+1)². Scale by (frame+1)
34
- // to get accumulated image quality ~sigma²/N — decreases as image converges.
35
- let frameScale = f32( frame + 1 );
36
- let effectiveVariance = temporalVariance * frameScale;
37
-
38
- // Normalize by luminance² — converts absolute variance to relative (CV²).
39
- // Floor of 0.01 prevents noise amplification for near-black pixels
40
- // (linear luminance < 0.1 → below perceptual visibility threshold).
41
- let normFactor = max( meanLuminance * meanLuminance, 0.01 );
42
- let normalizedVariance = effectiveVariance / normFactor;
43
-
44
- let varianceRatio = clamp( normalizedVariance / threshold, 0.0, 1.0 );
45
-
46
- // Apply sensitivity — higher values assign more samples to noisy pixels
47
- var normalizedSamples = clamp( varianceRatio * sensitivity, 0.0, 1.0 );
48
-
49
- // Small spatial boost for noisy neighbourhoods (un-scaled — provides
50
- // a minor secondary signal that naturally diminishes as image converges)
51
- let spatialBoost = clamp( spatialVariance / ( threshold * 4.0 ), 0.0, 0.2 );
52
- normalizedSamples = clamp( normalizedSamples + spatialBoost, 0.0, 1.0 );
53
-
54
- // Warm-up: variance estimates need a few frames to stabilise
55
- if ( frame < minFrames ) {
56
-
57
- let warmupFactor = f32( frame ) / f32( minFrames );
58
- normalizedSamples = mix( 1.0, normalizedSamples, warmupFactor * warmupFactor );
59
-
60
- }
61
-
62
- // Convergence: mark pixel only when per-frame noise is truly negligible.
63
- // convSpeedScale controls aggressiveness: higher = easier to converge
64
- // (scales the threshold up, so more pixels qualify as converged).
65
- let scaledConvThreshold = convThreshold * convSpeedScale;
66
- var converged = 0.0;
67
- if ( normalizedVariance < scaledConvThreshold && frame > minFrames ) {
68
-
69
- converged = 1.0;
70
-
71
- }
72
-
73
- return vec4f(
74
- normalizedSamples,
75
- varianceRatio,
76
- converged,
77
- 1.0
78
- );
79
-
80
- }
81
- ` );
82
-
83
- /**
84
- * 5-colour heatmap gradient with convergence desaturation.
85
- *
86
- * blue (t=0) → cyan → green → yellow → red (t=1)
87
- */
88
- const heatmapGradient = /*@__PURE__*/ wgslFn( `
89
- fn heatmapGradient( t: f32, normalizedVariance: f32, converged: f32 ) -> vec4f {
90
-
91
- let r = clamp( ( t - 0.5 ) * 4.0, 0.0, 1.0 );
92
- let g = clamp( t * 4.0, 0.0, 1.0 ) - clamp( ( t - 0.75 ) * 4.0, 0.0, 1.0 );
93
- let b = 1.0 - clamp( ( t - 0.25 ) * 4.0, 0.0, 1.0 );
94
-
95
- var color = vec3f( r, g, b );
96
-
97
- // Convergence: desaturate converged pixels
98
- if ( converged > 0.5 ) {
99
-
100
- let gray = color.x * 0.299 + color.y * 0.587 + color.z * 0.114;
101
- color = mix( color, vec3f( gray ), 0.6 );
102
-
103
- }
104
-
105
- // Brightness modulation
106
- color *= 0.7 + normalizedVariance * 0.3;
107
-
108
- return vec4f( color, 1.0 );
109
-
110
- }
111
- ` );
112
-
113
- /**
114
- * WebGPU Adaptive Sampling Stage (Compute Shader)
115
- *
116
- * Reads per-pixel temporal variance from Variance and
117
- * produces a guidance texture that tells the path tracer how many
118
- * samples each pixel needs.
119
- *
120
- * Algorithm:
121
- * 1. Read temporal + spatial variance from variance:output
122
- * 2. Map temporal variance → normalised sample count (0–1)
123
- * 3. Apply sensitivity scaling and spatial boost
124
- * 4. Warm-up ramp for early frames (variance EMA not yet stable)
125
- * 5. Mark converged pixels (temporal variance below threshold)
126
- * 6. Write (normalizedSamples, varianceRatio, converged, 1) to StorageTexture
127
- *
128
- * Output format (RGBA HalfFloat):
129
- * R — normalizedSamples (0-1, multiply by adaptiveSamplingMax)
130
- * G — variance / threshold (debug / convergence weight)
131
- * B — convergedFlag (1.0 = pixel converged, skip sampling)
132
- * A — 1.0
133
- *
134
- * The path tracer reads this via getRequiredSamples() in TSL/PathTracer.js.
135
- *
136
- * Execution: PER_CYCLE — only updates when a full tile cycle completes,
137
- * ensuring variance is computed from complete frame data.
138
- *
139
- * Textures published: adaptiveSampling:output
140
- * Textures read: variance:output (from Variance)
141
- */
142
- export class AdaptiveSampling extends RenderStage {
143
-
144
- constructor( renderer, options = {} ) {
145
-
146
- super( 'AdaptiveSampling', {
147
- ...options,
148
- executionMode: StageExecutionMode.PER_CYCLE
149
- } );
150
-
151
- this.renderer = renderer;
152
- this.frameNumber = 0;
153
- this.delayByFrames = options.delayByFrames ?? 2;
154
- this.showAdaptiveSamplingHelper = false;
155
-
156
- // Sampling parameters
157
- this.adaptiveSamplingMax = uniform( options.adaptiveSamplingMax ?? DEFAULT_STATE.adaptiveSamplingMax ?? 32, 'int' );
158
- this.varianceThreshold = uniform( options.varianceThreshold ?? DEFAULT_STATE.adaptiveSamplingVarianceThreshold ?? 0.01 );
159
- this.materialBias = uniform( options.materialBias ?? DEFAULT_STATE.adaptiveSamplingMaterialBias ?? 1.2 );
160
- this.edgeBias = uniform( options.edgeBias ?? DEFAULT_STATE.adaptiveSamplingEdgeBias ?? 1.5 );
161
- this.convergenceSpeed = uniform( options.convergenceSpeed ?? DEFAULT_STATE.adaptiveSamplingConvergenceSpeed ?? 2.0 );
162
- this.frameNumberUniform = uniform( 0, 'int' );
163
-
164
- // Resolution uniforms (int for compute pixel coords)
165
- this.resolutionWidth = uniform( options.width || 1024 );
166
- this.resolutionHeight = uniform( options.height || 1024 );
167
-
168
- // Convergence parameters — temporal variance stabilises after ~10 frames (EMA alpha=0.1)
169
- this.minConvergenceFrames = uniform( 10 );
170
- // Must be well below varianceThreshold — convergence means "skip entirely".
171
- // With (frame+1)² scaling, effective variance ≈ 5×σ², so 0.01 → σ² ≈ 0.002.
172
- this.convergenceThreshold = uniform( 0.01 );
173
-
174
- // StorageTexture for compute output (replaces RenderTarget)
175
- const w = options.width || 1;
176
- const h = options.height || 1;
177
-
178
- // LinearFilter for textureLoad codegen compatibility
179
- this._outputStorageTex = new StorageTexture( w, h );
180
- this._outputStorageTex.type = HalfFloatType;
181
- this._outputStorageTex.format = RGBAFormat;
182
- this._outputStorageTex.minFilter = LinearFilter;
183
- this._outputStorageTex.magFilter = LinearFilter;
184
-
185
- // Heatmap StorageTexture for compute output
186
- this._heatmapStorageTex = new StorageTexture( w, h );
187
- this._heatmapStorageTex.type = FloatType;
188
- this._heatmapStorageTex.format = RGBAFormat;
189
- this._heatmapStorageTex.minFilter = NearestFilter;
190
- this._heatmapStorageTex.magFilter = NearestFilter;
191
-
192
- // Heatmap render target — FloatType, exposed as a public field for hosts to
193
- // display via their own readback helper.
194
- this.heatmapTarget = new RenderTarget( w, h, {
195
- format: RGBAFormat,
196
- type: FloatType,
197
- minFilter: NearestFilter,
198
- magFilter: NearestFilter,
199
- depthBuffer: false,
200
- stencilBuffer: false
201
- } );
202
-
203
- // Dispatch dimensions
204
- this._dispatchX = Math.ceil( w / 8 );
205
- this._dispatchY = Math.ceil( h / 8 );
206
-
207
- // Input: variance texture from Variance
208
- // Use regular TextureNode (not StorageTexture) as compile-time placeholder so
209
- // textureLoad codegen includes the required level parameter for texture_2d
210
- this._varianceTexNode = new TextureNode();
211
-
212
- // Build compute + heatmap shaders
213
- this._buildCompute();
214
- this._buildHeatmapCompute();
215
-
216
- }
217
-
218
- setupEventListeners() {
219
-
220
- this.on( 'pathtracer:viewpointChanged', () => this.reset() );
221
-
222
- }
223
-
224
- /**
225
- * Build compute shader that maps variance → sampling guidance.
226
- *
227
- * Reads per-pixel temporal and spatial variance from Variance
228
- * output and maps it to a normalised sample count. No shared memory needed —
229
- * each thread processes one pixel independently.
230
- *
231
- * Workgroup: [8,8,1] — 64 threads per workgroup
232
- */
233
- _buildCompute() {
234
-
235
- const varianceTex = this._varianceTexNode;
236
- const threshold = this.varianceThreshold;
237
- const sensitivity = this.materialBias; // "Sensitivity": higher = more samples for noisy pixels
238
- const convSpeedScale = this.convergenceSpeed; // "Convergence Speed": scales convergence threshold
239
- const frame = this.frameNumberUniform;
240
- const minFrames = this.minConvergenceFrames;
241
- const convThreshold = this.convergenceThreshold;
242
- const resW = this.resolutionWidth;
243
- const resH = this.resolutionHeight;
244
- const outputTex = this._outputStorageTex;
245
-
246
- const WG_SIZE = 8;
247
-
248
- const computeFn = Fn( () => {
249
-
250
- const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
251
- const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
252
-
253
- If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
254
-
255
- // Variance texture: R=mean, G=meanSq, B=temporalVariance, A=spatialVariance
256
- const varianceData = textureLoad( varianceTex, ivec2( gx, gy ) );
257
-
258
- const result = computeSamplingGuidance(
259
- varianceData.z, // temporal variance
260
- varianceData.w, // spatial variance
261
- varianceData.x, // mean luminance (for HDR normalization)
262
- threshold,
263
- int( frame ),
264
- int( minFrames ),
265
- convThreshold,
266
- sensitivity,
267
- convSpeedScale
268
- );
269
-
270
- textureStore(
271
- outputTex,
272
- uvec2( uint( gx ), uint( gy ) ),
273
- result
274
- ).toWriteOnly();
275
-
276
- } );
277
-
278
- } );
279
-
280
- this._computeNode = computeFn().compute(
281
- [ this._dispatchX, this._dispatchY, 1 ],
282
- [ WG_SIZE, WG_SIZE, 1 ]
283
- );
284
-
285
- }
286
-
287
- /**
288
- * Build heatmap visualization compute shader.
289
- *
290
- * Reads the sampling guidance StorageTexture via textureLoad and maps
291
- * normalizedSamples to a smooth blue→cyan→green→yellow→red gradient.
292
- * Converged pixels are desaturated, brightness is modulated by variance.
293
- * Writes to _heatmapStorageTex, then copied to the public heatmapTarget
294
- * RenderTarget so the host can display it.
295
- */
296
- _buildHeatmapCompute() {
297
-
298
- const samplingTex = this._outputStorageTex;
299
- const heatmapOut = this._heatmapStorageTex;
300
- const resW = this.resolutionWidth;
301
- const resH = this.resolutionHeight;
302
-
303
- const WG_SIZE = 8;
304
-
305
- const computeFn = Fn( () => {
306
-
307
- const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
308
- const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
309
-
310
- If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
311
-
312
- const data = textureLoad( samplingTex, ivec2( gx, gy ) );
313
- const result = heatmapGradient( data.x.clamp( 0.0, 1.0 ), data.y, data.z );
314
-
315
- textureStore(
316
- heatmapOut,
317
- uvec2( uint( gx ), uint( gy ) ),
318
- result
319
- ).toWriteOnly();
320
-
321
- } );
322
-
323
- } );
324
-
325
- this._heatmapComputeNode = computeFn().compute(
326
- [ this._dispatchX, this._dispatchY, 1 ],
327
- [ WG_SIZE, WG_SIZE, 1 ]
328
- );
329
-
330
- }
331
-
332
- /**
333
- * Enable or disable the heatmap compute pass. When enabled, the heatmap is
334
- * rendered each frame to {@link this.heatmapTarget} (a public RenderTarget)
335
- * for the host to display however it wants.
336
- */
337
- setHeatmapEnabled( enabled ) {
338
-
339
- this.showAdaptiveSamplingHelper = enabled;
340
-
341
- }
342
-
343
- render( context ) {
344
-
345
- if ( ! this.enabled ) return;
346
-
347
- // Delay a few frames to let the path tracer accumulate
348
- this.frameNumber ++;
349
- if ( this.frameNumber <= this.delayByFrames ) return;
350
-
351
- this.frameNumberUniform.value = this.frameNumber;
352
-
353
- // Get temporal/spatial variance from Variance
354
- const varianceTexture = context.getTexture( 'variance:output' );
355
- if ( ! varianceTexture ) return;
356
-
357
- // Auto-match storage texture size to variance output
358
- const img = varianceTexture.image;
359
- if ( img && img.width > 0 && img.height > 0 &&
360
- ( img.width !== this._outputStorageTex.image.width ||
361
- img.height !== this._outputStorageTex.image.height ) ) {
362
-
363
- this.setSize( img.width, img.height );
364
-
365
- }
366
-
367
- // Update input texture (no shader recompile, just swap value)
368
- this._varianceTexNode.value = varianceTexture;
369
-
370
- // Compute dispatch — map variance → sampling guidance
371
- this.renderer.compute( this._computeNode );
372
-
373
- // Publish guidance texture for PathTracer to consume
374
- // (StorageTexture extends Texture, works as regular texture for sampling)
375
- context.setTexture( 'adaptiveSampling:output', this._outputStorageTex );
376
-
377
- // Render heatmap into public heatmapTarget when enabled
378
- if ( this.showAdaptiveSamplingHelper ) {
379
-
380
- this.renderer.compute( this._heatmapComputeNode );
381
- this.renderer.copyTextureToTexture( this._heatmapStorageTex, this.heatmapTarget.texture );
382
-
383
- }
384
-
385
- }
386
-
387
- reset() {
388
-
389
- this.frameNumber = 0;
390
- this.frameNumberUniform.value = 0;
391
-
392
- // Remove stale guidance from context so PathTracer (which runs
393
- // before us) doesn't read converged-pixel data from the old viewpoint
394
- // during the delay frames before we publish fresh guidance.
395
- if ( this.context ) this.context.removeTexture( 'adaptiveSampling:output' );
396
-
397
- }
398
-
399
- setSize( width, height ) {
400
-
401
- this._outputStorageTex.setSize( width, height );
402
- this._heatmapStorageTex.setSize( width, height );
403
- this.heatmapTarget.setSize( width, height );
404
- this.heatmapTarget.texture.needsUpdate = true;
405
- this.resolutionWidth.value = width;
406
- this.resolutionHeight.value = height;
407
-
408
- // Update dispatch dimensions
409
- this._dispatchX = Math.ceil( width / 8 );
410
- this._dispatchY = Math.ceil( height / 8 );
411
- this._computeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
412
- this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
413
-
414
- }
415
-
416
- setAdaptiveSamplingMax( value ) {
417
-
418
- this.adaptiveSamplingMax.value = value;
419
-
420
- }
421
-
422
- setVarianceThreshold( value ) {
423
-
424
- this.varianceThreshold.value = value;
425
-
426
- }
427
-
428
- setMaterialBias( value ) {
429
-
430
- this.materialBias.value = value;
431
-
432
- }
433
-
434
- setEdgeBias( value ) {
435
-
436
- this.edgeBias.value = value;
437
-
438
- }
439
-
440
- setConvergenceSpeed( value ) {
441
-
442
- this.convergenceSpeed.value = value;
443
-
444
- }
445
-
446
- /**
447
- * Unified setter for multiple adaptive sampling parameters.
448
- * @param {Object} params
449
- * @param {number} [params.threshold] - Variance threshold
450
- * @param {number} [params.materialBias] - Material bias multiplier
451
- * @param {number} [params.edgeBias] - Edge bias multiplier
452
- * @param {number} [params.convergenceSpeedUp] - Convergence speed
453
- * @param {number} [params.adaptiveSamplingMax] - Max samples
454
- */
455
- setAdaptiveSamplingParameters( params ) {
456
-
457
- if ( params.threshold !== undefined ) this.setVarianceThreshold( params.threshold );
458
- if ( params.materialBias !== undefined ) this.setMaterialBias( params.materialBias );
459
- if ( params.edgeBias !== undefined ) this.setEdgeBias( params.edgeBias );
460
- if ( params.convergenceSpeedUp !== undefined ) this.setConvergenceSpeed( params.convergenceSpeedUp );
461
- if ( params.adaptiveSamplingMax !== undefined ) this.setAdaptiveSamplingMax( params.adaptiveSamplingMax );
462
-
463
- }
464
-
465
- dispose() {
466
-
467
- this._computeNode?.dispose();
468
- this._heatmapComputeNode?.dispose();
469
- this._heatmapStorageTex?.dispose();
470
- this._outputStorageTex?.dispose();
471
- this.heatmapTarget?.dispose();
472
- this._varianceTexNode?.dispose();
473
-
474
- this._computeNode = null;
475
- this._heatmapComputeNode = null;
476
- this._heatmapStorageTex = null;
477
- this._outputStorageTex = null;
478
- this.heatmapTarget = null;
479
- this._varianceTexNode = null;
480
-
481
- }
482
-
483
- }