rayzee 11.3.0 → 11.4.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 +33 -5
- package/dist/{ArchiveImporter-D735xX-4.js → ArchiveImporter-Cgyx5AF7.js} +2 -2
- package/dist/{BidirectionalIntegrator-M89c0AlP.js → BidirectionalIntegrator-BKF-cwH2.js} +2 -2
- package/dist/{NeuralRetouch-DiBH63kz.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
- package/dist/{NeuralSuperRes-CZNQP13W.js → NeuralSuperRes-DV5TtURP.js} +11 -5
- package/dist/{PathTracer-BXCCVrPK.js → PathTracer-D3MvEaZK.js} +19 -12
- package/dist/{PhysicalSky-DqmaS0lb.js → PhysicalSky-CY4jiz6a.js} +1 -1
- package/dist/{RayzeeRenderer-DUSMFhmc.js → RayzeeRenderer-D23m6NqG.js} +313 -281
- package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +3 -3
- package/dist/rayzee.es.js +1343 -858
- package/dist/rayzee.umd.js +124 -113
- package/dist/{sceneCachePolicy-BAib5fCp.js → sceneCachePolicy-AKOEu2lf.js} +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -2
- package/src/Passes/AIUpscaler.js +2 -0
- package/src/PathTracerApp.js +122 -16
- package/src/Pipeline/CompletionTracker.js +7 -3
- package/src/Processor/ToneMapCPU.js +16 -4
- package/src/Processor/ToneMapGPU.js +30 -14
- package/src/RayzeeRenderer.js +54 -7
- package/src/RenderSettings.js +4 -2
- package/src/Stages/AutoExposure.js +484 -494
- package/src/Stages/Compositor.js +27 -6
- package/src/Stages/LocalExposure.js +546 -0
- package/src/Stages/PathTracer.js +6 -4
- package/src/Stages/PathTracerStage.js +9 -7
- package/src/index.js +1 -0
- package/src/managers/DenoisingManager.js +11 -15
- package/src/managers/VideoRenderManager.js +30 -4
- package/src/neural/NeuralRetouch.js +1 -0
- package/src/neural/NeuralSuperRes.js +6 -1
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -24,8 +24,8 @@ export const ENGINE_DEFAULTS = deepFreeze( {
|
|
|
24
24
|
|
|
25
25
|
// Sampling and convergence
|
|
26
26
|
maxSamples: 60,
|
|
27
|
-
renderLimitMode: 'frames',
|
|
28
|
-
renderTimeLimit: 30,
|
|
27
|
+
renderLimitMode: 'frames', // 'frames' | 'time' (ceiling or deadline, whichever first) | 'timeOnly' (deadline lifts the ceiling)
|
|
28
|
+
renderTimeLimit: 30, // seconds; read in 'time' and 'timeOnly'
|
|
29
29
|
renderMode: 0, // 0 preview, 1 production; configureForMode() sets it
|
|
30
30
|
useAdaptiveSampling: true,
|
|
31
31
|
noiseThreshold: 0.1, // a pixel under this √-luminance noise has converged; the interactive tier's value
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -100,6 +100,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
100
100
|
this.getExposure = options.getExposure || ( () => 1.0 );
|
|
101
101
|
this.getToneMapping = options.getToneMapping || ( () => ACESFilmicToneMapping );
|
|
102
102
|
this.getSaturation = options.getSaturation || ( () => 1.0 );
|
|
103
|
+
this.getDisplayGain = options.getDisplayGain || ( () => null );
|
|
103
104
|
|
|
104
105
|
// Configuration
|
|
105
106
|
this.enabled = false;
|
|
@@ -497,6 +498,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
497
498
|
exposure: this.getExposure(),
|
|
498
499
|
toneMapping: this.getToneMapping() ?? ACESFilmicToneMapping,
|
|
499
500
|
saturation: this.getSaturation(),
|
|
501
|
+
gain: this.getDisplayGain(),
|
|
500
502
|
} );
|
|
501
503
|
this._toneMapper.release();
|
|
502
504
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Variance } from './Stages/Variance.js';
|
|
|
8
8
|
import { BilateralFilter } from './Stages/BilateralFilter.js';
|
|
9
9
|
import { EdgeFilter } from './Stages/EdgeFilter.js';
|
|
10
10
|
import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
|
|
11
|
+
import { LocalExposure, LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
|
|
11
12
|
import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
|
|
12
13
|
import { createLogger } from './utils/Logger.js';
|
|
13
14
|
import { EngineEvents } from './EngineEvents.js';
|
|
@@ -156,10 +157,11 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
156
157
|
stages.bilateralFilter = new BilateralFilter( renderer, { enabled: false } );
|
|
157
158
|
stages.edgeFilter = new EdgeFilter( renderer, { enabled: false } );
|
|
158
159
|
stages.autoExposure = new AutoExposure( renderer, { enabled: AUTO_EXPOSURE_DEFAULTS.autoExposure } );
|
|
160
|
+
stages.localExposure = new LocalExposure( renderer, { enabled: LOCAL_EXPOSURE_DEFAULTS.localExposure } );
|
|
159
161
|
|
|
160
162
|
return [
|
|
161
163
|
stages.normalDepth, stages.motionVector, stages.nrd, stages.asvgf,
|
|
162
|
-
stages.variance, stages.bilateralFilter, stages.edgeFilter, stages.autoExposure,
|
|
164
|
+
stages.variance, stages.bilateralFilter, stages.edgeFilter, stages.autoExposure, stages.localExposure,
|
|
163
165
|
];
|
|
164
166
|
|
|
165
167
|
}
|
|
@@ -325,10 +327,11 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
325
327
|
const core = super._settingsBindings();
|
|
326
328
|
return {
|
|
327
329
|
...core,
|
|
328
|
-
//
|
|
330
|
+
// the manual exposure, and the compensation on top of auto exposure while that is on
|
|
329
331
|
applyExposure: ( value ) => {
|
|
330
332
|
|
|
331
|
-
if (
|
|
333
|
+
if ( this.stages.autoExposure ) this.stages.autoExposure.setCompensation( value );
|
|
334
|
+
else core.applyExposure( value );
|
|
332
335
|
|
|
333
336
|
},
|
|
334
337
|
onCameraProjection: ( value ) => {
|
|
@@ -351,6 +354,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
351
354
|
|
|
352
355
|
this.timeline.update();
|
|
353
356
|
this.cameraManager.updateControls();
|
|
357
|
+
this._updateExposure();
|
|
354
358
|
|
|
355
359
|
this._applyPendingRenderScale();
|
|
356
360
|
|
|
@@ -406,6 +410,8 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
406
410
|
// Render completion → denoise/upscale chain
|
|
407
411
|
_renderCompleted() {
|
|
408
412
|
|
|
413
|
+
// _finishImage() landed the exposure just before, with the loop already asleep: draw it.
|
|
414
|
+
if ( this.stages.autoExposure?.enabled ) this._refreshFinished();
|
|
409
415
|
this.denoisingManager.onRenderComplete( {
|
|
410
416
|
isStillComplete: () => this.completion.renderCompleteDispatched,
|
|
411
417
|
context: this.pipeline?.context,
|
|
@@ -413,6 +419,65 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
413
419
|
|
|
414
420
|
}
|
|
415
421
|
|
|
422
|
+
_settling() {
|
|
423
|
+
|
|
424
|
+
return !! this.stages.autoExposure?.settling;
|
|
425
|
+
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_displayGain() {
|
|
429
|
+
|
|
430
|
+
const le = this.stages.localExposure;
|
|
431
|
+
// A readback outside the loop (headless) can come before the loop would have built it from this image.
|
|
432
|
+
if ( le?.wantsBuild ) le.build( this.pipeline.context );
|
|
433
|
+
return le?.toneGain() ?? null;
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Local exposure (Unreal Engine 5's): scales the contrast of large bright and dark areas around middle grey
|
|
439
|
+
* while keeping detail, on the canvas and every tone-mapped readback alike.
|
|
440
|
+
* @param {boolean} enabled
|
|
441
|
+
*/
|
|
442
|
+
setLocalExposure( enabled ) {
|
|
443
|
+
|
|
444
|
+
const le = this.stages.localExposure;
|
|
445
|
+
if ( ! le ) return;
|
|
446
|
+
le.setEnabled( enabled );
|
|
447
|
+
this.stages.compositor.setDisplayGain( enabled ? ( rgb, uv ) => le.gainNode( rgb, uv ) : null );
|
|
448
|
+
this.refreshFrame();
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @param {{highlightContrast?: number, shadowContrast?: number, detailStrength?: number}} params - 1 changes nothing */
|
|
453
|
+
setLocalExposureParams( params ) {
|
|
454
|
+
|
|
455
|
+
this.stages.localExposure?.updateParameters( params );
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// A finished render is exposed for its own image: one reading of it, landed on.
|
|
460
|
+
_finishImage() {
|
|
461
|
+
|
|
462
|
+
const ae = this.stages.autoExposure;
|
|
463
|
+
if ( ! ae?.enabled ) return null;
|
|
464
|
+
return ae.meter( this.pipeline.context ).then( () => ae.advance( Infinity ) );
|
|
465
|
+
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The core's, plus the display-only exposure an sRGB readback was shaped by, which no setting records. */
|
|
469
|
+
getProvenance() {
|
|
470
|
+
|
|
471
|
+
const provenance = super.getProvenance();
|
|
472
|
+
const ae = this.stages.autoExposure, le = this.stages.localExposure;
|
|
473
|
+
provenance.exposure = {
|
|
474
|
+
auto: ae?.enabled ? { exposure: ae.getExposure(), strength: ae.strength, metering: ae.metering, keyValue: ae.keyValue } : null,
|
|
475
|
+
local: le?.enabled ? { highlightContrast: le.highlightContrast, shadowContrast: le.shadowContrast, detailStrength: le.detailStrength } : null,
|
|
476
|
+
};
|
|
477
|
+
return provenance;
|
|
478
|
+
|
|
479
|
+
}
|
|
480
|
+
|
|
416
481
|
_renderHelperOverlay() {
|
|
417
482
|
|
|
418
483
|
super._renderHelperOverlay();
|
|
@@ -550,7 +615,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
550
615
|
if ( enabled ) {
|
|
551
616
|
|
|
552
617
|
this.cameraManager?.setAutoFocusMode( 'manual' );
|
|
553
|
-
|
|
618
|
+
this.stages.autoExposure?.setEnabled( false );
|
|
554
619
|
// Cadence denoising is wall-clock driven, so which frame it lands on is not reproducible.
|
|
555
620
|
this.denoisingManager?.setContinuousDenoise( false );
|
|
556
621
|
return;
|
|
@@ -558,11 +623,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
558
623
|
}
|
|
559
624
|
|
|
560
625
|
if ( snapshot.autoFocusMode !== undefined ) this.cameraManager?.setAutoFocusMode( snapshot.autoFocusMode );
|
|
561
|
-
if ( this.stages.autoExposure
|
|
562
|
-
|
|
563
|
-
this.stages.autoExposure.enabled = snapshot.autoExposure;
|
|
564
|
-
|
|
565
|
-
}
|
|
626
|
+
if ( snapshot.autoExposure !== undefined ) this.stages.autoExposure?.setEnabled( snapshot.autoExposure );
|
|
566
627
|
|
|
567
628
|
if ( snapshot.continuousDenoise !== undefined ) {
|
|
568
629
|
|
|
@@ -839,6 +900,9 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
839
900
|
|
|
840
901
|
this.timeline.stop();
|
|
841
902
|
this.cameraManager.controls.enabled = ! isProduction;
|
|
903
|
+
// A final render keeps the exposure it starts with through its first, noisiest samples (20 bounces: a 0.4-stop
|
|
904
|
+
// dip and back within 0.2 s at 2–8 samples), then lands as any still image does.
|
|
905
|
+
if ( this.stages.autoExposure ) this.stages.autoExposure.holdSamples = isProduction ? 8 : 0;
|
|
842
906
|
|
|
843
907
|
// Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
|
|
844
908
|
// `prev === value`, so a uniform written behind the map leaves it stale and the next set() silently no-ops.
|
|
@@ -1053,6 +1117,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
1053
1117
|
pipeline: this.pipeline,
|
|
1054
1118
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
1055
1119
|
getSaturation: () => this.settings.get( 'saturation' ) ?? 1.0,
|
|
1120
|
+
getDisplayGain: () => this._displayGain(),
|
|
1056
1121
|
issues: this._issues,
|
|
1057
1122
|
} );
|
|
1058
1123
|
|
|
@@ -1072,20 +1137,61 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
1072
1137
|
|
|
1073
1138
|
_setupAutoExposureListener() {
|
|
1074
1139
|
|
|
1075
|
-
|
|
1140
|
+
const ae = this.stages.autoExposure;
|
|
1141
|
+
if ( ! ae ) return;
|
|
1076
1142
|
|
|
1077
|
-
this.
|
|
1143
|
+
ae.setCompensation( this.settings.get( 'exposure' ) ?? 1 );
|
|
1144
|
+
if ( this.stages.localExposure ) this.stages.localExposure.onChange = () => this._refreshFinished();
|
|
1145
|
+
ae.onChange = () => this._refreshFinished();
|
|
1078
1146
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1147
|
+
// Another camera is a cut: meter it afresh.
|
|
1148
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.CAMERA_SWITCHED, () => ae.resetHistory() );
|
|
1149
|
+
|
|
1150
|
+
ae.on( 'autoexposure:updated', ( { exposure, autoExposure, targetExposure, luminance } ) => {
|
|
1151
|
+
|
|
1152
|
+
this.dispatchEvent( { type: EngineEvents.AUTO_EXPOSURE_UPDATED, exposure, autoExposure, targetExposure, luminance } );
|
|
1084
1153
|
|
|
1085
1154
|
} );
|
|
1086
1155
|
|
|
1087
1156
|
}
|
|
1088
1157
|
|
|
1158
|
+
// Only a finished render's idle loop is woken: renderFrames() and a video export drive frames themselves.
|
|
1159
|
+
_refreshFinished() {
|
|
1160
|
+
|
|
1161
|
+
if ( ! this.completion.renderCompleteDispatched ) return;
|
|
1162
|
+
this._needsDisplayRefresh = true;
|
|
1163
|
+
this.wake();
|
|
1164
|
+
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Every loop frame, a finished render included: builds local exposure and meters a finished image when asked, adapts.
|
|
1168
|
+
_updateExposure() {
|
|
1169
|
+
|
|
1170
|
+
const le = this.stages.localExposure;
|
|
1171
|
+
if ( le?.wantsBuild && this.completion.renderCompleteDispatched ) le.build( this.pipeline.context );
|
|
1172
|
+
|
|
1173
|
+
const ae = this.stages.autoExposure;
|
|
1174
|
+
if ( ! ae?.enabled ) return;
|
|
1175
|
+
|
|
1176
|
+
this._noteExposureView();
|
|
1177
|
+
const af = this.cameraManager.afScreenPoint;
|
|
1178
|
+
if ( ae.metering === 'spot' && ( ae.meteringPoint.x !== af.x || ae.meteringPoint.y !== af.y ) ) ae.updateParameters( { meteringPoint: af } );
|
|
1179
|
+
if ( ae.wantsMetering && this.completion.renderCompleteDispatched ) ae.meter( this.pipeline.context );
|
|
1180
|
+
if ( ae.update() ) this._needsDisplayRefresh = true;
|
|
1181
|
+
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Whatever moved the camera (controls, walk, timeline, a video export), auto exposure learns of it here.
|
|
1185
|
+
_noteExposureView() {
|
|
1186
|
+
|
|
1187
|
+
const { matrixWorld, projectionMatrix } = this.camera;
|
|
1188
|
+
const last = this._exposureView;
|
|
1189
|
+
if ( last?.view.equals( matrixWorld ) && last.projection.equals( projectionMatrix ) ) return;
|
|
1190
|
+
this._exposureView = { view: matrixWorld.clone(), projection: projectionMatrix.clone() };
|
|
1191
|
+
if ( last ) this.stages.autoExposure?.noteViewChanged();
|
|
1192
|
+
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1089
1195
|
async _setupOverlayManager() {
|
|
1090
1196
|
|
|
1091
1197
|
this.overlayManager = new OverlayManager( this.cameraManager.camera );
|
|
@@ -33,20 +33,24 @@ export class CompletionTracker {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Checks whether the wall-clock render budget has expired. An additional stop condition,
|
|
36
|
-
* never a replacement —
|
|
36
|
+
* never a replacement — convergence still binds when it comes first, and in 'time' mode the
|
|
37
|
+
* sample ceiling too ('timeOnly' lifts it: PathTracerStage.updateCompletionThreshold).
|
|
37
38
|
*
|
|
38
39
|
* Floored at one sample: frameCount does not advance while the stage compiles kernels or
|
|
39
40
|
* while the camera is in interaction mode, and retiring in that window denoises an empty
|
|
40
41
|
* buffer and captures a blank canvas.
|
|
41
42
|
*
|
|
42
43
|
* @param {Object} pathTracer - The PathTracer stage
|
|
43
|
-
* @param {string} renderLimitMode - 'time' or '
|
|
44
|
+
* @param {string} renderLimitMode - 'frames', 'time' or 'timeOnly'
|
|
44
45
|
* @param {number} renderTimeLimit - Time limit in seconds
|
|
45
46
|
* @returns {boolean}
|
|
46
47
|
*/
|
|
47
48
|
isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) {
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// Decided afresh on every check: a raised limit or a switch to 'frames' reopens a render the deadline retired.
|
|
51
|
+
this.budgetOverrun = false;
|
|
52
|
+
|
|
53
|
+
if ( ( renderLimitMode !== 'time' && renderLimitMode !== 'timeOnly' ) || ! ( renderTimeLimit > 0 ) ) return false;
|
|
50
54
|
if ( this.timeElapsed < renderTimeLimit ) return false;
|
|
51
55
|
|
|
52
56
|
if ( ! ( pathTracer?.frameCount > 0 ) ) {
|
|
@@ -128,21 +128,33 @@ export function applySaturation( out, saturation ) {
|
|
|
128
128
|
* @param {number} options.toneMapping - a registered view transform id
|
|
129
129
|
* @param {number} [options.saturation=1]
|
|
130
130
|
* @param {boolean} [options.preserveAlpha=false]
|
|
131
|
+
* @param {{fn: function(number, number, number, number): number, width: number, height: number}} [options.pixelGain] -
|
|
132
|
+
* the twin of the GPU pass's `gain`: `fn( u, v, luminance, log2 exposure )` per pixel, before the curve
|
|
131
133
|
* @returns {Uint8ClampedArray} RGBA bytes
|
|
132
134
|
*/
|
|
133
|
-
export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1, preserveAlpha = false } ) {
|
|
135
|
+
export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1, preserveAlpha = false, pixelGain = null } ) {
|
|
134
136
|
|
|
135
137
|
const curve = TONE_MAP_FNS.get( toneMapping ) ?? TONE_MAP_FNS.get( NoToneMapping );
|
|
136
138
|
const gain = effectiveExposure( exposure, toneMapping );
|
|
137
139
|
const encoded = isOutputEncoded( toneMapping );
|
|
138
140
|
const out = new Uint8ClampedArray( linear.length );
|
|
139
141
|
const scratch = [ 0, 0, 0 ];
|
|
142
|
+
const logExposure = Math.log2( exposure );
|
|
140
143
|
|
|
141
144
|
for ( let i = 0; i < linear.length; i += 4 ) {
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
let g = gain;
|
|
147
|
+
if ( pixelGain ) {
|
|
148
|
+
|
|
149
|
+
const p = i >> 2, x = p % pixelGain.width, y = ( p - x ) / pixelGain.width;
|
|
150
|
+
const lum = linear[ i ] * 0.2126 + linear[ i + 1 ] * 0.7152 + linear[ i + 2 ] * 0.0722;
|
|
151
|
+
g *= pixelGain.fn( ( x + 0.5 ) / pixelGain.width, ( y + 0.5 ) / pixelGain.height, lum, logExposure );
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
scratch[ 0 ] = linear[ i ] * g;
|
|
156
|
+
scratch[ 1 ] = linear[ i + 1 ] * g;
|
|
157
|
+
scratch[ 2 ] = linear[ i + 2 ] * g;
|
|
146
158
|
|
|
147
159
|
applySaturation( scratch, saturation );
|
|
148
160
|
curve( scratch[ 0 ], scratch[ 1 ], scratch[ 2 ], 1.0, scratch );
|
|
@@ -88,7 +88,7 @@ fn rayzee_store( i: u32, linear: vec4<f32>, mapped: vec3<f32> ) {
|
|
|
88
88
|
|
|
89
89
|
const BYTES_PER_PIXEL = { rgba8: 4, planar: 16 };
|
|
90
90
|
|
|
91
|
-
const toneMapPassWGSL = ( transformWGSL, input, output ) => /* wgsl */ `
|
|
91
|
+
const toneMapPassWGSL = ( transformWGSL, input, output, gainWGSL ) => /* wgsl */ `
|
|
92
92
|
struct Params {
|
|
93
93
|
width: u32,
|
|
94
94
|
height: u32,
|
|
@@ -104,6 +104,7 @@ ${SOURCES[ input ]}
|
|
|
104
104
|
|
|
105
105
|
${transformWGSL}
|
|
106
106
|
${OUTPUTS[ output ]}
|
|
107
|
+
${gainWGSL ?? ''}
|
|
107
108
|
|
|
108
109
|
@compute @workgroup_size(8, 8)
|
|
109
110
|
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
@@ -113,7 +114,9 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
113
114
|
if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
|
|
114
115
|
|
|
115
116
|
let linear = rayzee_source( gid.x, srcY );
|
|
116
|
-
|
|
117
|
+
let uv = vec2<f32>( ( f32( gid.x ) + 0.5 ) / f32( params.width ), ( f32( gid.y ) + 0.5 ) / f32( params.height ) );
|
|
118
|
+
let rgb = linear.rgb${gainWGSL ? ' * rayzee_gain( uv, linear.rgb, params.exposure )' : ''};
|
|
119
|
+
rayzee_store( gid.y * params.width + gid.x, linear, rayzee_tone_map( rgb, params.mode, params.exposure, params.saturation ) );
|
|
117
120
|
}
|
|
118
121
|
`;
|
|
119
122
|
|
|
@@ -136,7 +139,7 @@ export class PackedToneMapper {
|
|
|
136
139
|
this.output = output;
|
|
137
140
|
this.width = 0;
|
|
138
141
|
this.height = 0;
|
|
139
|
-
this.
|
|
142
|
+
this._pipelines = new Map();
|
|
140
143
|
this._storage = null;
|
|
141
144
|
this._map = null;
|
|
142
145
|
this._params = null;
|
|
@@ -157,21 +160,31 @@ export class PackedToneMapper {
|
|
|
157
160
|
* heard of. Without this the readback silently falls through to the clamp and a saved image
|
|
158
161
|
* comes back untone-mapped.
|
|
159
162
|
*/
|
|
160
|
-
_ensurePipeline() {
|
|
163
|
+
_ensurePipeline( gain = null ) {
|
|
161
164
|
|
|
162
165
|
const { wgsl, bindings, version } = currentShader();
|
|
163
|
-
if ( this.
|
|
166
|
+
if ( this._shaderVersion !== version ) {
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
this._pipelines.clear();
|
|
169
|
+
this._bindings = bindings;
|
|
170
|
+
this._shaderVersion = version;
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const key = gain?.key ?? '';
|
|
175
|
+
let pipeline = this._pipelines.get( key );
|
|
176
|
+
if ( pipeline ) return pipeline;
|
|
177
|
+
|
|
178
|
+
pipeline = this.device.createComputePipeline( {
|
|
168
179
|
label: this.label,
|
|
169
180
|
layout: 'auto',
|
|
170
181
|
compute: {
|
|
171
|
-
module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input, this.output ) } ),
|
|
182
|
+
module: this.device.createShaderModule( { label: this.label, code: toneMapPassWGSL( wgsl, this.input, this.output, gain?.wgsl ) } ),
|
|
172
183
|
entryPoint: 'main',
|
|
173
184
|
},
|
|
174
185
|
} );
|
|
186
|
+
this._pipelines.set( key, pipeline );
|
|
187
|
+
return pipeline;
|
|
175
188
|
|
|
176
189
|
}
|
|
177
190
|
|
|
@@ -262,6 +275,8 @@ export class PackedToneMapper {
|
|
|
262
275
|
* @param {number} [tone.saturation=1]
|
|
263
276
|
* @param {boolean} [tone.flipY=false]
|
|
264
277
|
* @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
|
|
278
|
+
* @param {{key: string, wgsl: string, entries: GPUBindGroupEntry[]}} [tone.gain] - a per-pixel gain before the
|
|
279
|
+
* curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1
|
|
265
280
|
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
266
281
|
*/
|
|
267
282
|
async toRGBA8( src, tone ) {
|
|
@@ -283,14 +298,14 @@ export class PackedToneMapper {
|
|
|
283
298
|
|
|
284
299
|
}
|
|
285
300
|
|
|
286
|
-
async _run( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false } = {} ) {
|
|
301
|
+
async _run( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false, gain = null } = {} ) {
|
|
287
302
|
|
|
288
303
|
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
289
304
|
if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
|
290
305
|
|
|
291
306
|
// A config can be loaded between two readbacks, so the registry is re-checked here rather
|
|
292
307
|
// than only on resize.
|
|
293
|
-
this._ensurePipeline();
|
|
308
|
+
const pipeline = this._ensurePipeline( gain );
|
|
294
309
|
this._ensureTables();
|
|
295
310
|
|
|
296
311
|
const { width, height } = this;
|
|
@@ -309,7 +324,7 @@ export class PackedToneMapper {
|
|
|
309
324
|
.map( ( [ binding, held ] ) => ( { binding, resource: held.view } ) );
|
|
310
325
|
|
|
311
326
|
const group = this.device.createBindGroup( {
|
|
312
|
-
layout:
|
|
327
|
+
layout: pipeline.getBindGroupLayout( 0 ),
|
|
313
328
|
entries: [
|
|
314
329
|
...tableEntries,
|
|
315
330
|
{ binding: 0, resource: this.input === 'texture' ? src.createView() : { buffer: src, size: width * height * 8 } },
|
|
@@ -320,8 +335,9 @@ export class PackedToneMapper {
|
|
|
320
335
|
|
|
321
336
|
const encoder = this.device.createCommandEncoder( { label: this.label } );
|
|
322
337
|
const pass = encoder.beginComputePass();
|
|
323
|
-
pass.setPipeline(
|
|
338
|
+
pass.setPipeline( pipeline );
|
|
324
339
|
pass.setBindGroup( 0, group );
|
|
340
|
+
if ( gain ) pass.setBindGroup( 1, this.device.createBindGroup( { layout: pipeline.getBindGroupLayout( 1 ), entries: gain.entries } ) );
|
|
325
341
|
pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
|
|
326
342
|
pass.end();
|
|
327
343
|
encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, bytes );
|
|
@@ -360,7 +376,7 @@ export class PackedToneMapper {
|
|
|
360
376
|
this._releaseBuffers();
|
|
361
377
|
for ( const held of this._tables.values() ) held.texture.destroy();
|
|
362
378
|
this._tables.clear();
|
|
363
|
-
this.
|
|
379
|
+
this._pipelines.clear();
|
|
364
380
|
|
|
365
381
|
}
|
|
366
382
|
|
package/src/RayzeeRenderer.js
CHANGED
|
@@ -532,6 +532,13 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
532
532
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
if ( this._settling() ) {
|
|
536
|
+
|
|
537
|
+
this.dispatchEvent( { type: EngineEvents.FRAME } );
|
|
538
|
+
return;
|
|
539
|
+
|
|
540
|
+
}
|
|
541
|
+
|
|
535
542
|
// Stop the loop to avoid constant CPU usage while idle
|
|
536
543
|
this.stopAnimation();
|
|
537
544
|
return;
|
|
@@ -558,12 +565,7 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
558
565
|
|
|
559
566
|
this._traceFrame( { liveDenoise: true } );
|
|
560
567
|
|
|
561
|
-
if ( this.stages.pathTracer.isComplete && this.completion.markComplete() )
|
|
562
|
-
|
|
563
|
-
this._renderCompleted();
|
|
564
|
-
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...this._completionInfo() } );
|
|
565
|
-
|
|
566
|
-
}
|
|
568
|
+
if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) this._announceComplete();
|
|
567
569
|
|
|
568
570
|
}
|
|
569
571
|
|
|
@@ -572,6 +574,25 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
572
574
|
|
|
573
575
|
}
|
|
574
576
|
|
|
577
|
+
// The closing passes and RENDER_COMPLETE wait for _finishImage(), so both see the finished image's exposure.
|
|
578
|
+
_announceComplete() {
|
|
579
|
+
|
|
580
|
+
const info = this._completionInfo();
|
|
581
|
+
const restarts = this.stages.pathTracer.resetCount;
|
|
582
|
+
const announce = () => {
|
|
583
|
+
|
|
584
|
+
if ( ! this.completion.renderCompleteDispatched || this.stages.pathTracer?.resetCount !== restarts ) return;
|
|
585
|
+
this._renderCompleted();
|
|
586
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
|
|
587
|
+
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const finishing = this._finishImage();
|
|
591
|
+
if ( finishing ) finishing.then( announce, announce );
|
|
592
|
+
else announce();
|
|
593
|
+
|
|
594
|
+
}
|
|
595
|
+
|
|
575
596
|
// One frame of a render and its bookkeeping, shared by animate() and renderUntilComplete().
|
|
576
597
|
_traceFrame( { liveDenoise = false } = {} ) {
|
|
577
598
|
|
|
@@ -2499,6 +2520,7 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2499
2520
|
|
|
2500
2521
|
}
|
|
2501
2522
|
|
|
2523
|
+
await this._finishImage();
|
|
2502
2524
|
return stage.frameCount;
|
|
2503
2525
|
|
|
2504
2526
|
}
|
|
@@ -2576,6 +2598,8 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2576
2598
|
}
|
|
2577
2599
|
|
|
2578
2600
|
const info = this._completionInfo();
|
|
2601
|
+
// Before it is announced: a listener reading pixels sees the finished image's exposure.
|
|
2602
|
+
await this._finishImage();
|
|
2579
2603
|
if ( this.completion.markComplete() ) this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
|
|
2580
2604
|
|
|
2581
2605
|
const denoised = denoise ? await this._finalDenoise() : false;
|
|
@@ -2735,12 +2759,14 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2735
2759
|
toneMapping: this.renderer.toneMapping,
|
|
2736
2760
|
saturation: this.settings.get( 'saturation' ) ?? 1,
|
|
2737
2761
|
preserveAlpha,
|
|
2762
|
+
gain: this._displayGain(),
|
|
2738
2763
|
};
|
|
2739
2764
|
|
|
2740
2765
|
const gpu = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone );
|
|
2766
|
+
const fn = ! gpu && tone.gain ? await tone.gain.cpu() : null;
|
|
2741
2767
|
const data = gpu ?? toneMapToRGBA8( shown
|
|
2742
2768
|
? await this._readTexture( shown.texture, width, height )
|
|
2743
|
-
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), tone );
|
|
2769
|
+
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), { ...tone, pixelGain: fn && { fn, width, height } } );
|
|
2744
2770
|
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation', toneMappedOn: gpu ? 'gpu' : 'cpu' };
|
|
2745
2771
|
|
|
2746
2772
|
}
|
|
@@ -3982,6 +4008,20 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3982
4008
|
/** Once, on the loop frame the render completes. */
|
|
3983
4009
|
_renderCompleted() {}
|
|
3984
4010
|
|
|
4011
|
+
/** True keeps the loop running over a finished render, to redraw what is still changing. */
|
|
4012
|
+
_settling() {
|
|
4013
|
+
|
|
4014
|
+
return false;
|
|
4015
|
+
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
/** A render has its samples, before any closing pass or announcement: a promise to wait for, or null. */
|
|
4019
|
+
_finishImage() {
|
|
4020
|
+
|
|
4021
|
+
return null;
|
|
4022
|
+
|
|
4023
|
+
}
|
|
4024
|
+
|
|
3985
4025
|
/** Draws what sits over the image. Here only brings the light scene's matrices up to date. */
|
|
3986
4026
|
_renderHelperOverlay() {
|
|
3987
4027
|
|
|
@@ -4032,6 +4072,13 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
4032
4072
|
|
|
4033
4073
|
}
|
|
4034
4074
|
|
|
4075
|
+
/** A per-pixel gain the shown picture carries before the view (the compositor's), for tone-mapped readbacks; or null. */
|
|
4076
|
+
_displayGain() {
|
|
4077
|
+
|
|
4078
|
+
return null;
|
|
4079
|
+
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4035
4082
|
/** Whether a 'display' read should find a processed picture. */
|
|
4036
4083
|
_denoiserInUse() {
|
|
4037
4084
|
|
package/src/RenderSettings.js
CHANGED
|
@@ -258,7 +258,7 @@ export class RenderSettings extends EventDispatcher {
|
|
|
258
258
|
|
|
259
259
|
handleRenderLimitMode: ( value ) => {
|
|
260
260
|
|
|
261
|
-
stages.pathTracer?.setRenderLimitMode?.( value );
|
|
261
|
+
stages.pathTracer?.setRenderLimitMode?.( value, this.get( 'renderTimeLimit' ) );
|
|
262
262
|
reconcileCompletion?.();
|
|
263
263
|
|
|
264
264
|
},
|
|
@@ -271,8 +271,10 @@ export class RenderSettings extends EventDispatcher {
|
|
|
271
271
|
|
|
272
272
|
},
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
// Arming or disarming the deadline lifts or restores the ceiling in 'timeOnly'.
|
|
275
|
+
handleRenderTimeLimit: ( value ) => {
|
|
275
276
|
|
|
277
|
+
stages.pathTracer?.setRenderLimitMode?.( this.get( 'renderLimitMode' ), value );
|
|
276
278
|
reconcileCompletion?.();
|
|
277
279
|
|
|
278
280
|
},
|