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/README.md +3 -0
- package/dist/assets/CDFWorker-BFQUr3By.js +2 -0
- package/dist/assets/CDFWorker-BFQUr3By.js.map +1 -0
- package/dist/rayzee.es.js +2374 -2347
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +49 -43
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineEvents.js +3 -0
- package/src/PathTracerApp.js +32 -11
- package/src/Processor/EquirectHDRInfo.js +76 -16
- package/src/Processor/ShaderBuilder.js +1 -0
- package/src/Processor/Workers/CDFWorker.js +72 -11
- package/src/RenderSettings.js +10 -8
- package/src/Stages/Compositor.js +101 -0
- package/src/Stages/NormalDepth.js +37 -20
- package/src/TSL/Common.js +9 -0
- package/src/TSL/Environment.js +15 -6
- package/src/TSL/LightsIndirect.js +1 -1
- package/src/TSL/LightsSampling.js +5 -4
- package/src/TSL/PathTracer.js +2 -2
- package/src/TSL/PathTracerCore.js +6 -5
- package/src/managers/DenoisingManager.js +8 -18
- package/src/managers/EnvironmentManager.js +17 -0
- package/src/managers/TransformManager.js +9 -0
- package/src/managers/UniformManager.js +1 -0
- package/dist/assets/CDFWorker-2MoynL4F.js +0 -2
- package/dist/assets/CDFWorker-2MoynL4F.js.map +0 -1
- package/src/Stages/Display.js +0 -120
package/package.json
CHANGED
package/src/EngineEvents.js
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebGPURenderer, RectAreaLightNode } from 'three/webgpu';
|
|
1
|
+
import { WebGPURenderer, RectAreaLightNode, LinearSRGBColorSpace, SRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
4
|
ACESFilmicToneMapping, Scene, EventDispatcher, TimestampQuery
|
|
@@ -16,7 +16,7 @@ import { AdaptiveSampling } from './Stages/AdaptiveSampling.js';
|
|
|
16
16
|
import { EdgeFilter } from './Stages/EdgeFilter.js';
|
|
17
17
|
import { AutoExposure } from './Stages/AutoExposure.js';
|
|
18
18
|
import { SSRC } from './Stages/SSRC.js';
|
|
19
|
-
import {
|
|
19
|
+
import { Compositor } from './Stages/Compositor.js';
|
|
20
20
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
21
21
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
22
22
|
import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
|
|
@@ -36,6 +36,10 @@ import { OverlayManager } from './managers/OverlayManager.js';
|
|
|
36
36
|
import { AnimationManager } from './managers/AnimationManager.js';
|
|
37
37
|
import { TransformManager } from './managers/TransformManager.js';
|
|
38
38
|
|
|
39
|
+
// One app per canvas — auto-dispose a prior owner if the caller double-
|
|
40
|
+
// instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
|
|
41
|
+
const _appsByCanvas = new WeakMap();
|
|
42
|
+
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* WebGPU Path Tracer Application.
|
|
@@ -67,6 +71,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
67
71
|
|
|
68
72
|
super();
|
|
69
73
|
|
|
74
|
+
try {
|
|
75
|
+
|
|
76
|
+
_appsByCanvas.get( canvas )?.dispose();
|
|
77
|
+
|
|
78
|
+
} catch ( err ) {
|
|
79
|
+
|
|
80
|
+
console.warn( 'PathTracerApp: prior canvas owner dispose failed', err );
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_appsByCanvas.set( canvas, this );
|
|
85
|
+
|
|
70
86
|
this.canvas = canvas;
|
|
71
87
|
this._autoResize = options.autoResize !== false;
|
|
72
88
|
this._showStats = options.showStats !== false;
|
|
@@ -270,7 +286,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
270
286
|
if ( this._needsDisplayRefresh ) {
|
|
271
287
|
|
|
272
288
|
this._needsDisplayRefresh = false;
|
|
273
|
-
this.stages.
|
|
289
|
+
this.stages.compositor.render( this.pipeline.context );
|
|
274
290
|
this._renderHelperOverlay();
|
|
275
291
|
|
|
276
292
|
}
|
|
@@ -399,6 +415,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
399
415
|
if ( this._disposed ) return;
|
|
400
416
|
this._disposed = true;
|
|
401
417
|
|
|
418
|
+
this.dispatchEvent( { type: EngineEvents.DISPOSE } );
|
|
402
419
|
this.stopAnimation();
|
|
403
420
|
clearTimeout( this._resizeDebounceTimer );
|
|
404
421
|
this._resizeDebounceTimer = null;
|
|
@@ -758,7 +775,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
758
775
|
// Apply all settings to stages in one shot
|
|
759
776
|
timer.start( 'Apply settings' );
|
|
760
777
|
this.settings.applyAll();
|
|
761
|
-
this.stages.
|
|
778
|
+
this.stages.compositor.setTransparentBackground( this.settings.get( 'transparentBackground' ) );
|
|
762
779
|
timer.end( 'Apply settings' );
|
|
763
780
|
|
|
764
781
|
timer.print();
|
|
@@ -989,10 +1006,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
989
1006
|
|
|
990
1007
|
if ( usePostProcess ) return dm.denoiserCanvas;
|
|
991
1008
|
|
|
992
|
-
// Re-render
|
|
993
|
-
if ( this.stages.
|
|
1009
|
+
// Re-render compositor stage so the WebGPU canvas has valid content
|
|
1010
|
+
if ( this.stages.compositor && this.pipeline?.context ) {
|
|
994
1011
|
|
|
995
|
-
this.stages.
|
|
1012
|
+
this.stages.compositor.render( this.pipeline.context );
|
|
996
1013
|
|
|
997
1014
|
}
|
|
998
1015
|
|
|
@@ -1191,6 +1208,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1191
1208
|
|
|
1192
1209
|
RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
|
|
1193
1210
|
|
|
1211
|
+
this.renderer.workingColorSpace = SRGBColorSpace;
|
|
1212
|
+
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
1194
1213
|
this.renderer.toneMapping = ACESFilmicToneMapping;
|
|
1195
1214
|
this.renderer.toneMappingExposure = 1.0;
|
|
1196
1215
|
this.renderer.setPixelRatio( 1.0 );
|
|
@@ -1244,7 +1263,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1244
1263
|
this.pipeline.addStage( this.stages.adaptiveSampling );
|
|
1245
1264
|
this.pipeline.addStage( this.stages.edgeFilter );
|
|
1246
1265
|
this.pipeline.addStage( this.stages.autoExposure );
|
|
1247
|
-
this.pipeline.addStage( this.stages.
|
|
1266
|
+
this.pipeline.addStage( this.stages.compositor );
|
|
1248
1267
|
|
|
1249
1268
|
const initRenderW = this.canvas.clientWidth || 1;
|
|
1250
1269
|
const initRenderH = this.canvas.clientHeight || 1;
|
|
@@ -1350,10 +1369,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1350
1369
|
// Bind settings to pipeline stages
|
|
1351
1370
|
this.settings.bind( {
|
|
1352
1371
|
stages: this.stages,
|
|
1372
|
+
renderer: this.renderer,
|
|
1353
1373
|
resetCallback: () => this.reset(),
|
|
1354
1374
|
reconcileCompletion: () => this._reconcileCompletion(),
|
|
1355
1375
|
} );
|
|
1356
1376
|
|
|
1377
|
+
this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
|
|
1378
|
+
|
|
1357
1379
|
// Resize handling
|
|
1358
1380
|
this.onResize();
|
|
1359
1381
|
this.resizeHandler = () => this.onResize();
|
|
@@ -1457,8 +1479,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1457
1479
|
this.stages.edgeFilter = new EdgeFilter( this.renderer, { enabled: false } );
|
|
1458
1480
|
this.stages.autoExposure = new AutoExposure( this.renderer, { enabled: DEFAULT_STATE.autoExposure ?? false } );
|
|
1459
1481
|
|
|
1460
|
-
this.stages.
|
|
1461
|
-
exposure: ( DEFAULT_STATE.autoExposure ) ? 1.0 : ( this.settings.get( 'exposure' ) ?? 1.0 ),
|
|
1482
|
+
this.stages.compositor = new Compositor( this.renderer, {
|
|
1462
1483
|
saturation: this.settings.get( 'saturation' ) ?? DEFAULT_STATE.saturation,
|
|
1463
1484
|
} );
|
|
1464
1485
|
|
|
@@ -1480,7 +1501,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1480
1501
|
edgeFilter: this.stages.edgeFilter,
|
|
1481
1502
|
ssrc: this.stages.ssrc,
|
|
1482
1503
|
autoExposure: this.stages.autoExposure,
|
|
1483
|
-
|
|
1504
|
+
compositor: this.stages.compositor,
|
|
1484
1505
|
},
|
|
1485
1506
|
pipeline: this.pipeline,
|
|
1486
1507
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
@@ -172,6 +172,7 @@ export class EquirectHDRInfo {
|
|
|
172
172
|
this.marginalData = new Float32Array( [ 0, 1 ] );
|
|
173
173
|
this.conditionalData = new Float32Array( [ 0, 0, 1, 1 ] );
|
|
174
174
|
this.totalSum = 0;
|
|
175
|
+
this.compensationDelta = 0;
|
|
175
176
|
this.width = 0;
|
|
176
177
|
this.height = 0;
|
|
177
178
|
|
|
@@ -205,6 +206,7 @@ export class EquirectHDRInfo {
|
|
|
205
206
|
this.marginalData = result.marginalData;
|
|
206
207
|
this.conditionalData = result.conditionalData;
|
|
207
208
|
this.totalSum = result.totalSum;
|
|
209
|
+
this.compensationDelta = result.compensationDelta;
|
|
208
210
|
this.width = width;
|
|
209
211
|
this.height = height;
|
|
210
212
|
|
|
@@ -263,6 +265,7 @@ export class EquirectHDRInfo {
|
|
|
263
265
|
this.marginalData = result.marginalData;
|
|
264
266
|
this.conditionalData = result.conditionalData;
|
|
265
267
|
this.totalSum = result.totalSum;
|
|
268
|
+
this.compensationDelta = result.compensationDelta;
|
|
266
269
|
this.width = result.width;
|
|
267
270
|
this.height = result.height;
|
|
268
271
|
|
|
@@ -285,28 +288,87 @@ export class EquirectHDRInfo {
|
|
|
285
288
|
*/
|
|
286
289
|
static computeCDF( floatData, width, height ) {
|
|
287
290
|
|
|
288
|
-
const
|
|
291
|
+
const numPixels = width * height;
|
|
292
|
+
|
|
293
|
+
// Pass 1: compute per-pixel luminance weighted by sin(theta) and raw total sum.
|
|
294
|
+
// sin(theta) compensates for the equirectangular projection: pixels near the poles
|
|
295
|
+
// cover less solid angle, so weighting by sin(theta) makes the CDF proportional to
|
|
296
|
+
// luminance per solid angle rather than luminance per pixel.
|
|
297
|
+
const pixelWeights = new Float32Array( numPixels );
|
|
298
|
+
let rawTotalSum = 0.0;
|
|
299
|
+
|
|
300
|
+
for ( let y = 0; y < height; y ++ ) {
|
|
301
|
+
|
|
302
|
+
const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
|
|
303
|
+
|
|
304
|
+
for ( let x = 0; x < width; x ++ ) {
|
|
305
|
+
|
|
306
|
+
const i = y * width + x;
|
|
307
|
+
const w = colorToLuminance(
|
|
308
|
+
floatData[ 4 * i ],
|
|
309
|
+
floatData[ 4 * i + 1 ],
|
|
310
|
+
floatData[ 4 * i + 2 ],
|
|
311
|
+
) * sinTheta;
|
|
312
|
+
pixelWeights[ i ] = w;
|
|
313
|
+
rawTotalSum += w;
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// MIS Compensation (Karlík et al. 2019, Eq. 14)
|
|
320
|
+
// With equal sample allocation (c_I = 0.5): delta = 2*(1 - 0.5)*meanWeight = meanWeight
|
|
321
|
+
// Subtracting mean sharpens the env map PDF, reducing oversampling
|
|
322
|
+
// of dim regions already well-covered by BSDF sampling.
|
|
323
|
+
const meanWeight = rawTotalSum / numPixels;
|
|
324
|
+
let compensatedTotalSum = 0.0;
|
|
325
|
+
|
|
326
|
+
for ( let i = 0; i < numPixels; i ++ ) {
|
|
327
|
+
|
|
328
|
+
pixelWeights[ i ] = Math.max( 0, pixelWeights[ i ] - meanWeight );
|
|
329
|
+
compensatedTotalSum += pixelWeights[ i ];
|
|
330
|
+
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fall back to raw weights if compensation zeroed everything (uniform env map)
|
|
334
|
+
const useCompensation = compensatedTotalSum > 0;
|
|
335
|
+
const totalSumValue = useCompensation ? compensatedTotalSum : rawTotalSum;
|
|
336
|
+
const compensationDelta = useCompensation ? meanWeight : 0;
|
|
337
|
+
|
|
338
|
+
if ( ! useCompensation ) {
|
|
339
|
+
|
|
340
|
+
for ( let y = 0; y < height; y ++ ) {
|
|
341
|
+
|
|
342
|
+
const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
|
|
343
|
+
|
|
344
|
+
for ( let x = 0; x < width; x ++ ) {
|
|
345
|
+
|
|
346
|
+
const i = y * width + x;
|
|
347
|
+
pixelWeights[ i ] = colorToLuminance(
|
|
348
|
+
floatData[ 4 * i ],
|
|
349
|
+
floatData[ 4 * i + 1 ],
|
|
350
|
+
floatData[ 4 * i + 2 ],
|
|
351
|
+
) * sinTheta;
|
|
352
|
+
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Pass 2: build conditional and marginal CDFs from (compensated) weights
|
|
360
|
+
const cdfConditional = new Float32Array( numPixels );
|
|
289
361
|
const cdfMarginal = new Float32Array( height );
|
|
290
362
|
|
|
291
|
-
let totalSumValue = 0.0;
|
|
292
363
|
let cumulativeWeightMarginal = 0.0;
|
|
293
364
|
|
|
294
|
-
// Build conditional CDFs (per-row distribution)
|
|
295
365
|
for ( let y = 0; y < height; y ++ ) {
|
|
296
366
|
|
|
297
367
|
let cumulativeRowWeight = 0.0;
|
|
298
368
|
for ( let x = 0; x < width; x ++ ) {
|
|
299
369
|
|
|
300
370
|
const i = y * width + x;
|
|
301
|
-
|
|
302
|
-
const g = floatData[ 4 * i + 1 ];
|
|
303
|
-
const b = floatData[ 4 * i + 2 ];
|
|
304
|
-
|
|
305
|
-
// Weight by luminance
|
|
306
|
-
const weight = colorToLuminance( r, g, b );
|
|
307
|
-
cumulativeRowWeight += weight;
|
|
308
|
-
totalSumValue += weight;
|
|
309
|
-
|
|
371
|
+
cumulativeRowWeight += pixelWeights[ i ];
|
|
310
372
|
cdfConditional[ i ] = cumulativeRowWeight;
|
|
311
373
|
|
|
312
374
|
}
|
|
@@ -323,8 +385,6 @@ export class EquirectHDRInfo {
|
|
|
323
385
|
}
|
|
324
386
|
|
|
325
387
|
cumulativeWeightMarginal += cumulativeRowWeight;
|
|
326
|
-
|
|
327
|
-
// Build marginal CDF (row distribution)
|
|
328
388
|
cdfMarginal[ y ] = cumulativeWeightMarginal;
|
|
329
389
|
|
|
330
390
|
}
|
|
@@ -342,7 +402,7 @@ export class EquirectHDRInfo {
|
|
|
342
402
|
|
|
343
403
|
// Create inverted CDF arrays (Float32 directly for storage buffers)
|
|
344
404
|
const marginalData = new Float32Array( height );
|
|
345
|
-
const conditionalData = new Float32Array(
|
|
405
|
+
const conditionalData = new Float32Array( numPixels );
|
|
346
406
|
|
|
347
407
|
// Invert marginal CDF
|
|
348
408
|
for ( let i = 0; i < height; i ++ ) {
|
|
@@ -369,7 +429,7 @@ export class EquirectHDRInfo {
|
|
|
369
429
|
|
|
370
430
|
}
|
|
371
431
|
|
|
372
|
-
return { marginalData, conditionalData, totalSum: totalSumValue };
|
|
432
|
+
return { marginalData, conditionalData, totalSum: totalSumValue, compensationDelta };
|
|
373
433
|
|
|
374
434
|
}
|
|
375
435
|
|
|
@@ -365,6 +365,7 @@ export class ShaderBuilder {
|
|
|
365
365
|
envMatrix: stage.environmentMatrix,
|
|
366
366
|
envCDFBuffer: envCDFStorage,
|
|
367
367
|
envTotalSum: stage.envTotalSum,
|
|
368
|
+
envCompensationDelta: stage.envCompensationDelta,
|
|
368
369
|
envResolution: stage.envResolution,
|
|
369
370
|
enableEnvironmentLight: stage.enableEnvironment,
|
|
370
371
|
useEnvMapIS: stage.useEnvMapIS,
|
|
@@ -33,16 +33,19 @@ function binarySearchFindClosestIndexOf( array, targetValue, offset, count ) {
|
|
|
33
33
|
|
|
34
34
|
function buildCDF( floatData, width, height ) {
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
const cdfMarginal = new Float32Array( height );
|
|
36
|
+
const numPixels = width * height;
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// Pass 1: compute per-pixel luminance weighted by sin(theta) and raw total sum.
|
|
39
|
+
// sin(theta) compensates for the equirectangular projection: pixels near the poles
|
|
40
|
+
// cover less solid angle, so weighting by sin(theta) makes the CDF proportional to
|
|
41
|
+
// luminance per solid angle rather than luminance per pixel.
|
|
42
|
+
const pixelWeights = new Float32Array( numPixels );
|
|
43
|
+
let rawTotalSum = 0.0;
|
|
41
44
|
|
|
42
|
-
// Build conditional CDFs (per-row distribution)
|
|
43
45
|
for ( let y = 0; y < height; y ++ ) {
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
|
|
48
|
+
|
|
46
49
|
for ( let x = 0; x < width; x ++ ) {
|
|
47
50
|
|
|
48
51
|
const i = y * width + x;
|
|
@@ -50,11 +53,68 @@ function buildCDF( floatData, width, height ) {
|
|
|
50
53
|
const g = floatData[ 4 * i + 1 ];
|
|
51
54
|
const b = floatData[ 4 * i + 2 ];
|
|
52
55
|
|
|
53
|
-
// Luminance (Rec. 709)
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// Luminance (Rec. 709) weighted by solid angle factor
|
|
57
|
+
const w = ( 0.2126 * r + 0.7152 * g + 0.0722 * b ) * sinTheta;
|
|
58
|
+
pixelWeights[ i ] = w;
|
|
59
|
+
rawTotalSum += w;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MIS Compensation (Karlík et al. 2019, Eq. 14)
|
|
66
|
+
// With equal sample allocation (c_I = 0.5): delta = 2*(1 - 0.5)*meanWeight = meanWeight
|
|
67
|
+
// Subtracting mean sharpens the env map PDF, reducing oversampling
|
|
68
|
+
// of dim regions already well-covered by BSDF sampling.
|
|
69
|
+
const meanWeight = rawTotalSum / numPixels;
|
|
70
|
+
let compensatedTotalSum = 0.0;
|
|
71
|
+
|
|
72
|
+
for ( let i = 0; i < numPixels; i ++ ) {
|
|
73
|
+
|
|
74
|
+
pixelWeights[ i ] = Math.max( 0, pixelWeights[ i ] - meanWeight );
|
|
75
|
+
compensatedTotalSum += pixelWeights[ i ];
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fall back to raw weights if compensation zeroed everything (uniform env map)
|
|
80
|
+
const useCompensation = compensatedTotalSum > 0;
|
|
81
|
+
const totalSumValue = useCompensation ? compensatedTotalSum : rawTotalSum;
|
|
82
|
+
const compensationDelta = useCompensation ? meanWeight : 0;
|
|
83
|
+
|
|
84
|
+
if ( ! useCompensation ) {
|
|
85
|
+
|
|
86
|
+
// Restore raw sin-weighted luminance
|
|
87
|
+
for ( let y = 0; y < height; y ++ ) {
|
|
88
|
+
|
|
89
|
+
const sinTheta = Math.sin( Math.PI * ( y + 0.5 ) / height );
|
|
90
|
+
|
|
91
|
+
for ( let x = 0; x < width; x ++ ) {
|
|
57
92
|
|
|
93
|
+
const i = y * width + x;
|
|
94
|
+
const r = floatData[ 4 * i ];
|
|
95
|
+
const g = floatData[ 4 * i + 1 ];
|
|
96
|
+
const b = floatData[ 4 * i + 2 ];
|
|
97
|
+
pixelWeights[ i ] = ( 0.2126 * r + 0.7152 * g + 0.0722 * b ) * sinTheta;
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Pass 2: build conditional and marginal CDFs from (compensated) weights
|
|
106
|
+
const cdfConditional = new Float32Array( numPixels );
|
|
107
|
+
const cdfMarginal = new Float32Array( height );
|
|
108
|
+
|
|
109
|
+
let cumulativeWeightMarginal = 0.0;
|
|
110
|
+
|
|
111
|
+
for ( let y = 0; y < height; y ++ ) {
|
|
112
|
+
|
|
113
|
+
let cumulativeRowWeight = 0.0;
|
|
114
|
+
for ( let x = 0; x < width; x ++ ) {
|
|
115
|
+
|
|
116
|
+
const i = y * width + x;
|
|
117
|
+
cumulativeRowWeight += pixelWeights[ i ];
|
|
58
118
|
cdfConditional[ i ] = cumulativeRowWeight;
|
|
59
119
|
|
|
60
120
|
}
|
|
@@ -111,7 +171,7 @@ function buildCDF( floatData, width, height ) {
|
|
|
111
171
|
|
|
112
172
|
}
|
|
113
173
|
|
|
114
|
-
return { marginalData, conditionalData, totalSum: totalSumValue };
|
|
174
|
+
return { marginalData, conditionalData, totalSum: totalSumValue, compensationDelta };
|
|
115
175
|
|
|
116
176
|
}
|
|
117
177
|
|
|
@@ -129,6 +189,7 @@ self.onmessage = function ( e ) {
|
|
|
129
189
|
marginalData: result.marginalData,
|
|
130
190
|
conditionalData: result.conditionalData,
|
|
131
191
|
totalSum: result.totalSum,
|
|
192
|
+
compensationDelta: result.compensationDelta,
|
|
132
193
|
width,
|
|
133
194
|
height,
|
|
134
195
|
},
|
package/src/RenderSettings.js
CHANGED
|
@@ -104,16 +104,16 @@ export class RenderSettings extends EventDispatcher {
|
|
|
104
104
|
* Wires internal references. Called by PathTracerApp after init().
|
|
105
105
|
*
|
|
106
106
|
* @param {Object} params
|
|
107
|
-
* @param {Object} params.stages - Pipeline stages { pathTracer,
|
|
107
|
+
* @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
|
|
108
108
|
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
109
109
|
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
110
110
|
*/
|
|
111
|
-
bind( { stages, resetCallback, reconcileCompletion } ) {
|
|
111
|
+
bind( { stages, renderer, resetCallback, reconcileCompletion } ) {
|
|
112
112
|
|
|
113
113
|
this._pathTracer = stages.pathTracer;
|
|
114
114
|
this._resetCallback = resetCallback;
|
|
115
115
|
this._delegates = {};
|
|
116
|
-
this._handlers = this._buildHandlers( stages, reconcileCompletion );
|
|
116
|
+
this._handlers = this._buildHandlers( stages, renderer, reconcileCompletion );
|
|
117
117
|
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -121,22 +121,24 @@ export class RenderSettings extends EventDispatcher {
|
|
|
121
121
|
* Builds handler functions for multi-stage settings that can't
|
|
122
122
|
* be routed with a simple uniform forward.
|
|
123
123
|
*/
|
|
124
|
-
_buildHandlers( stages, reconcileCompletion ) {
|
|
124
|
+
_buildHandlers( stages, renderer, reconcileCompletion ) {
|
|
125
125
|
|
|
126
126
|
return {
|
|
127
127
|
|
|
128
128
|
handleTransparentBackground: ( value ) => {
|
|
129
129
|
|
|
130
130
|
stages.pathTracer?.setUniform( 'transparentBackground', value );
|
|
131
|
-
stages.
|
|
131
|
+
stages.compositor?.setTransparentBackground( value );
|
|
132
132
|
|
|
133
133
|
},
|
|
134
134
|
|
|
135
135
|
handleExposure: ( value ) => {
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// Three.js applies toneMappingExposure inside the tone-mapping branch,
|
|
138
|
+
// so this has no effect when renderer.toneMapping === NoToneMapping.
|
|
139
|
+
if ( ! stages.autoExposure?.enabled && renderer ) {
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
renderer.toneMappingExposure = value;
|
|
140
142
|
|
|
141
143
|
}
|
|
142
144
|
|
|
@@ -144,7 +146,7 @@ export class RenderSettings extends EventDispatcher {
|
|
|
144
146
|
|
|
145
147
|
handleSaturation: ( value ) => {
|
|
146
148
|
|
|
147
|
-
stages.
|
|
149
|
+
stages.compositor?.setSaturation( value );
|
|
148
150
|
|
|
149
151
|
},
|
|
150
152
|
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
* Compositor — Terminal pipeline stage.
|
|
9
|
+
*
|
|
10
|
+
* Selects the latest upstream texture via a priority fallback chain, applies
|
|
11
|
+
* a saturation grade, sets alpha, and hands the linear HDR result to the
|
|
12
|
+
* renderer's output pass (tone mapping + sRGB gamma happen there).
|
|
13
|
+
*
|
|
14
|
+
* Exposure is not applied here — `renderer.toneMappingExposure` owns it,
|
|
15
|
+
* and Three.js applies it inside the tone-mapping branch of the output pass
|
|
16
|
+
* (so it has no effect when `renderer.toneMapping === NoToneMapping`).
|
|
17
|
+
*/
|
|
18
|
+
export class Compositor extends RenderStage {
|
|
19
|
+
|
|
20
|
+
constructor( renderer, options = {} ) {
|
|
21
|
+
|
|
22
|
+
super( 'Compositor', {
|
|
23
|
+
...options,
|
|
24
|
+
executionMode: StageExecutionMode.ALWAYS
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
this.renderer = renderer;
|
|
28
|
+
|
|
29
|
+
// 1.0 = neutral; >1 boosts to compensate for ACES/AgX desaturation.
|
|
30
|
+
this.saturation = uniform( options.saturation ?? 1.0 );
|
|
31
|
+
|
|
32
|
+
this._transparentBackground = uniform( 0, 'int' );
|
|
33
|
+
|
|
34
|
+
// TextureNode reused across frames — only `.value` mutates, shader doesn't recompile.
|
|
35
|
+
this._sourceTexNode = new TextureNode();
|
|
36
|
+
|
|
37
|
+
const texSample = this._sourceTexNode.sample( uv() );
|
|
38
|
+
|
|
39
|
+
const luma = dot( texSample.xyz, REC709_LUMINANCE_COEFFICIENTS );
|
|
40
|
+
const gradedColor = mix( vec3( luma ), texSample.xyz, this.saturation );
|
|
41
|
+
|
|
42
|
+
const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
|
|
43
|
+
|
|
44
|
+
this.compositorMaterial = new MeshBasicNodeMaterial();
|
|
45
|
+
this.compositorMaterial.colorNode = vec4( gradedColor, outputAlpha );
|
|
46
|
+
this.compositorMaterial.blending = NoBlending;
|
|
47
|
+
|
|
48
|
+
this.compositorQuad = new QuadMesh( this.compositorMaterial );
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Later stages in the chain take priority; `pathtracer:color` is the
|
|
54
|
+
* baseline fallback that is always present.
|
|
55
|
+
*/
|
|
56
|
+
_resolveSourceTexture( context ) {
|
|
57
|
+
|
|
58
|
+
return context.getTexture( 'bloom:output' )
|
|
59
|
+
|| context.getTexture( 'edgeFiltering:output' )
|
|
60
|
+
|| context.getTexture( 'asvgf:output' )
|
|
61
|
+
|| context.getTexture( 'ssrc:output' )
|
|
62
|
+
|| context.getTexture( 'pathtracer:color' );
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render( context ) {
|
|
67
|
+
|
|
68
|
+
if ( ! this.enabled ) return;
|
|
69
|
+
|
|
70
|
+
const sourceTexture = this._resolveSourceTexture( context );
|
|
71
|
+
if ( ! sourceTexture ) return;
|
|
72
|
+
|
|
73
|
+
this._sourceTexNode.value = sourceTexture;
|
|
74
|
+
|
|
75
|
+
this.renderer.setRenderTarget( null );
|
|
76
|
+
this.compositorQuad.render( this.renderer );
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setSaturation( value ) {
|
|
81
|
+
|
|
82
|
+
this.saturation.value = value;
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setTransparentBackground( enabled ) {
|
|
87
|
+
|
|
88
|
+
this._transparentBackground.value = enabled ? 1 : 0;
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dispose() {
|
|
93
|
+
|
|
94
|
+
this._sourceTexNode?.dispose();
|
|
95
|
+
this.compositorMaterial?.dispose();
|
|
96
|
+
// QuadMesh extends Mesh — no dispose method; material already released above.
|
|
97
|
+
this.compositorQuad = null;
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
@@ -95,6 +95,14 @@ export class NormalDepth extends RenderStage {
|
|
|
95
95
|
this._bvhStorageNode = null;
|
|
96
96
|
this._matStorageNode = null;
|
|
97
97
|
|
|
98
|
+
// Last-seen attribute identities. PathTracer replaces these in-place
|
|
99
|
+
// across model load / BVH rebuild; the compute's bind group is locked
|
|
100
|
+
// to whatever buffer was bound at pipeline compile time, so we rebuild
|
|
101
|
+
// when any of them swaps to a new object.
|
|
102
|
+
this._lastTriAttr = null;
|
|
103
|
+
this._lastBvhAttr = null;
|
|
104
|
+
this._lastMatAttr = null;
|
|
105
|
+
|
|
98
106
|
// Compute node — built once when storage buffers are ready
|
|
99
107
|
this._computeNode = null;
|
|
100
108
|
this._computeBuilt = false;
|
|
@@ -138,50 +146,59 @@ export class NormalDepth extends RenderStage {
|
|
|
138
146
|
const pt = this.pathTracer;
|
|
139
147
|
if ( ! pt ) return false;
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
const matStorageAttr = pt.materialData.materialStorageAttr;
|
|
150
|
+
|
|
151
|
+
// Detect attribute identity swap (PathTracer.setTriangleData /
|
|
152
|
+
// setBVHData replace the attribute object on growth). The compute
|
|
153
|
+
// node's bind group is locked to the buffer bound at compile time —
|
|
154
|
+
// updating the storage node's .value alone leaves the GPU binding
|
|
155
|
+
// pointing at the now-discarded buffer, so every traversal misses.
|
|
156
|
+
const triSwapped = pt.triangleStorageAttr && pt.triangleStorageAttr !== this._lastTriAttr;
|
|
157
|
+
const bvhSwapped = pt.bvhStorageAttr && pt.bvhStorageAttr !== this._lastBvhAttr;
|
|
158
|
+
const matSwapped = matStorageAttr && matStorageAttr !== this._lastMatAttr;
|
|
159
|
+
|
|
160
|
+
if ( triSwapped || bvhSwapped || matSwapped ) {
|
|
161
|
+
|
|
162
|
+
// Drop compute + storage nodes so they get rebuilt against the
|
|
163
|
+
// current buffers. Cheap: this only happens on model load.
|
|
164
|
+
this._computeNode?.dispose?.();
|
|
165
|
+
this._computeNode = null;
|
|
166
|
+
this._computeBuilt = false;
|
|
167
|
+
this._triStorageNode = null;
|
|
168
|
+
this._bvhStorageNode = null;
|
|
169
|
+
this._matStorageNode = null;
|
|
170
|
+
this._dirty = true;
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
if ( pt.triangleStorageAttr && ! this._triStorageNode ) {
|
|
143
175
|
|
|
144
176
|
this._triStorageNode = storage(
|
|
145
177
|
pt.triangleStorageAttr, 'vec4', pt.triangleStorageAttr.count
|
|
146
178
|
).toReadOnly();
|
|
147
179
|
|
|
148
|
-
} else if ( pt.triangleStorageAttr && this._triStorageNode ) {
|
|
149
|
-
|
|
150
|
-
// Data changed (new model loaded) — update in-place
|
|
151
|
-
this._triStorageNode.value = pt.triangleStorageAttr;
|
|
152
|
-
this._triStorageNode.bufferCount = pt.triangleStorageAttr.count;
|
|
153
|
-
|
|
154
180
|
}
|
|
155
181
|
|
|
156
|
-
// BVH storage
|
|
157
182
|
if ( pt.bvhStorageAttr && ! this._bvhStorageNode ) {
|
|
158
183
|
|
|
159
184
|
this._bvhStorageNode = storage(
|
|
160
185
|
pt.bvhStorageAttr, 'vec4', pt.bvhStorageAttr.count
|
|
161
186
|
).toReadOnly();
|
|
162
187
|
|
|
163
|
-
} else if ( pt.bvhStorageAttr && this._bvhStorageNode ) {
|
|
164
|
-
|
|
165
|
-
this._bvhStorageNode.value = pt.bvhStorageAttr;
|
|
166
|
-
this._bvhStorageNode.bufferCount = pt.bvhStorageAttr.count;
|
|
167
|
-
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
// Material storage
|
|
171
|
-
const matStorageAttr = pt.materialData.materialStorageAttr;
|
|
172
190
|
if ( matStorageAttr && ! this._matStorageNode ) {
|
|
173
191
|
|
|
174
192
|
this._matStorageNode = storage(
|
|
175
193
|
matStorageAttr, 'vec4', matStorageAttr.count
|
|
176
194
|
).toReadOnly();
|
|
177
195
|
|
|
178
|
-
} else if ( matStorageAttr && this._matStorageNode ) {
|
|
179
|
-
|
|
180
|
-
this._matStorageNode.value = matStorageAttr;
|
|
181
|
-
this._matStorageNode.bufferCount = matStorageAttr.count;
|
|
182
|
-
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
|
|
199
|
+
this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
|
|
200
|
+
this._lastMatAttr = matStorageAttr || this._lastMatAttr;
|
|
201
|
+
|
|
185
202
|
return !! ( this._triStorageNode && this._bvhStorageNode && this._matStorageNode );
|
|
186
203
|
|
|
187
204
|
}
|