rayzee 5.6.1 → 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.
- 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 +896 -863
- 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 +17 -0
- package/src/Processor/EquirectHDRInfo.js +76 -16
- package/src/Processor/ShaderBuilder.js +1 -0
- package/src/Processor/Workers/CDFWorker.js +72 -11
- 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/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/package.json
CHANGED
package/src/EngineEvents.js
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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/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 {
|
package/src/TSL/Environment.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1237
|
+
balanceHeuristic( { pdf1: envPdf, pdf2: bPdf } ),
|
|
1237
1238
|
float( 1.0 )
|
|
1238
1239
|
).toVar();
|
|
1239
1240
|
|
package/src/TSL/PathTracer.js
CHANGED
|
@@ -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(
|
|
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 );
|
|
@@ -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"}
|