rayzee 5.6.1 → 5.7.1

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/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 );
@@ -41,7 +41,7 @@ export class DenoisingManager extends EventDispatcher {
41
41
  this.pipeline = pipeline;
42
42
 
43
43
  // Stage references — only used internally for orchestration
44
- this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, adaptiveSampling, edgeFilter, ssrc, autoExposure, display }
44
+ this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, adaptiveSampling, edgeFilter, ssrc, autoExposure, compositor }
45
45
 
46
46
  this._getExposure = getExposure;
47
47
  this._getSaturation = getSaturation;
@@ -295,9 +295,8 @@ export class DenoisingManager extends EventDispatcher {
295
295
  }
296
296
 
297
297
  /**
298
- * Enables/disables auto-exposure with proper exposure stacking management.
299
298
  * @param {boolean} enabled
300
- * @param {number} manualExposure - The manual exposure value to restore when disabling
299
+ * @param {number} manualExposure - Restored to renderer.toneMappingExposure when disabling.
301
300
  */
302
301
  setAutoExposureEnabled( enabled, manualExposure ) {
303
302
 
@@ -306,19 +305,10 @@ export class DenoisingManager extends EventDispatcher {
306
305
 
307
306
  s.autoExposure.enabled = enabled;
308
307
 
309
- if ( enabled ) {
310
-
311
- // Neutralize Display manual exposure to avoid stacking
312
- s.display?.setExposure( 1.0 );
313
-
314
- } else {
308
+ // AutoExposure overwrites renderer.toneMappingExposure each frame; restore manual on disable.
309
+ if ( ! enabled && this.renderer ) {
315
310
 
316
- s.display?.setExposure( manualExposure );
317
- if ( s.display && this.renderer ) {
318
-
319
- this.renderer.toneMappingExposure = 1.0;
320
-
321
- }
311
+ this.renderer.toneMappingExposure = manualExposure;
322
312
 
323
313
  }
324
314
 
@@ -417,10 +407,10 @@ export class DenoisingManager extends EventDispatcher {
417
407
 
418
408
  } else {
419
409
 
420
- // Re-render display stage so WebGPU canvas has valid content
421
- if ( this.upscaler?.enabled && this._stages.display && context ) {
410
+ // Re-render compositor stage so WebGPU canvas has valid content
411
+ if ( this.upscaler?.enabled && this._stages.compositor && context ) {
422
412
 
423
- this._stages.display.render( context );
413
+ this._stages.compositor.render( context );
424
414
 
425
415
  }
426
416
 
@@ -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"}
@@ -1,120 +0,0 @@
1
- import { vec4, vec3, uv, uniform, select, dot, mix } from 'three/tsl';
2
- import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
3
- import { NoBlending } from 'three';
4
- import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
5
- import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
6
-
7
- /**
8
- * Display — Terminal pipeline stage for WebGPU.
9
- *
10
- * Reads the final colour texture from the pipeline context (using a
11
- * priority fallback chain), applies exposure, and renders to screen.
12
- *
13
- * When new post-processing stages are added between PathTracer and
14
- * Display, the fallback chain automatically picks up the latest output
15
- * without any wiring changes.
16
- */
17
- export class Display extends RenderStage {
18
-
19
- constructor( renderer, options = {} ) {
20
-
21
- super( 'Display', {
22
- ...options,
23
- executionMode: StageExecutionMode.ALWAYS
24
- } );
25
-
26
- this.renderer = renderer;
27
-
28
- // Exposure uniform — linear multiplier (consistent with auto-exposure)
29
- this.exposure = uniform( options.exposure ?? 1.0 );
30
-
31
- // Pre-tonemapping saturation — compensates for ACES/AgX desaturation (1.0 = neutral)
32
- this.saturation = uniform( options.saturation ?? 1.0 );
33
-
34
- // Transparent background toggle
35
- this._transparentBackground = uniform( 0, 'int' );
36
-
37
- // Updatable texture node — swap .value each frame, no shader recompile
38
- this._displayTexNode = new TextureNode();
39
-
40
- const texSample = this._displayTexNode.sample( uv() );
41
-
42
- // Build material once (TSL compiles on first render)
43
- const exposed = texSample.xyz.mul( this.exposure );
44
-
45
- // Saturation adjustment (before tonemapping): mix between luminance and color
46
- const luma = dot( exposed, REC709_LUMINANCE_COEFFICIENTS );
47
- let displayShader = mix( vec3( luma ), exposed, this.saturation );
48
-
49
- // Alpha: pass through source alpha when transparent, otherwise 1.0
50
- const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
51
-
52
- this.displayMaterial = new MeshBasicNodeMaterial();
53
- this.displayMaterial.colorNode = vec4( displayShader, outputAlpha );
54
- this.displayMaterial.blending = NoBlending;
55
- this.displayMaterial.toneMapped = true;
56
-
57
- this.displayQuad = new QuadMesh( this.displayMaterial );
58
-
59
- }
60
-
61
- /**
62
- * Resolve the best available output texture from the pipeline context.
63
- * Later stages in the chain take priority; pathtracer:color is the
64
- * baseline fallback that is always present.
65
- */
66
- _resolveDisplayTexture( context ) {
67
-
68
- return context.getTexture( 'bloom:output' )
69
- || context.getTexture( 'edgeFiltering:output' )
70
- || context.getTexture( 'asvgf:output' )
71
- || context.getTexture( 'ssrc:output' )
72
- || context.getTexture( 'pathtracer:color' );
73
-
74
- }
75
-
76
- render( context ) {
77
-
78
- if ( ! this.enabled ) return;
79
-
80
- const displayTexture = this._resolveDisplayTexture( context );
81
-
82
- if ( ! displayTexture ) return;
83
-
84
- // Swap texture reference (no shader recompilation)
85
- this._displayTexNode.value = displayTexture;
86
-
87
- // Render to screen
88
- this.renderer.setRenderTarget( null );
89
- this.displayQuad.render( this.renderer );
90
-
91
- }
92
-
93
- setExposure( value ) {
94
-
95
- this.exposure.value = value;
96
-
97
- }
98
-
99
- setSaturation( value ) {
100
-
101
- this.saturation.value = value;
102
-
103
- }
104
-
105
- setTransparentBackground( enabled ) {
106
-
107
- this._transparentBackground.value = enabled ? 1 : 0;
108
-
109
- }
110
-
111
- dispose() {
112
-
113
- this._displayTexNode?.dispose();
114
- this.displayMaterial?.dispose();
115
- // QuadMesh extends Mesh — no dispose method; material already disposed.
116
- this.displayQuad = null;
117
-
118
- }
119
-
120
- }