rayzee 5.6.0 → 5.7.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 (37) hide show
  1. package/README.md +3 -0
  2. package/dist/assets/CDFWorker-BFQUr3By.js +2 -0
  3. package/dist/assets/CDFWorker-BFQUr3By.js.map +1 -0
  4. package/dist/rayzee.es.js +965 -903
  5. package/dist/rayzee.es.js.map +1 -1
  6. package/dist/rayzee.umd.js +49 -43
  7. package/dist/rayzee.umd.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/EngineEvents.js +3 -0
  10. package/src/Passes/AIUpscaler.js +22 -0
  11. package/src/Passes/OIDNDenoiser.js +93 -28
  12. package/src/PathTracerApp.js +17 -0
  13. package/src/Pipeline/RenderPipeline.js +3 -0
  14. package/src/Processor/EquirectHDRInfo.js +76 -16
  15. package/src/Processor/ShaderBuilder.js +1 -0
  16. package/src/Processor/Workers/CDFWorker.js +72 -11
  17. package/src/Stages/ASVGF.js +18 -0
  18. package/src/Stages/AdaptiveSampling.js +2 -0
  19. package/src/Stages/AutoExposure.js +2 -0
  20. package/src/Stages/BilateralFilter.js +2 -0
  21. package/src/Stages/Display.js +1 -0
  22. package/src/Stages/EdgeFilter.js +1 -0
  23. package/src/Stages/MotionVector.js +1 -0
  24. package/src/Stages/SSRC.js +6 -0
  25. package/src/Stages/Variance.js +3 -0
  26. package/src/TSL/Common.js +9 -0
  27. package/src/TSL/Environment.js +15 -6
  28. package/src/TSL/LightsIndirect.js +1 -1
  29. package/src/TSL/LightsSampling.js +5 -4
  30. package/src/TSL/PathTracer.js +2 -2
  31. package/src/TSL/PathTracerCore.js +6 -5
  32. package/src/managers/DenoisingManager.js +68 -14
  33. package/src/managers/EnvironmentManager.js +17 -0
  34. package/src/managers/TransformManager.js +9 -0
  35. package/src/managers/UniformManager.js +1 -0
  36. package/dist/assets/CDFWorker-2MoynL4F.js +0 -2
  37. package/dist/assets/CDFWorker-2MoynL4F.js.map +0 -1
@@ -275,6 +275,7 @@ export class EdgeFilter extends RenderStage {
275
275
  this._computeNode?.dispose();
276
276
  this._outputStorageTex?.dispose();
277
277
  this.outputTarget?.dispose();
278
+ this._inputTexNode?.dispose();
278
279
 
279
280
  }
280
281
 
@@ -547,6 +547,7 @@ export class MotionVector extends RenderStage {
547
547
  this._worldSpaceStorageTex?.dispose();
548
548
  this.screenSpaceTarget?.dispose();
549
549
  this.worldSpaceTarget?.dispose();
550
+ this._normalDepthTexNode?.dispose();
550
551
 
551
552
  }
552
553
 
@@ -194,6 +194,12 @@ export class SSRC extends RenderStage {
194
194
  this._prevNDTexA.dispose();
195
195
  this._prevNDTexB.dispose();
196
196
  this._outputTex.dispose();
197
+ this._colorTexNode?.dispose();
198
+ this._ndTexNode?.dispose();
199
+ this._motionTexNode?.dispose();
200
+ this._readCacheTexNode?.dispose();
201
+ this._readPrevNDTexNode?.dispose();
202
+ this._readPass1CacheTexNode?.dispose();
197
203
 
198
204
  }
199
205
 
@@ -371,6 +371,9 @@ export class Variance extends RenderStage {
371
371
  this._computeNodeB?.dispose();
372
372
  this._storageTexA?.dispose();
373
373
  this._storageTexB?.dispose();
374
+ this._colorTexNode?.dispose();
375
+ this._readTexNodeA?.dispose();
376
+ this._readTexNodeB?.dispose();
374
377
 
375
378
  }
376
379
 
package/src/TSL/Common.js CHANGED
@@ -99,6 +99,15 @@ export const powerHeuristic = wgslFn( `
99
99
  }
100
100
  ` );
101
101
 
102
+ // Balance heuristic — optimal for MIS-compensated env map sampling (Karlík et al. 2019)
103
+ export const balanceHeuristic = wgslFn( `
104
+ fn balanceHeuristic( pdf1: f32, pdf2: f32 ) -> f32 {
105
+
106
+ return pdf1 / max( pdf1 + pdf2, ${MIN_PDF} );
107
+
108
+ }
109
+ ` );
110
+
102
111
  // Bayer matrix 4x4 dithering — exact port of GLSL
103
112
  export const applyDithering = wgslFn( `
104
113
  fn applyDithering( color: vec3f, uv: vec2f, ditheringAmount: f32, resolution: vec2f ) -> vec3f {
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, vec2, vec4, float, int, If, texture, sampler, dot, floor, fract, min, mix, clamp } from 'three/tsl';
1
+ import { Fn, wgslFn, vec2, vec4, float, int, If, texture, sampler, dot, sin, floor, fract, min, max, mix, clamp } from 'three/tsl';
2
2
 
3
3
  import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
4
4
 
@@ -47,9 +47,9 @@ export const equirectDirectionPdf = /*@__PURE__*/ wgslFn( `
47
47
  `, [ equirectDirectionToUv ] );
48
48
 
49
49
  // Evaluate PDF for a given direction (for MIS)
50
- // Exact implementation from three-gpu-pathtracer
51
50
  // Returns vec4(color.rgb, pdf) since TSL cannot use inout params
52
- export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix, envTotalSum, envResolution ] ) => {
51
+ // Uses MIS-compensated PDF (Karlík et al. 2019): max(0, lum - delta) / compensatedTotalSum
52
+ export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix, envTotalSum, envCompensationDelta, envResolution ] ) => {
53
53
 
54
54
  const result = vec4( 0.0 ).toVar();
55
55
 
@@ -63,8 +63,13 @@ export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix,
63
63
  const uv = equirectDirectionToUv( { direction, environmentMatrix } ).toVar();
64
64
  const color = texture( environment, uv, 0 ).rgb.toVar();
65
65
 
66
+ // sin(theta) matches the CDF's solid-angle weighting (lum * sinTheta)
67
+ const sinTheta = sin( uv.y.mul( Math.PI ) ).toVar();
66
68
  const lum = dot( color, REC709_LUMINANCE_COEFFICIENTS ).toVar();
67
- const pdf = lum.div( envTotalSum ).toVar();
69
+ const weightedLum = lum.mul( sinTheta ).toVar();
70
+ // MIS Compensation: subtract delta to match the sharpened CDF
71
+ const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) ).toVar();
72
+ const pdf = compensatedWeight.div( envTotalSum ).toVar();
68
73
 
69
74
  const dirPdf = equirectDirectionPdf( { direction, environmentMatrix } ).toVar();
70
75
  const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf ).toVar();
@@ -86,6 +91,7 @@ export const sampleEquirectProbability = Fn( ( [
86
91
  environmentMatrix,
87
92
  environmentIntensity,
88
93
  envTotalSum,
94
+ envCompensationDelta,
89
95
  envResolution,
90
96
  r,
91
97
  colorOutput
@@ -132,9 +138,12 @@ export const sampleEquirectProbability = Fn( ( [
132
138
  // Write color to output parameter (avoids redundant CDF texture lookups)
133
139
  colorOutput.assign( color );
134
140
 
135
- // Calculate PDF
141
+ // Calculate PDF — sin(theta) weighting + MIS Compensation (Karlík et al. 2019)
142
+ const sinTheta = sin( uv.y.mul( Math.PI ) ).toVar();
136
143
  const lum = dot( color.div( environmentIntensity ), REC709_LUMINANCE_COEFFICIENTS ).toVar();
137
- const pdf = lum.div( envTotalSum ).toVar();
144
+ const weightedLum = lum.mul( sinTheta ).toVar();
145
+ const compensatedWeight = max( float( 0.0 ), weightedLum.sub( envCompensationDelta ) ).toVar();
146
+ const pdf = compensatedWeight.div( envTotalSum ).toVar();
138
147
 
139
148
  const dirPdf = equirectDirectionPdf( { direction, environmentMatrix } ).toVar();
140
149
  const finalPdf = float( envResolution.x ).mul( float( envResolution.y ) ).mul( pdf ).mul( dirPdf ).toVar();
@@ -273,7 +273,7 @@ export const calculateIndirectLighting = Fn( ( [
273
273
  samplingInfo,
274
274
  // Environment resources
275
275
  envTexture, environmentIntensity, envMatrix,
276
- envTotalSum, envResolution,
276
+ envTotalSum, envCompensationDelta, envResolution,
277
277
  enableEnvironmentLight, useEnvMapIS,
278
278
  ] ) => {
279
279
 
@@ -84,6 +84,7 @@ import {
84
84
  EPSILON,
85
85
  MIN_PDF,
86
86
  powerHeuristic,
87
+ balanceHeuristic,
87
88
  } from './Common.js';
88
89
  import {
89
90
  sampleEquirectProbability,
@@ -935,7 +936,7 @@ export const calculateDirectLightingUnified = Fn( ( [
935
936
  // Environment resources
936
937
  envTexture, environmentIntensity, envMatrix,
937
938
  envCDFBuffer,
938
- envTotalSum, envResolution,
939
+ envTotalSum, envCompensationDelta, envResolution,
939
940
  enableEnvironmentLight,
940
941
  ] ) => {
941
942
 
@@ -1204,7 +1205,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1204
1205
  // Sample direction + PDF + color from importance-sampled environment
1205
1206
  const envSampleResult = sampleEquirectProbability(
1206
1207
  envTexture, envCDFBuffer,
1207
- envMatrix, environmentIntensity, envTotalSum, envResolution, envRandom, envColor
1208
+ envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
1208
1209
  ).toVar();
1209
1210
 
1210
1211
  const envDirection = envSampleResult.xyz.toVar();
@@ -1229,11 +1230,11 @@ export const calculateDirectLightingUnified = Fn( ( [
1229
1230
  const brdfValue = evaluateMaterialResponse( viewDir, envDirection, hitNormal, material );
1230
1231
  const bPdf = calculateMaterialPDF( viewDir, envDirection, hitNormal, material ).toVar();
1231
1232
 
1232
- // Standard two-strategy MIS: NEE (envPdf) vs implicit miss (materialPdf).
1233
+ // Balance heuristic for env MIS optimal for MIS-compensated PDFs (Karlík et al. 2019).
1233
1234
  // The implicit path uses material combinedPdf as prevBouncePdf at the miss check.
1234
1235
  const misW = select(
1235
1236
  bPdf.greaterThan( 0.0 ),
1236
- powerHeuristic( { pdf1: envPdf, pdf2: bPdf } ),
1237
+ balanceHeuristic( { pdf1: envPdf, pdf2: bPdf } ),
1237
1238
  float( 1.0 )
1238
1239
  ).toVar();
1239
1240
 
@@ -138,7 +138,7 @@ export const pathTracerMain = ( params ) => {
138
138
  spotLightsBuffer, numSpotLights,
139
139
  envTexture, environmentIntensity, envMatrix,
140
140
  envCDFBuffer,
141
- envTotalSum, envResolution,
141
+ envTotalSum, envCompensationDelta, envResolution,
142
142
  enableEnvironmentLight, useEnvMapIS,
143
143
  maxBounceCount, transmissiveBounces,
144
144
  showBackground, transparentBackground, backgroundIntensity,
@@ -285,7 +285,7 @@ export const pathTracerMain = ( params ) => {
285
285
  spotLightsBuffer, numSpotLights,
286
286
  envTexture, environmentIntensity, envMatrix,
287
287
  envCDFBuffer,
288
- envTotalSum, envResolution,
288
+ envTotalSum, envCompensationDelta, envResolution,
289
289
  enableEnvironmentLight, useEnvMapIS,
290
290
  maxBounceCount, transmissiveBounces,
291
291
  backgroundIntensity, showBackground, transparentBackground,
@@ -56,6 +56,7 @@ import {
56
56
  applySoftSuppressionRGB,
57
57
  getMaterial,
58
58
  powerHeuristic,
59
+ balanceHeuristic,
59
60
  } from './Common.js';
60
61
  import {
61
62
  DirectionSample,
@@ -582,7 +583,7 @@ export const Trace = Fn( ( [
582
583
  // Environment
583
584
  envTexture, environmentIntensity, envMatrix,
584
585
  envCDFBuffer,
585
- envTotalSum, envResolution,
586
+ envTotalSum, envCompensationDelta, envResolution,
586
587
  enableEnvironmentLight, useEnvMapIS,
587
588
  // Rendering parameters
588
589
  maxBounceCount, transmissiveBounces,
@@ -703,12 +704,12 @@ export const Trace = Fn( ( [
703
704
  If( prevBouncePdf.greaterThan( 0.0 ).and( enableEnvironmentLight ).and( useEnvMapIS ), () => {
704
705
 
705
706
  const envEval = sampleEquirect(
706
- envTexture, rayDirection, envMatrix, envTotalSum, envResolution,
707
+ envTexture, rayDirection, envMatrix, envTotalSum, envCompensationDelta, envResolution,
707
708
  );
708
709
  const envPdf = envEval.w.toVar();
709
710
  If( envPdf.greaterThan( 0.0 ), () => {
710
711
 
711
- envMisWeight.assign( powerHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) );
712
+ envMisWeight.assign( balanceHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) );
712
713
 
713
714
  } );
714
715
 
@@ -1001,7 +1002,7 @@ export const Trace = Fn( ( [
1001
1002
  materialBuffer,
1002
1003
  envTexture, environmentIntensity, envMatrix,
1003
1004
  envCDFBuffer,
1004
- envTotalSum, envResolution,
1005
+ envTotalSum, envCompensationDelta, envResolution,
1005
1006
  enableEnvironmentLight,
1006
1007
  );
1007
1008
 
@@ -1120,7 +1121,7 @@ export const Trace = Fn( ( [
1120
1121
  rngState,
1121
1122
  samplingInfo,
1122
1123
  envTexture, environmentIntensity, envMatrix,
1123
- envTotalSum, envResolution,
1124
+ envTotalSum, envCompensationDelta, envResolution,
1124
1125
  enableEnvironmentLight, useEnvMapIS,
1125
1126
  ) );
1126
1127
  throughput.mulAssign( indirectResult.throughput );
@@ -54,6 +54,17 @@ export class DenoisingManager extends EventDispatcher {
54
54
  this._lastRenderWidth = 0;
55
55
  this._lastRenderHeight = 0;
56
56
 
57
+ // Track the current completion-chain listener so it can be removed on re-trigger
58
+ this._pendingStartUpscaler = null;
59
+
60
+ // Bound event forwarding handlers (stored for removal on re-setup / dispose)
61
+ this._denoiserStartHandler = null;
62
+ this._denoiserEndHandler = null;
63
+ this._upscalerResChangedHandler = null;
64
+ this._upscalerStartHandler = null;
65
+ this._upscalerProgressHandler = null;
66
+ this._upscalerEndHandler = null;
67
+
57
68
  }
58
69
 
59
70
  _createDenoiserCanvas( mainCanvas ) {
@@ -146,11 +157,13 @@ export class DenoisingManager extends EventDispatcher {
146
157
 
147
158
  this.denoiser.enabled = DEFAULT_STATE.enableOIDN;
148
159
 
149
- // Forward lifecycle events
150
- this.denoiser.addEventListener( 'start', () =>
151
- this.dispatchEvent( { type: EngineEvents.DENOISING_START } ) );
152
- this.denoiser.addEventListener( 'end', () =>
153
- this.dispatchEvent( { type: EngineEvents.DENOISING_END } ) );
160
+ // Forward lifecycle events (store refs for removal on re-setup / dispose)
161
+ this._denoiserStartHandler = () =>
162
+ this.dispatchEvent( { type: EngineEvents.DENOISING_START } );
163
+ this._denoiserEndHandler = () =>
164
+ this.dispatchEvent( { type: EngineEvents.DENOISING_END } );
165
+ this.denoiser.addEventListener( 'start', this._denoiserStartHandler );
166
+ this.denoiser.addEventListener( 'end', this._denoiserEndHandler );
154
167
 
155
168
  }
156
169
 
@@ -189,15 +202,19 @@ export class DenoisingManager extends EventDispatcher {
189
202
 
190
203
  this.upscaler.enabled = DEFAULT_STATE.enableUpscaler || false;
191
204
 
192
- // Forward lifecycle events
193
- this.upscaler.addEventListener( 'resolution_changed', ( e ) =>
194
- this.dispatchEvent( { type: 'resolution_changed', width: e.width, height: e.height } ) );
195
- this.upscaler.addEventListener( 'start', () =>
196
- this.dispatchEvent( { type: EngineEvents.UPSCALING_START } ) );
197
- this.upscaler.addEventListener( 'progress', ( e ) =>
198
- this.dispatchEvent( { type: EngineEvents.UPSCALING_PROGRESS, progress: e.progress } ) );
199
- this.upscaler.addEventListener( 'end', () =>
200
- this.dispatchEvent( { type: EngineEvents.UPSCALING_END } ) );
205
+ // Forward lifecycle events (store refs for removal on re-setup / dispose)
206
+ this._upscalerResChangedHandler = ( e ) =>
207
+ this.dispatchEvent( { type: 'resolution_changed', width: e.width, height: e.height } );
208
+ this._upscalerStartHandler = () =>
209
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_START } );
210
+ this._upscalerProgressHandler = ( e ) =>
211
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_PROGRESS, progress: e.progress } );
212
+ this._upscalerEndHandler = () =>
213
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_END } );
214
+ this.upscaler.addEventListener( 'resolution_changed', this._upscalerResChangedHandler );
215
+ this.upscaler.addEventListener( 'start', this._upscalerStartHandler );
216
+ this.upscaler.addEventListener( 'progress', this._upscalerProgressHandler );
217
+ this.upscaler.addEventListener( 'end', this._upscalerEndHandler );
201
218
 
202
219
  }
203
220
 
@@ -353,8 +370,23 @@ export class DenoisingManager extends EventDispatcher {
353
370
  * @param {Function} params.isStillComplete - () => boolean, guard for async race
354
371
  * @param {import('../Pipeline/PipelineContext.js').PipelineContext} params.context
355
372
  */
373
+ _cleanupCompletionListener() {
374
+
375
+ if ( this._pendingStartUpscaler && this.denoiser ) {
376
+
377
+ this.denoiser.removeEventListener( 'end', this._pendingStartUpscaler );
378
+
379
+ }
380
+
381
+ this._pendingStartUpscaler = null;
382
+
383
+ }
384
+
356
385
  onRenderComplete( { isStillComplete, context } ) {
357
386
 
387
+ // Remove any stale completion-chain listener from a previous render cycle
388
+ this._cleanupCompletionListener();
389
+
358
390
  // Show post-process canvas if any post-process is enabled
359
391
  if ( ( this.denoiser?.enabled || this.upscaler?.enabled ) && this.denoiserCanvas ) {
360
392
 
@@ -365,6 +397,8 @@ export class DenoisingManager extends EventDispatcher {
365
397
  // Chain: denoise first (if enabled), then upscale (if enabled)
366
398
  const startUpscaler = () => {
367
399
 
400
+ this._pendingStartUpscaler = null;
401
+
368
402
  if ( ! isStillComplete() ) return;
369
403
 
370
404
  if ( this.upscaler?.enabled ) {
@@ -377,6 +411,7 @@ export class DenoisingManager extends EventDispatcher {
377
411
 
378
412
  if ( this.denoiser?.enabled ) {
379
413
 
414
+ this._pendingStartUpscaler = startUpscaler;
380
415
  this.denoiser.addEventListener( 'end', startUpscaler, { once: true } );
381
416
  this.denoiser.start();
382
417
 
@@ -401,6 +436,9 @@ export class DenoisingManager extends EventDispatcher {
401
436
  */
402
437
  abort( mainCanvas ) {
403
438
 
439
+ // Remove stale completion-chain listener before aborting
440
+ this._cleanupCompletionListener();
441
+
404
442
  if ( mainCanvas ) mainCanvas.style.opacity = '1';
405
443
 
406
444
  if ( this.upscaler ) this.upscaler.abort();
@@ -416,8 +454,13 @@ export class DenoisingManager extends EventDispatcher {
416
454
 
417
455
  dispose() {
418
456
 
457
+ // Remove pending completion-chain listener
458
+ this._cleanupCompletionListener();
459
+
419
460
  if ( this.denoiser ) {
420
461
 
462
+ if ( this._denoiserStartHandler ) this.denoiser.removeEventListener( 'start', this._denoiserStartHandler );
463
+ if ( this._denoiserEndHandler ) this.denoiser.removeEventListener( 'end', this._denoiserEndHandler );
421
464
  this.denoiser.dispose();
422
465
  this.denoiser = null;
423
466
 
@@ -425,11 +468,22 @@ export class DenoisingManager extends EventDispatcher {
425
468
 
426
469
  if ( this.upscaler ) {
427
470
 
471
+ if ( this._upscalerResChangedHandler ) this.upscaler.removeEventListener( 'resolution_changed', this._upscalerResChangedHandler );
472
+ if ( this._upscalerStartHandler ) this.upscaler.removeEventListener( 'start', this._upscalerStartHandler );
473
+ if ( this._upscalerProgressHandler ) this.upscaler.removeEventListener( 'progress', this._upscalerProgressHandler );
474
+ if ( this._upscalerEndHandler ) this.upscaler.removeEventListener( 'end', this._upscalerEndHandler );
428
475
  this.upscaler.dispose();
429
476
  this.upscaler = null;
430
477
 
431
478
  }
432
479
 
480
+ this._denoiserStartHandler = null;
481
+ this._denoiserEndHandler = null;
482
+ this._upscalerResChangedHandler = null;
483
+ this._upscalerStartHandler = null;
484
+ this._upscalerProgressHandler = null;
485
+ this._upscalerEndHandler = null;
486
+
433
487
  if ( this.denoiserCanvas?.parentNode ) {
434
488
 
435
489
  this.denoiserCanvas.parentNode.removeChild( this.denoiserCanvas );
@@ -280,6 +280,7 @@ export class EnvironmentManager {
280
280
 
281
281
  this._updateCDFStorageBuffers();
282
282
  this.uniforms.set( 'envTotalSum', 0.0 );
283
+ this.uniforms.set( 'envCompensationDelta', 0.0 );
283
284
  this.uniforms.set( 'useEnvMapIS', 0 );
284
285
  return;
285
286
 
@@ -294,6 +295,7 @@ export class EnvironmentManager {
294
295
 
295
296
  this._updateCDFStorageBuffers();
296
297
  this.uniforms.set( 'envTotalSum', 0.0 );
298
+ this.uniforms.set( 'envCompensationDelta', 0.0 );
297
299
  this.uniforms.set( 'useEnvMapIS', 0 );
298
300
  return;
299
301
 
@@ -313,6 +315,7 @@ export class EnvironmentManager {
313
315
 
314
316
  this._updateCDFStorageBuffers();
315
317
  this.uniforms.set( 'envTotalSum', this.equirectHdrInfo.totalSum );
318
+ this.uniforms.set( 'envCompensationDelta', this.equirectHdrInfo.compensationDelta );
316
319
  this.uniforms.set( 'useEnvMapIS', 1 );
317
320
 
318
321
  const { width, height } = this.equirectHdrInfo;
@@ -329,6 +332,7 @@ export class EnvironmentManager {
329
332
  console.error( 'Error building environment CDF:', error );
330
333
  this.uniforms.set( 'useEnvMapIS', 0 );
331
334
  this.uniforms.set( 'envTotalSum', 0.0 );
335
+ this.uniforms.set( 'envCompensationDelta', 0.0 );
332
336
 
333
337
  }
334
338
 
@@ -375,6 +379,7 @@ export class EnvironmentManager {
375
379
 
376
380
  this._updateCDFStorageBuffers();
377
381
  this.uniforms.set( 'envTotalSum', 0.0 );
382
+ this.uniforms.set( 'envCompensationDelta', 0.0 );
378
383
  this.uniforms.set( 'useEnvMapIS', 0 );
379
384
 
380
385
  }
@@ -537,11 +542,23 @@ export class EnvironmentManager {
537
542
 
538
543
  this.proceduralSkyRenderer = null;
539
544
  this.simpleSkyRenderer = null;
545
+
546
+ this.envCDFStorageAttr?.dispose?.();
540
547
  this.envCDFStorageAttr = null;
541
548
  this.envCDFStorageNode = null;
549
+
550
+ // Dispose the HDRI environment texture unless it's the shared placeholder
551
+ // (the placeholder is handled separately just below).
552
+ if ( this.environmentTexture && this.environmentTexture !== this._envPlaceholder ) {
553
+
554
+ this.environmentTexture.dispose?.();
555
+
556
+ }
557
+
542
558
  this._envPlaceholder?.dispose();
543
559
  this._envPlaceholder = null;
544
560
  this.environmentTexture = null;
561
+ this._previousHDRI = null;
545
562
 
546
563
  }
547
564
 
@@ -437,6 +437,15 @@ export class TransformManager {
437
437
  this._normalCache = null;
438
438
  this._baselineComputed = false;
439
439
 
440
+ // Drop back-references to the owning app and shared resources so the
441
+ // PathTracerApp graph can be GC'd. Without this, _app pinned the entire
442
+ // engine (verified via heap snapshot retainer chain).
443
+ this._app = null;
444
+ this._orbitControls = null;
445
+ this._camera = null;
446
+ this._controls = null;
447
+ this._gizmoScene = null;
448
+
440
449
  }
441
450
 
442
451
  }
@@ -182,6 +182,7 @@ export class UniformManager {
182
182
  u( 'environmentMatrix', new Matrix4(), 'mat4' );
183
183
  ub( 'useEnvMapIS', DEFAULT_STATE.useImportanceSampledEnvironment );
184
184
  u( 'envTotalSum', 0.0, 'float' );
185
+ u( 'envCompensationDelta', 0.0, 'float' );
185
186
  u( 'envResolution', new Vector2( 1, 1 ), 'vec2' );
186
187
 
187
188
  // Sun parameters
@@ -1,2 +0,0 @@
1
- (function(){function e(e,t,n,r){let i=n,a=n+r-1;for(;i<a;){let n=i+a>>1;e[n]<t?i=n+1:a=n}return i-n}function t(t,n,r){let i=new Float32Array(n*r),a=new Float32Array(r),o=0,s=0;for(let e=0;e<r;e++){let r=0;for(let a=0;a<n;a++){let s=e*n+a,c=t[4*s],l=t[4*s+1],u=t[4*s+2],d=.2126*c+.7152*l+.0722*u;r+=d,o+=d,i[s]=r}if(r!==0)for(let t=e*n,a=e*n+n;t<a;t++)i[t]/=r;s+=r,a[e]=s}if(s!==0)for(let e=0,t=a.length;e<t;e++)a[e]/=s;let c=new Float32Array(r);for(let t=0;t<r;t++)c[t]=(e(a,(t+1)/r,0,r)+.5)/r;let l=new Float32Array(n*r);for(let t=0;t<r;t++)for(let r=0;r<n;r++){let a=t*n+r;l[a]=(e(i,(r+1)/n,t*n,n)+.5)/n}return{marginalData:c,conditionalData:l,totalSum:o}}self.onmessage=function(e){let{floatData:n,width:r,height:i}=e.data;try{let e=t(n,r,i);self.postMessage({marginalData:e.marginalData,conditionalData:e.conditionalData,totalSum:e.totalSum,width:r,height:i},[e.marginalData.buffer,e.conditionalData.buffer])}catch(e){self.postMessage({error:e.message})}}})();
2
- //# sourceMappingURL=CDFWorker-2MoynL4F.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CDFWorker-2MoynL4F.js","names":[],"sources":["../../src/Processor/Workers/CDFWorker.js"],"sourcesContent":["/**\n * Web Worker for computing environment map CDF (Cumulative Distribution Function)\n * for importance sampling. Pure math — no Three.js dependencies.\n *\n * Input: { floatData: Float32Array, width, height }\n * Output: { marginalData: Float32Array, conditionalData: Float32Array, totalSum, width, height }\n */\n\nfunction binarySearchFindClosestIndexOf( array, targetValue, offset, count ) {\n\n\tlet lower = offset;\n\tlet upper = offset + count - 1;\n\n\twhile ( lower < upper ) {\n\n\t\tconst mid = ( lower + upper ) >> 1;\n\n\t\tif ( array[ mid ] < targetValue ) {\n\n\t\t\tlower = mid + 1;\n\n\t\t} else {\n\n\t\t\tupper = mid;\n\n\t\t}\n\n\t}\n\n\treturn lower - offset;\n\n}\n\nfunction buildCDF( floatData, width, height ) {\n\n\tconst cdfConditional = new Float32Array( width * height );\n\tconst cdfMarginal = new Float32Array( height );\n\n\tlet totalSumValue = 0.0;\n\tlet cumulativeWeightMarginal = 0.0;\n\n\t// Build conditional CDFs (per-row distribution)\n\tfor ( let y = 0; y < height; y ++ ) {\n\n\t\tlet cumulativeRowWeight = 0.0;\n\t\tfor ( let x = 0; x < width; x ++ ) {\n\n\t\t\tconst i = y * width + x;\n\t\t\tconst r = floatData[ 4 * i ];\n\t\t\tconst g = floatData[ 4 * i + 1 ];\n\t\t\tconst b = floatData[ 4 * i + 2 ];\n\n\t\t\t// Luminance (Rec. 709)\n\t\t\tconst weight = 0.2126 * r + 0.7152 * g + 0.0722 * b;\n\t\t\tcumulativeRowWeight += weight;\n\t\t\ttotalSumValue += weight;\n\n\t\t\tcdfConditional[ i ] = cumulativeRowWeight;\n\n\t\t}\n\n\t\t// Normalize row CDF to [0, 1]\n\t\tif ( cumulativeRowWeight !== 0 ) {\n\n\t\t\tfor ( let i = y * width, l = y * width + width; i < l; i ++ ) {\n\n\t\t\t\tcdfConditional[ i ] /= cumulativeRowWeight;\n\n\t\t\t}\n\n\t\t}\n\n\t\tcumulativeWeightMarginal += cumulativeRowWeight;\n\t\tcdfMarginal[ y ] = cumulativeWeightMarginal;\n\n\t}\n\n\t// Normalize marginal CDF to [0, 1]\n\tif ( cumulativeWeightMarginal !== 0 ) {\n\n\t\tfor ( let i = 0, l = cdfMarginal.length; i < l; i ++ ) {\n\n\t\t\tcdfMarginal[ i ] /= cumulativeWeightMarginal;\n\n\t\t}\n\n\t}\n\n\t// Invert marginal CDF\n\tconst marginalData = new Float32Array( height );\n\tfor ( let i = 0; i < height; i ++ ) {\n\n\t\tconst dist = ( i + 1 ) / height;\n\t\tconst row = binarySearchFindClosestIndexOf( cdfMarginal, dist, 0, height );\n\t\tmarginalData[ i ] = ( row + 0.5 ) / height;\n\n\t}\n\n\t// Invert conditional CDFs\n\tconst conditionalData = new Float32Array( width * height );\n\tfor ( let y = 0; y < height; y ++ ) {\n\n\t\tfor ( let x = 0; x < width; x ++ ) {\n\n\t\t\tconst i = y * width + x;\n\t\t\tconst dist = ( x + 1 ) / width;\n\t\t\tconst col = binarySearchFindClosestIndexOf( cdfConditional, dist, y * width, width );\n\t\t\tconditionalData[ i ] = ( col + 0.5 ) / width;\n\n\t\t}\n\n\t}\n\n\treturn { marginalData, conditionalData, totalSum: totalSumValue };\n\n}\n\nself.onmessage = function ( e ) {\n\n\tconst { floatData, width, height } = e.data;\n\n\ttry {\n\n\t\tconst result = buildCDF( floatData, width, height );\n\n\t\t// Transfer arrays back zero-copy\n\t\tself.postMessage(\n\t\t\t{\n\t\t\t\tmarginalData: result.marginalData,\n\t\t\t\tconditionalData: result.conditionalData,\n\t\t\t\ttotalSum: result.totalSum,\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t},\n\t\t\t[ result.marginalData.buffer, result.conditionalData.buffer ]\n\t\t);\n\n\t} catch ( error ) {\n\n\t\tself.postMessage( { error: error.message } );\n\n\t}\n\n};\n"],"mappings":"YAQA,SAAS,EAAgC,EAAO,EAAa,EAAQ,EAAQ,CAE5E,IAAI,EAAQ,EACR,EAAQ,EAAS,EAAQ,EAE7B,KAAQ,EAAQ,GAAQ,CAEvB,IAAM,EAAQ,EAAQ,GAAW,EAE5B,EAAO,GAAQ,EAEnB,EAAQ,EAAM,EAId,EAAQ,EAMV,OAAO,EAAQ,EAIhB,SAAS,EAAU,EAAW,EAAO,EAAS,CAE7C,IAAM,EAAiB,IAAI,aAAc,EAAQ,EAAQ,CACnD,EAAc,IAAI,aAAc,EAAQ,CAE1C,EAAgB,EAChB,EAA2B,EAG/B,IAAM,IAAI,EAAI,EAAG,EAAI,EAAQ,IAAO,CAEnC,IAAI,EAAsB,EAC1B,IAAM,IAAI,EAAI,EAAG,EAAI,EAAO,IAAO,CAElC,IAAM,EAAI,EAAI,EAAQ,EAChB,EAAI,EAAW,EAAI,GACnB,EAAI,EAAW,EAAI,EAAI,GACvB,EAAI,EAAW,EAAI,EAAI,GAGvB,EAAS,MAAS,EAAI,MAAS,EAAI,MAAS,EAClD,GAAuB,EACvB,GAAiB,EAEjB,EAAgB,GAAM,EAKvB,GAAK,IAAwB,EAE5B,IAAM,IAAI,EAAI,EAAI,EAAO,EAAI,EAAI,EAAQ,EAAO,EAAI,EAAG,IAEtD,EAAgB,IAAO,EAMzB,GAA4B,EAC5B,EAAa,GAAM,EAKpB,GAAK,IAA6B,EAEjC,IAAM,IAAI,EAAI,EAAG,EAAI,EAAY,OAAQ,EAAI,EAAG,IAE/C,EAAa,IAAO,EAOtB,IAAM,EAAe,IAAI,aAAc,EAAQ,CAC/C,IAAM,IAAI,EAAI,EAAG,EAAI,EAAQ,IAI5B,EAAc,IADF,EAAgC,GAD7B,EAAI,GAAM,EACsC,EAAG,EAAQ,CAC9C,IAAQ,EAKrC,IAAM,EAAkB,IAAI,aAAc,EAAQ,EAAQ,CAC1D,IAAM,IAAI,EAAI,EAAG,EAAI,EAAQ,IAE5B,IAAM,IAAI,EAAI,EAAG,EAAI,EAAO,IAAO,CAElC,IAAM,EAAI,EAAI,EAAQ,EAGtB,EAAiB,IADL,EAAgC,GAD7B,EAAI,GAAM,EACyC,EAAI,EAAO,EAAO,CACrD,IAAQ,EAMzC,MAAO,CAAE,eAAc,kBAAiB,SAAU,EAAe,CAIlE,KAAK,UAAY,SAAW,EAAI,CAE/B,GAAM,CAAE,YAAW,QAAO,UAAW,EAAE,KAEvC,GAAI,CAEH,IAAM,EAAS,EAAU,EAAW,EAAO,EAAQ,CAGnD,KAAK,YACJ,CACC,aAAc,EAAO,aACrB,gBAAiB,EAAO,gBACxB,SAAU,EAAO,SACjB,QACA,SACA,CACD,CAAE,EAAO,aAAa,OAAQ,EAAO,gBAAgB,OAAQ,CAC7D,OAEQ,EAAQ,CAEjB,KAAK,YAAa,CAAE,MAAO,EAAM,QAAS,CAAE"}