rayzee 7.24.0 → 7.25.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 +1 -1
- package/dist/rayzee.es.js +1787 -1735
- package/dist/rayzee.umd.js +6 -6
- package/package.json +2 -2
- package/src/Passes/AIUpscaler.js +3 -3
- package/src/Passes/OIDNDenoiser.js +2 -2
- package/src/PathTracerApp.js +13 -4
- package/src/Pipeline/CompletionTracker.js +45 -6
- package/src/Processor/AssetLoader.js +77 -11
- package/src/Processor/ToneMapCPU.js +37 -7
- package/src/RenderSettings.js +1 -0
- package/src/Stages/PathTracerStage.js +6 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time WebGPU path tracing engine built on Three.js",
|
|
6
6
|
"main": "dist/rayzee.umd.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/atul-mourya/
|
|
21
|
+
"url": "git+https://github.com/atul-mourya/rayzee-renderer.git",
|
|
22
22
|
"directory": "rayzee"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventDispatcher, ACESFilmicToneMapping } from 'three';
|
|
2
|
-
import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation } from '../Processor/ToneMapCPU.js';
|
|
2
|
+
import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation, effectiveExposure } from '../Processor/ToneMapCPU.js';
|
|
3
3
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
4
4
|
import AIUpscalerWorker from '../Processor/Workers/AIUpscalerWorker.js?worker&inline';
|
|
5
5
|
|
|
@@ -400,7 +400,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
400
400
|
if ( sourceImageData.isHDR ) {
|
|
401
401
|
|
|
402
402
|
this._hdrToneMapFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
403
|
-
this._hdrExposure = this.getExposure();
|
|
403
|
+
this._hdrExposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
|
|
404
404
|
this._hdrSaturation = this.getSaturation();
|
|
405
405
|
this._tmOut = new Float32Array( 3 );
|
|
406
406
|
|
|
@@ -793,7 +793,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
793
793
|
const imageData = ctx.createImageData( width, height );
|
|
794
794
|
const pixels = imageData.data;
|
|
795
795
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
796
|
-
const exposure = this.getExposure();
|
|
796
|
+
const exposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
|
|
797
797
|
const saturation = this.getSaturation();
|
|
798
798
|
const out = new Float32Array( 3 );
|
|
799
799
|
|
|
@@ -16,7 +16,7 @@ async function getInitUNetFromURL() {
|
|
|
16
16
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
import { TONE_MAP_FNS, linearToSRGB, applySaturation } from '../Processor/ToneMapCPU.js';
|
|
19
|
+
import { TONE_MAP_FNS, linearToSRGB, applySaturation, effectiveExposure } from '../Processor/ToneMapCPU.js';
|
|
20
20
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
21
21
|
|
|
22
22
|
/** Reusable RGB output buffer (avoids per-pixel allocation). */
|
|
@@ -764,7 +764,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
764
764
|
/** Display exposure with the input scale divided back out — output arrives pre-multiplied. */
|
|
765
765
|
_outputExposure() {
|
|
766
766
|
|
|
767
|
-
return this.getExposure() / this._oidnInputScale;
|
|
767
|
+
return effectiveExposure( this.getExposure(), this.getToneMapping() ) / this._oidnInputScale;
|
|
768
768
|
|
|
769
769
|
}
|
|
770
770
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -366,8 +366,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
366
366
|
memoryPeak: tracker?.peak ?? 0,
|
|
367
367
|
} );
|
|
368
368
|
|
|
369
|
-
//
|
|
370
|
-
|
|
369
|
+
// Only the wall-clock stop — PathTracer.render() retires the ceiling and convergence
|
|
370
|
+
// itself, so whichever of the three arrives first wins.
|
|
371
|
+
if ( this.completion.isTimeLimitReached(
|
|
372
|
+
this.stages.pathTracer, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' )
|
|
373
|
+
) ) {
|
|
371
374
|
|
|
372
375
|
this.stages.pathTracer.isComplete = true;
|
|
373
376
|
|
|
@@ -381,8 +384,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
381
384
|
context: this.pipeline?.context,
|
|
382
385
|
} );
|
|
383
386
|
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
const completionInfo = {
|
|
388
|
+
samples: this.stages.pathTracer.frameCount,
|
|
389
|
+
timeElapsed: this.completion.timeElapsed,
|
|
390
|
+
budgetOverrun: this.completion.budgetOverrun,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
this.dispatchEvent( { type: 'RenderComplete', ...completionInfo } );
|
|
394
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...completionInfo } );
|
|
386
395
|
|
|
387
396
|
}
|
|
388
397
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '../utils/Logger.js';
|
|
2
|
+
|
|
3
|
+
const log = createLogger( 'engine' );
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Tracks render completion state, time limits, and sample limits.
|
|
3
7
|
*
|
|
@@ -12,6 +16,10 @@ export class CompletionTracker {
|
|
|
12
16
|
this.lastResetTime = performance.now();
|
|
13
17
|
this.renderCompleteDispatched = false;
|
|
14
18
|
|
|
19
|
+
/** Whether the deadline — rather than the ceiling or convergence — retired the frame. */
|
|
20
|
+
this.budgetOverrun = false;
|
|
21
|
+
this._budgetHeldWarned = false;
|
|
22
|
+
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
@@ -24,19 +32,48 @@ export class CompletionTracker {
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/**
|
|
27
|
-
* Checks whether the
|
|
28
|
-
*
|
|
35
|
+
* Checks whether the wall-clock render budget has expired. An additional stop condition,
|
|
36
|
+
* never a replacement — the sample ceiling and convergence still bind when they come first.
|
|
37
|
+
*
|
|
38
|
+
* Floored at one sample: frameCount does not advance while the stage compiles kernels or
|
|
39
|
+
* while the camera is in interaction mode, and retiring in that window denoises an empty
|
|
40
|
+
* buffer and captures a blank canvas.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} pathTracer - The PathTracer stage
|
|
43
|
+
* @param {string} renderLimitMode - 'time' or 'frames'
|
|
29
44
|
* @param {number} renderTimeLimit - Time limit in seconds
|
|
30
45
|
* @returns {boolean}
|
|
31
46
|
*/
|
|
32
|
-
isTimeLimitReached( renderLimitMode, renderTimeLimit ) {
|
|
47
|
+
isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) {
|
|
48
|
+
|
|
49
|
+
if ( renderLimitMode !== 'time' || ! ( renderTimeLimit > 0 ) ) return false;
|
|
50
|
+
if ( this.timeElapsed < renderTimeLimit ) return false;
|
|
51
|
+
|
|
52
|
+
if ( ! ( pathTracer?.frameCount > 0 ) ) {
|
|
33
53
|
|
|
34
|
-
|
|
54
|
+
if ( ! this._budgetHeldWarned ) {
|
|
55
|
+
|
|
56
|
+
this._budgetHeldWarned = true;
|
|
57
|
+
log.warn(
|
|
58
|
+
`render time limit (${renderTimeLimit}s) expired before the first sample landed — ` +
|
|
59
|
+
'holding the deadline open to avoid capturing an empty frame'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.budgetOverrun = true;
|
|
69
|
+
return true;
|
|
35
70
|
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
/**
|
|
39
|
-
* Checks whether ANY render limit
|
|
74
|
+
* Checks whether ANY render limit is reached: sample ceiling, convergence, or time budget,
|
|
75
|
+
* whichever arrives first.
|
|
76
|
+
*
|
|
40
77
|
* @param {Object} pathTracer - The PathTracer stage
|
|
41
78
|
* @param {string} renderLimitMode
|
|
42
79
|
* @param {number} renderTimeLimit
|
|
@@ -46,7 +83,7 @@ export class CompletionTracker {
|
|
|
46
83
|
|
|
47
84
|
if ( ! pathTracer ) return false;
|
|
48
85
|
|
|
49
|
-
if ( this.isTimeLimitReached( renderLimitMode, renderTimeLimit ) ) return true;
|
|
86
|
+
if ( this.isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) ) return true;
|
|
50
87
|
|
|
51
88
|
// Tier-1 convergence early-stop must agree with PathTracer.render()'s own check, else the app-level
|
|
52
89
|
// reconcile would flip isComplete back off and keep dispatching after the frame converged.
|
|
@@ -75,6 +112,8 @@ export class CompletionTracker {
|
|
|
75
112
|
this.timeElapsed = 0;
|
|
76
113
|
this.lastResetTime = performance.now();
|
|
77
114
|
this.renderCompleteDispatched = false;
|
|
115
|
+
this.budgetOverrun = false;
|
|
116
|
+
this._budgetHeldWarned = false;
|
|
78
117
|
|
|
79
118
|
}
|
|
80
119
|
|
|
@@ -9,7 +9,7 @@ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
|
9
9
|
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
10
10
|
import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
|
|
11
11
|
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
12
|
-
import { unzipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
12
|
+
import { unzipSync, zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
13
13
|
import { disposeObjectFromMemory, updateLoading } from './utils';
|
|
14
14
|
import { BuildTimer } from './BuildTimer.js';
|
|
15
15
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
@@ -21,13 +21,20 @@ const SUPPORTED_FORMATS = {
|
|
|
21
21
|
'fbx': { type: 'model', name: 'FBX' }, 'obj': { type: 'model', name: 'OBJ' },
|
|
22
22
|
'stl': { type: 'model', name: 'STL' }, 'ply': { type: 'model', name: 'PLY (Polygon File Format)' },
|
|
23
23
|
'dae': { type: 'model', name: 'Collada' }, '3mf': { type: 'model', name: '3D Manufacturing Format' },
|
|
24
|
-
'
|
|
24
|
+
'usd': { type: 'model', name: 'USD (Universal Scene Description)' },
|
|
25
|
+
'usda': { type: 'model', name: 'USDA (USD ASCII)' },
|
|
26
|
+
'usdc': { type: 'model', name: 'USDC (USD Crate)' },
|
|
27
|
+
'usdz': { type: 'model', name: 'USDZ (USD Archive)' },
|
|
25
28
|
'hdr': { type: 'environment', name: 'HDR (High Dynamic Range)' }, 'exr': { type: 'environment', name: 'EXR (OpenEXR)' },
|
|
26
29
|
'png': { type: 'image', name: 'PNG' }, 'jpg': { type: 'image', name: 'JPEG' },
|
|
27
30
|
'jpeg': { type: 'image', name: 'JPEG' }, 'webp': { type: 'image', name: 'WebP' },
|
|
28
31
|
'zip': { type: 'archive', name: 'ZIP Archive' }
|
|
29
32
|
};
|
|
30
33
|
|
|
34
|
+
// Loose USD layers inside a ZIP compose into one scene; these pick out the
|
|
35
|
+
// layers and the image assets they reference.
|
|
36
|
+
const USD_LAYER_RE = /\.(usd|usda|usdc)$/i;
|
|
37
|
+
const USD_IMAGE_RE = /\.(png|jpg|jpeg|avif)$/i;
|
|
31
38
|
/**
|
|
32
39
|
* AssetLoader class - handles loading of 3D models, environment maps, and archives
|
|
33
40
|
*/
|
|
@@ -234,7 +241,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
234
241
|
case 'ply': return await this.loadPLYFromArrayBuffer( arrayBuffer, filename );
|
|
235
242
|
case 'dae': return await this.loadColladaFromFile( file, filename );
|
|
236
243
|
case '3mf': return await this.load3MFFromArrayBuffer( arrayBuffer, filename );
|
|
237
|
-
case '
|
|
244
|
+
case 'usd':
|
|
245
|
+
case 'usda':
|
|
246
|
+
case 'usdc':
|
|
247
|
+
case 'usdz': return await this.loadUSDFromArrayBuffer( arrayBuffer, filename );
|
|
238
248
|
default: throw new Error( `Support for ${extension} files is not yet implemented` );
|
|
239
249
|
|
|
240
250
|
}
|
|
@@ -647,6 +657,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
647
657
|
const extension = path.split( '.' ).pop().toLowerCase();
|
|
648
658
|
if ( SUPPORTED_FORMATS[ extension ] && SUPPORTED_FORMATS[ extension ].type === 'model' ) {
|
|
649
659
|
|
|
660
|
+
// A loose layer is only one slice of a USD scene — hand the whole
|
|
661
|
+
// archive over so its references and payloads can resolve.
|
|
662
|
+
if ( USD_LAYER_RE.test( path ) ) return await this.loadUSDHierarchyFromZip( zip );
|
|
663
|
+
|
|
650
664
|
console.log( `Loading model file from ZIP: ${path}` );
|
|
651
665
|
return await this.loadModelFromZipEntry( zip[ path ], path, extension, zip );
|
|
652
666
|
|
|
@@ -658,6 +672,48 @@ export class AssetLoader extends EventDispatcher {
|
|
|
658
672
|
|
|
659
673
|
}
|
|
660
674
|
|
|
675
|
+
// USDLoader only builds a cross-layer asset map on its USDZ branch, so repack
|
|
676
|
+
// the archive as USDZ — root layer first, per AOUSD core spec 16.4.1.2 — and
|
|
677
|
+
// let the loader resolve the references/payloads itself.
|
|
678
|
+
async loadUSDHierarchyFromZip( zip ) {
|
|
679
|
+
|
|
680
|
+
const layers = Object.keys( zip ).filter( name => USD_LAYER_RE.test( name ) );
|
|
681
|
+
if ( layers.length === 0 ) throw new Error( 'No USD layers found in the ZIP archive' );
|
|
682
|
+
|
|
683
|
+
const root = AssetLoader._pickUSDRootLayer( layers );
|
|
684
|
+
console.log( `Loading USD scene from ZIP: ${root} (${layers.length} layers)` );
|
|
685
|
+
|
|
686
|
+
const packed = { [ root ]: [ zip[ root ], { level: 0 } ] };
|
|
687
|
+
for ( const name of Object.keys( zip ) ) {
|
|
688
|
+
|
|
689
|
+
if ( name === root ) continue;
|
|
690
|
+
if ( USD_LAYER_RE.test( name ) || USD_IMAGE_RE.test( name ) ) packed[ name ] = [ zip[ name ], { level: 0 } ];
|
|
691
|
+
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return await this.loadUSDFromArrayBuffer( zipSync( packed ), root );
|
|
695
|
+
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Shallowest layer wins; among ties prefer the <dir>/<dir>.usd convention so a
|
|
699
|
+
// set's entry point beats its sibling variants.
|
|
700
|
+
static _pickUSDRootLayer( layers ) {
|
|
701
|
+
|
|
702
|
+
const depth = name => name.split( '/' ).length;
|
|
703
|
+
const minDepth = Math.min( ...layers.map( depth ) );
|
|
704
|
+
const candidates = layers.filter( name => depth( name ) === minDepth );
|
|
705
|
+
|
|
706
|
+
const conventional = candidates.find( name => {
|
|
707
|
+
|
|
708
|
+
const parts = name.split( '/' );
|
|
709
|
+
return parts.length > 1 && parts[ parts.length - 1 ].replace( USD_LAYER_RE, '' ) === parts[ parts.length - 2 ];
|
|
710
|
+
|
|
711
|
+
} );
|
|
712
|
+
|
|
713
|
+
return conventional || candidates[ 0 ];
|
|
714
|
+
|
|
715
|
+
}
|
|
716
|
+
|
|
661
717
|
async loadModelFromZipEntry( fileContent, filePath, extension, zipContents ) {
|
|
662
718
|
|
|
663
719
|
try {
|
|
@@ -698,8 +754,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
698
754
|
case '3mf':
|
|
699
755
|
result = await this.load3MFFromArrayBuffer( fileContent.buffer, filePath );
|
|
700
756
|
break;
|
|
757
|
+
case 'usd':
|
|
758
|
+
case 'usda':
|
|
759
|
+
case 'usdc':
|
|
701
760
|
case 'usdz':
|
|
702
|
-
result = await this.
|
|
761
|
+
result = await this.loadUSDFromArrayBuffer( fileContent, filePath );
|
|
703
762
|
break;
|
|
704
763
|
default:
|
|
705
764
|
throw new Error( `Support for ${extension} files is not yet implemented` );
|
|
@@ -1396,21 +1455,28 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1396
1455
|
|
|
1397
1456
|
}
|
|
1398
1457
|
|
|
1399
|
-
async
|
|
1458
|
+
async loadUSDFromArrayBuffer( data, filename = 'model.usd' ) {
|
|
1400
1459
|
|
|
1401
1460
|
try {
|
|
1402
1461
|
|
|
1403
|
-
updateLoading( { isLoading: true, status: "Processing
|
|
1462
|
+
updateLoading( { isLoading: true, status: "Processing USD Data...", progress: 5 } );
|
|
1404
1463
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
1405
1464
|
|
|
1406
|
-
if ( ! this.loaderCache.
|
|
1465
|
+
if ( ! this.loaderCache.usd ) {
|
|
1407
1466
|
|
|
1408
|
-
const {
|
|
1409
|
-
this.loaderCache.
|
|
1467
|
+
const { USDLoader } = await import( 'three/examples/jsm/loaders/USDLoader.js' );
|
|
1468
|
+
this.loaderCache.usd = new USDLoader();
|
|
1410
1469
|
|
|
1411
1470
|
}
|
|
1412
1471
|
|
|
1413
|
-
|
|
1472
|
+
// parse() returns the group synchronously but resolves textures async;
|
|
1473
|
+
// setupPathTracing snapshots materials, so maps must land before it runs.
|
|
1474
|
+
const object = await new Promise( ( resolve, reject ) => {
|
|
1475
|
+
|
|
1476
|
+
this.loaderCache.usd.parse( data, '', resolve, reject );
|
|
1477
|
+
|
|
1478
|
+
} );
|
|
1479
|
+
|
|
1414
1480
|
object.name = filename;
|
|
1415
1481
|
|
|
1416
1482
|
this.releaseTargetModel();
|
|
@@ -1424,7 +1490,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1424
1490
|
|
|
1425
1491
|
} catch ( error ) {
|
|
1426
1492
|
|
|
1427
|
-
console.error( 'Error loading
|
|
1493
|
+
console.error( 'Error loading USD:', error );
|
|
1428
1494
|
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
1429
1495
|
throw error;
|
|
1430
1496
|
|
|
@@ -140,15 +140,26 @@ function neutralToneMap( r, g, b, exposure, out ) {
|
|
|
140
140
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Three.js clamps the fragment output with `.max( 0 )` *before* tone mapping
|
|
145
|
+
* (NodeMaterial.js, "force unsigned floats"), so the GPU curves never see a negative
|
|
146
|
+
* channel. Compositor's saturation grade (default 1.2) drives complementary channels
|
|
147
|
+
* below zero on a third of a typical frame, and AgX/Neutral mix those negatives across
|
|
148
|
+
* channels instead of clipping them — measured 7-10 levels of shadow error against the
|
|
149
|
+
* viewport until the readback clamps the same way.
|
|
150
|
+
*/
|
|
151
|
+
const clampNegative = fn => ( r, g, b, exposure, out ) =>
|
|
152
|
+
fn( r > 0 ? r : 0, g > 0 ? g : 0, b > 0 ? b : 0, exposure, out );
|
|
153
|
+
|
|
143
154
|
/** Look-up table mapping Three.js ToneMapping constants to CPU functions. */
|
|
144
155
|
export const TONE_MAP_FNS = new Map( [
|
|
145
|
-
[ NoToneMapping, noToneMap ],
|
|
146
|
-
[ LinearToneMapping, linearToneMap ],
|
|
147
|
-
[ ReinhardToneMapping, reinhardToneMap ],
|
|
148
|
-
[ CineonToneMapping, cineonToneMap ],
|
|
149
|
-
[ ACESFilmicToneMapping, acesFilmicToneMap ],
|
|
150
|
-
[ AgXToneMapping, agxToneMap ],
|
|
151
|
-
[ NeutralToneMapping, neutralToneMap ]
|
|
156
|
+
[ NoToneMapping, clampNegative( noToneMap ) ],
|
|
157
|
+
[ LinearToneMapping, clampNegative( linearToneMap ) ],
|
|
158
|
+
[ ReinhardToneMapping, clampNegative( reinhardToneMap ) ],
|
|
159
|
+
[ CineonToneMapping, clampNegative( cineonToneMap ) ],
|
|
160
|
+
[ ACESFilmicToneMapping, clampNegative( acesFilmicToneMap ) ],
|
|
161
|
+
[ AgXToneMapping, clampNegative( agxToneMap ) ],
|
|
162
|
+
[ NeutralToneMapping, clampNegative( neutralToneMap ) ]
|
|
152
163
|
] );
|
|
153
164
|
|
|
154
165
|
/** sRGB gamma (1/2.2) — fast pow approximation. Prefer `linearToSRGB` when matching Three.js's output. */
|
|
@@ -165,6 +176,25 @@ export function linearToSRGB( c ) {
|
|
|
165
176
|
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Exposure as the WebGPU output pass applies it.
|
|
181
|
+
*
|
|
182
|
+
* Three.js applies `toneMappingExposure` *inside* ToneMappingNode's tone-mapping branch, and
|
|
183
|
+
* that branch returns the colour untouched for NoToneMapping (ToneMappingNode.js: `if
|
|
184
|
+
* ( toneMapping === NoToneMapping ) return colorNode`). So exposure is a no-op on screen there,
|
|
185
|
+
* and a CPU readback that applies it anyway paints brighter than the viewport it replaces
|
|
186
|
+
* (measured +33.6 % at exposure 2).
|
|
187
|
+
*
|
|
188
|
+
* @param {number} exposure - renderer.toneMappingExposure
|
|
189
|
+
* @param {number} toneMapping - Three.js ToneMapping constant
|
|
190
|
+
* @returns {number}
|
|
191
|
+
*/
|
|
192
|
+
export function effectiveExposure( exposure, toneMapping ) {
|
|
193
|
+
|
|
194
|
+
return toneMapping === NoToneMapping ? 1.0 : exposure;
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
168
198
|
/** Rec.709 luminance coefficients (same as Display / Common.js). */
|
|
169
199
|
const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
|
|
170
200
|
|
package/src/RenderSettings.js
CHANGED
|
@@ -1185,25 +1185,15 @@ export class PathTracerStage extends RenderStage {
|
|
|
1185
1185
|
}
|
|
1186
1186
|
|
|
1187
1187
|
/**
|
|
1188
|
-
* Update completion threshold based on render mode
|
|
1188
|
+
* Update completion threshold based on render mode. The ceiling holds in every limit mode —
|
|
1189
|
+
* uncapping it under a time budget lets a generous deadline silently slow every render.
|
|
1189
1190
|
*/
|
|
1190
1191
|
updateCompletionThreshold() {
|
|
1191
1192
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
this.completionThreshold = Infinity;
|
|
1198
|
-
|
|
1199
|
-
} else {
|
|
1200
|
-
|
|
1201
|
-
this.completionThreshold = updateCompletionThreshold(
|
|
1202
|
-
renderMode,
|
|
1203
|
-
maxFrames
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1193
|
+
this.completionThreshold = updateCompletionThreshold(
|
|
1194
|
+
this.renderMode.value,
|
|
1195
|
+
this.maxSamples.value
|
|
1196
|
+
);
|
|
1207
1197
|
|
|
1208
1198
|
}
|
|
1209
1199
|
|