rayzee 11.3.0 → 11.5.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.
Files changed (35) hide show
  1. package/README.md +33 -5
  2. package/dist/{ArchiveImporter-D735xX-4.js → ArchiveImporter-29Nl1567.js} +2 -2
  3. package/dist/{BidirectionalIntegrator-M89c0AlP.js → BidirectionalIntegrator-CJay8tXK.js} +2 -2
  4. package/dist/{NeuralRetouch-DiBH63kz.js → NeuralRetouch-KqGVPV7u.js} +4 -3
  5. package/dist/{NeuralSuperRes-CZNQP13W.js → NeuralSuperRes-BHxhYzlD.js} +11 -5
  6. package/dist/{PathTracer-BXCCVrPK.js → PathTracer-Cldt0mXi.js} +19 -12
  7. package/dist/{PhysicalSky-DqmaS0lb.js → PhysicalSky-CUWrTcmT.js} +1 -1
  8. package/dist/{RayzeeRenderer-DUSMFhmc.js → RayzeeRenderer-Dd-Dit0v.js} +313 -281
  9. package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-sAs8wdgI.js} +44 -22
  10. package/dist/addons/archives.es.js +1 -1
  11. package/dist/addons/bidirectional.es.js +1 -1
  12. package/dist/addons/physical-sky.es.js +1 -1
  13. package/dist/rayzee-core.es.js +3 -3
  14. package/dist/rayzee.es.js +1428 -902
  15. package/dist/rayzee.umd.js +124 -113
  16. package/dist/{sceneCachePolicy-BAib5fCp.js → sceneCachePolicy--ta6moJu.js} +1 -1
  17. package/package.json +1 -1
  18. package/src/EngineDefaults.js +2 -2
  19. package/src/Passes/AIUpscaler.js +17 -12
  20. package/src/PathTracerApp.js +122 -16
  21. package/src/Pipeline/CompletionTracker.js +7 -3
  22. package/src/Processor/ToneMapCPU.js +16 -4
  23. package/src/Processor/ToneMapGPU.js +49 -14
  24. package/src/RayzeeRenderer.js +54 -7
  25. package/src/RenderSettings.js +4 -2
  26. package/src/Stages/AutoExposure.js +484 -494
  27. package/src/Stages/Compositor.js +27 -6
  28. package/src/Stages/LocalExposure.js +569 -0
  29. package/src/Stages/PathTracer.js +6 -4
  30. package/src/Stages/PathTracerStage.js +9 -7
  31. package/src/index.js +1 -0
  32. package/src/managers/DenoisingManager.js +93 -34
  33. package/src/managers/VideoRenderManager.js +30 -4
  34. package/src/neural/NeuralRetouch.js +1 -0
  35. package/src/neural/NeuralSuperRes.js +6 -1
package/src/index.js CHANGED
@@ -68,6 +68,7 @@ export {
68
68
  } from './Stages/DenoiserSettings.js';
69
69
  export { SKY_DEFAULTS, DEFAULT_SUN_PATH } from './managers/EnvironmentManager.js';
70
70
  export { AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
71
+ export { LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
71
72
  export { AUTO_FOCUS_DEFAULTS } from './managers/CameraManager.js';
72
73
  export { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS } from './Processor/BufferLayout.js';
73
74
  export { sunPosition, timeForSunElevation, dayOfYearForMonth } from './Processor/SunPosition.js';
@@ -104,9 +104,10 @@ export class DenoisingManager extends EventDispatcher {
104
104
  * @param {import('../Pipeline/RenderPipeline.js').RenderPipeline} params.pipeline
105
105
  * @param {Function} params.getExposure - () => current exposure value
106
106
  * @param {Function} params.getSaturation - () => current saturation value
107
+ * @param {Function} [params.getDisplayGain] - () => the shown picture's per-pixel gain (PackedToneMapper `gain`)
107
108
  * @param {import('../EngineIssues.js').IssueLog} [params.issues]
108
109
  */
109
- constructor( { renderer, mainCanvas, stages, pipeline, getExposure, getSaturation, issues = null } ) {
110
+ constructor( { renderer, mainCanvas, stages, pipeline, getExposure, getSaturation, getDisplayGain = null, issues = null } ) {
110
111
 
111
112
  super();
112
113
 
@@ -121,6 +122,7 @@ export class DenoisingManager extends EventDispatcher {
121
122
 
122
123
  this._getExposure = getExposure;
123
124
  this._getSaturation = getSaturation;
125
+ this._getDisplayGain = getDisplayGain;
124
126
 
125
127
  this.denoiser = null;
126
128
  this.upscaler = null;
@@ -135,6 +137,11 @@ export class DenoisingManager extends EventDispatcher {
135
137
  this._retouch = null;
136
138
  this._superResModule = null;
137
139
  this._neuralPostBusy = false;
140
+ // The finished render's post passes: its guard, the run in flight, a queued rerun, and a cancel.
141
+ this._postIsStillComplete = null;
142
+ this._postRun = null;
143
+ this._postRedo = null;
144
+ this._postCancelled = false;
138
145
 
139
146
  // The tier the finished image uses. The loaded tier is not always this one: while the
140
147
  // image is still accumulating we run a cheaper model (see previewQuality).
@@ -409,6 +416,7 @@ export class DenoisingManager extends EventDispatcher {
409
416
  getExposure: () => this._getEffectiveExposure(),
410
417
  getToneMapping: () => this._getToneMapping(),
411
418
  getSaturation: () => this._getSaturation(),
419
+ getDisplayGain: () => this._getDisplayGain?.() ?? null,
412
420
  } );
413
421
 
414
422
  this.upscaler.enabled = DENOISER_DEFAULTS.enableUpscaler || false;
@@ -546,21 +554,11 @@ export class DenoisingManager extends EventDispatcher {
546
554
 
547
555
  /**
548
556
  * @param {boolean} enabled
549
- * @param {number} manualExposure - Restored to renderer.toneMappingExposure when disabling.
557
+ * @param {number} manualExposure - the compensation while on, the exposure again once off
550
558
  */
551
559
  setAutoExposureEnabled( enabled, manualExposure ) {
552
560
 
553
- const s = this._stages;
554
- if ( ! s.autoExposure ) return;
555
-
556
- s.autoExposure.enabled = enabled;
557
-
558
- // AutoExposure overwrites renderer.toneMappingExposure each frame; restore manual on disable.
559
- if ( ! enabled && this.renderer ) {
560
-
561
- this.renderer.toneMappingExposure = manualExposure;
562
-
563
- }
561
+ this._stages.autoExposure?.setEnabled( enabled, manualExposure );
564
562
 
565
563
  }
566
564
 
@@ -1155,6 +1153,7 @@ export class DenoisingManager extends EventDispatcher {
1155
1153
  /** Turns the neural-rendering (detail) pass on or off, and sets its appearance controls. */
1156
1154
  setNeuralRendering( enabled, settings = null ) {
1157
1155
 
1156
+ const changed = this.neuralRendering !== !! enabled;
1158
1157
  this.neuralRendering = !! enabled;
1159
1158
  if ( settings ) this.neuralRenderingSettings = { ...this.neuralRenderingSettings, ...settings };
1160
1159
  if ( ! this.neuralRendering ) {
@@ -1164,6 +1163,8 @@ export class DenoisingManager extends EventDispatcher {
1164
1163
 
1165
1164
  }
1166
1165
 
1166
+ if ( changed ) this._redoPostPasses();
1167
+
1167
1168
  }
1168
1169
 
1169
1170
  _releaseNeuralRetouch() {
@@ -1174,14 +1175,8 @@ export class DenoisingManager extends EventDispatcher {
1174
1175
  }
1175
1176
 
1176
1177
  /**
1177
- * The neural post-chain over the picture the closing denoise just produced: super resolution
1178
- * first, then the detail pass, either or both.
1179
- *
1180
- * Order is forced by the models. Super resolution hands back light that can be fed onward; the
1181
- * detail pass writes an `rgba8unorm` texture with no `COPY_SRC`, so nothing can read its result
1182
- * and it has to be last — and it presents in its own display space rather than the engine's tone
1183
- * curve. Which shape super resolution returns therefore depends on what follows it: packed
1184
- * halves when the detail pass is next, display bytes when it is the end of the chain.
1178
+ * The post-chain over the picture the closing denoise just produced: the detail pass at render size,
1179
+ * then the chosen upscaler (neural super resolution or the ONNX one), either or both.
1185
1180
  *
1186
1181
  * Deliberately fire-and-forget: the render is already finished and a failure must not break it.
1187
1182
  */
@@ -1189,6 +1184,7 @@ export class DenoisingManager extends EventDispatcher {
1189
1184
 
1190
1185
  const wantSR = this.upscaler?.enabled && this.upscalerBackend === 'neural';
1191
1186
  const wantNR = this.neuralRendering;
1187
+ const wantESRGAN = this.upscaler?.enabled && ! wantSR;
1192
1188
  if ( ! wantSR && ! wantNR ) return;
1193
1189
 
1194
1190
  // A second completion can land while the first pass is still in flight — the models are
@@ -1237,7 +1233,12 @@ export class DenoisingManager extends EventDispatcher {
1237
1233
 
1238
1234
  }
1239
1235
 
1240
- if ( ! wantSR && ! runNR ) return;
1236
+ if ( ! wantSR && ! runNR ) {
1237
+
1238
+ if ( wantESRGAN && ! this._postCancelled ) await this.upscaler.start();
1239
+ return;
1240
+
1241
+ }
1241
1242
 
1242
1243
  // Must be the effective value, not the settings one: AutoExposure overwrites
1243
1244
  // `renderer.toneMappingExposure` every frame and never touches settings, so reading
@@ -1246,6 +1247,7 @@ export class DenoisingManager extends EventDispatcher {
1246
1247
  exposure: this._getEffectiveExposure(),
1247
1248
  toneMapping: this._getToneMapping(),
1248
1249
  saturation: this._getSaturation?.() ?? 1,
1250
+ gain: this._getDisplayGain?.() ?? null,
1249
1251
  };
1250
1252
 
1251
1253
  // Scene-referred throughout. Exposure reaches the detail pass through its own `paper_white`
@@ -1297,6 +1299,15 @@ export class DenoisingManager extends EventDispatcher {
1297
1299
 
1298
1300
  }
1299
1301
 
1302
+ if ( this._postRedo || this._postCancelled ) return;
1303
+
1304
+ if ( wantESRGAN ) {
1305
+
1306
+ await this.upscaler.start( new ImageData( image.rgba8, image.width, image.height ) );
1307
+ return;
1308
+
1309
+ }
1310
+
1300
1311
  sr.presentRGBA8( this.upscalerCanvas, image.rgba8, image.width, image.height );
1301
1312
 
1302
1313
  if ( ! isStillComplete() ) {
@@ -1366,10 +1377,61 @@ export class DenoisingManager extends EventDispatcher {
1366
1377
 
1367
1378
  }
1368
1379
 
1380
+ /** The neural chain or the ONNX upscaler over the finished render, as the switches ask. */
1381
+ _runPostPasses( isStillComplete ) {
1382
+
1383
+ this._postIsStillComplete = isStillComplete;
1384
+ this._postCancelled = false;
1385
+
1386
+ // The neural chain owns the overlay canvas when it runs, and hands its result to the ONNX upscaler.
1387
+ if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'neural' ) ) {
1388
+
1389
+ this._postRun = this._runNeuralPost( isStillComplete );
1390
+
1391
+ } else {
1392
+
1393
+ this._postRun = this.upscaler?.enabled ? this.upscaler.start() : null;
1394
+
1395
+ }
1396
+
1397
+ }
1398
+
1399
+ /** Runs the post passes again on a finished render whose switches changed, once the run in flight ends. */
1400
+ _redoPostPasses() {
1401
+
1402
+ const isStillComplete = this._postIsStillComplete;
1403
+ // No chain yet: the closing denoise is still running and the chain reads the switches when it starts.
1404
+ if ( ! isStillComplete?.() || this._postRedo ) return;
1405
+
1406
+ this.upscaler?.abort();
1407
+ this._postCancelled = false;
1408
+ this._postRedo = Promise.resolve( this._postRun ).catch( () => {} ).then( () => {
1409
+
1410
+ this._postRedo = null;
1411
+ if ( this._postCancelled || this._postIsStillComplete !== isStillComplete || ! isStillComplete() ) return;
1412
+ this._restoreRenderDisplay();
1413
+ this._runPostPasses( isStillComplete );
1414
+
1415
+ } );
1416
+
1417
+ }
1418
+
1419
+ /** Stops the post passes on the finished render, which stays on screen as rendered. The switches stay as they are. */
1420
+ cancelPostPasses() {
1421
+
1422
+ this._postCancelled = true;
1423
+ this.upscaler?.abort();
1424
+ this._restoreRenderDisplay();
1425
+ // A retouch in flight cannot be stopped, only discarded, and ends later.
1426
+ this.dispatchEvent( { type: EngineEvents.UPSCALING_END } );
1427
+
1428
+ }
1429
+
1369
1430
  onRenderComplete( { isStillComplete } ) {
1370
1431
 
1371
1432
  // Remove any stale completion-chain listener from a previous render cycle
1372
1433
  this._cleanupCompletionListener();
1434
+ this._postIsStillComplete = null;
1373
1435
 
1374
1436
  // What closes the render, if anything. A full pass at the chosen tier when that switch is
1375
1437
  // on; otherwise, if OIDN owns the live view, one last cheap refresh — the cadence's last
@@ -1389,16 +1451,7 @@ export class DenoisingManager extends EventDispatcher {
1389
1451
  // denoise. Ordered after `_denoiserEndHandler`, which publishes the picture.
1390
1452
  if ( closing ) this._onDisplayRefresh?.();
1391
1453
 
1392
- // One owner of the overlay canvas, so the neural chain and the ONNX upscaler are
1393
- // exclusive. The neural chain also runs on its own when only its detail pass is on.
1394
- if ( this.neuralRendering || ( this.upscaler?.enabled && this.upscalerBackend === 'neural' ) ) {
1395
-
1396
- this._runNeuralPost( isStillComplete );
1397
- return;
1398
-
1399
- }
1400
-
1401
- if ( this.upscaler?.enabled ) this.upscaler.start();
1454
+ this._runPostPasses( isStillComplete );
1402
1455
 
1403
1456
  };
1404
1457
 
@@ -1473,6 +1526,7 @@ export class DenoisingManager extends EventDispatcher {
1473
1526
 
1474
1527
  // Remove stale completion-chain listener before aborting
1475
1528
  this._cleanupCompletionListener();
1529
+ this._postIsStillComplete = null;
1476
1530
 
1477
1531
  // The 2D canvas is the upscaler's alone: a reset means its enlarged result no longer
1478
1532
  // describes anything, so the render canvas comes back.
@@ -1515,6 +1569,7 @@ export class DenoisingManager extends EventDispatcher {
1515
1569
 
1516
1570
  // Remove pending completion-chain listener
1517
1571
  this._cleanupCompletionListener();
1572
+ this._postIsStillComplete = null;
1518
1573
  // Owns a second GPUDevice plus 3 MB of weights, so it must not outlive the manager.
1519
1574
  this._releaseNeuralUpscaler();
1520
1575
  this._releaseNeuralRetouch();
@@ -1561,6 +1616,9 @@ export class DenoisingManager extends EventDispatcher {
1561
1616
  this._onReset = null;
1562
1617
  this._onPostProcessRefresh = null;
1563
1618
  this._onDisplayRefresh = null;
1619
+ this._getExposure = null;
1620
+ this._getSaturation = null;
1621
+ this._getDisplayGain = null;
1564
1622
  this._issues = null;
1565
1623
 
1566
1624
  if ( this.upscalerCanvas?.parentNode ) {
@@ -1740,10 +1798,12 @@ export class DenoisingManager extends EventDispatcher {
1740
1798
  /** Enables or disables the AI upscaler. */
1741
1799
  setUpscalerEnabled( enabled ) {
1742
1800
 
1801
+ const changed = !! this.upscaler && this.upscaler.enabled !== !! enabled;
1743
1802
  if ( this.upscaler ) this.upscaler.enabled = enabled;
1744
1803
  else if ( enabled ) this._recordNoOverlayCanvas( 'upscaler' );
1745
1804
  // The enlarged picture describes a setting that is no longer on.
1746
1805
  if ( ! enabled ) this._restoreRenderDisplay();
1806
+ if ( changed ) this._redoPostPasses();
1747
1807
 
1748
1808
  }
1749
1809
 
@@ -1764,13 +1824,12 @@ export class DenoisingManager extends EventDispatcher {
1764
1824
  // ── Convenience (match DenoisingAPI names with reset) ────────
1765
1825
 
1766
1826
  /**
1767
- * Enables or disables auto-exposure (convenience wrapper).
1827
+ * Enables or disables auto-exposure. A finished render is metered as it stands, not restarted.
1768
1828
  * @param {boolean} enabled
1769
1829
  */
1770
1830
  setAutoExposure( enabled ) {
1771
1831
 
1772
1832
  this.setAutoExposureEnabled( enabled, this._getExposure() );
1773
- this._onReset?.();
1774
1833
 
1775
1834
  }
1776
1835
 
@@ -87,8 +87,13 @@ export class VideoRenderManager {
87
87
  // Configure for high-quality offline rendering
88
88
  app.configureForMode( 'production' );
89
89
 
90
- // Override samples per frame
91
- app.settings.setMany( { maxSamples: samplesPerFrame }, { silent: true } );
90
+ // Auto exposure adapts in video time, once a frame, not to each pass's wall clock.
91
+ const autoExposure = app.stages.autoExposure?.enabled ? app.stages.autoExposure : null;
92
+ if ( autoExposure ) autoExposure.instant = false;
93
+
94
+ // Override samples per frame. 'frames': this loop never checks the deadline, so a 'timeOnly'
95
+ // ceiling of Infinity would never finish a frame.
96
+ app.settings.setMany( { maxSamples: samplesPerFrame, renderLimitMode: 'frames' }, { silent: true, reset: false } );
92
97
  app.stages.pathTracer?.updateCompletionThreshold?.();
93
98
 
94
99
  // Disable camera controls during render
@@ -131,11 +136,23 @@ export class VideoRenderManager {
131
136
  // kill it immediately so it doesn't race with our manual render loop
132
137
  app.stopAnimation();
133
138
 
139
+ // Before accumulating: the stage meters as it goes, and a reading of this frame's view filed under the last
140
+ // one read as a scene change, moving the room the whole way each frame.
141
+ if ( autoExposure ) app._noteExposureView();
142
+
134
143
  // 3. Accumulate samples until convergence
135
144
  await this._accumulateFrame( app );
136
145
 
137
146
  if ( this._cancelled ) break;
138
147
 
148
+ if ( autoExposure ) {
149
+
150
+ await autoExposure.meter( app.pipeline.context );
151
+ autoExposure.advance( i === Math.max( 0, startFrame ) ? Infinity : frameDuration );
152
+ app._presentDisplay();
153
+
154
+ }
155
+
139
156
  // 4. Denoise if enabled
140
157
  if ( enableOIDN && app.denoisingManager?.finalDenoise ) {
141
158
 
@@ -298,17 +315,19 @@ export class VideoRenderManager {
298
315
 
299
316
  const app = this._app;
300
317
  const effective = app.settings.getEffective();
301
- const keys = Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) );
318
+ const keys = [ ...Object.keys( modePresetSettings( PRODUCTION_RENDER_CONFIG ) ), 'renderLimitMode' ];
302
319
  if ( view ) keys.push( 'focusDistance' );
303
320
 
304
321
  return {
305
322
  view: view ? app.cameraManager.captureView() : null,
306
- // Everything configureForMode( 'production' ) overwrites, with who set it.
323
+ // Everything configureForMode( 'production' ) and the export overwrite, with who set it.
307
324
  settings: keys.map( key => [ key, effective[ key ] ] ),
308
325
  renderMode: app.stages.pathTracer?.renderMode?.value,
309
326
  controlsEnabled: app.cameraManager.controls?.enabled,
310
327
  oidnEnabled: app.denoisingManager?.finalDenoise,
311
328
  oidnQuality: app.denoisingManager?.oidnQuality,
329
+ instantExposure: app.stages.autoExposure?.instant,
330
+ holdExposureSamples: app.stages.autoExposure?.holdSamples,
312
331
  wasPlaying: app.animationManager?.isPlaying,
313
332
  pauseRendering: app.pauseRendering,
314
333
  };
@@ -347,6 +366,13 @@ export class VideoRenderManager {
347
366
  }
348
367
 
349
368
  app.pauseRendering = state.pauseRendering ?? false;
369
+ if ( app.stages.autoExposure ) {
370
+
371
+ app.stages.autoExposure.instant = state.instantExposure ?? false;
372
+ // configureForMode( 'production' ) set it, and nothing switches the mode back: left on, it froze the exposure while orbiting.
373
+ app.stages.autoExposure.holdSamples = state.holdExposureSamples ?? 0;
374
+
375
+ }
350
376
 
351
377
  if ( state.view ) app.cameraManager.applyPose( state.view );
352
378
 
@@ -389,6 +389,7 @@ export class NeuralRetouch {
389
389
  exposure: tone.exposure ?? 1,
390
390
  toneMapping: tone.toneMapping ?? 0,
391
391
  saturation: tone.saturation ?? 1,
392
+ gain: tone.gain,
392
393
  } );
393
394
 
394
395
  return { rgba8, width: w, height: h };
@@ -373,6 +373,7 @@ export class NeuralSuperRes {
373
373
  exposure: tone.exposure ?? 1,
374
374
  toneMapping: tone.toneMapping ?? 0,
375
375
  saturation: tone.saturation ?? 1,
376
+ gain: tone.gain,
376
377
  // The network emits its picture vertically flipped.
377
378
  flipY: true,
378
379
  } );
@@ -482,6 +483,7 @@ export async function renderUpscaled( app, {
482
483
  width: pathTracer?.width,
483
484
  height: pathTracer?.height,
484
485
  maxSamples: app.settings?.get?.( 'maxSamples' ),
486
+ renderLimitMode: app.settings?.get?.( 'renderLimitMode' ),
485
487
  finalDenoise: app.denoisingManager?.finalDenoise,
486
488
  paused: app.pauseRendering,
487
489
  };
@@ -512,7 +514,8 @@ export async function renderUpscaled( app, {
512
514
  await new Promise( r => setTimeout( r, 300 ) );
513
515
 
514
516
  app.denoisingManager.applyOIDNEnabled( true );
515
- app.settings.set( 'maxSamples', samples );
517
+ // 'frames': under 'timeOnly' the deadline, not `samples`, would end the render.
518
+ app.settings.setMany( { maxSamples: samples, renderLimitMode: 'frames' }, { reset: false } );
516
519
  const done = waitForDenoise( app, samples, timeoutMs );
517
520
  app.reset();
518
521
  await done;
@@ -529,6 +532,7 @@ export async function renderUpscaled( app, {
529
532
  exposure: app.renderer.toneMappingExposure,
530
533
  toneMapping: app.renderer.toneMapping,
531
534
  saturation: app.settings.get( 'saturation' ) ?? 1,
535
+ gain: app._displayGain?.() ?? null,
532
536
  };
533
537
 
534
538
  const { rgba8, width, height, ms, toneMs } = await sr.upscaleToRGBA8( source, tone );
@@ -550,6 +554,7 @@ export async function renderUpscaled( app, {
550
554
 
551
555
  if ( restore.width && restore.height ) app.setCanvasSize( restore.width, restore.height );
552
556
  if ( restore.maxSamples !== undefined ) app.settings?.set?.( 'maxSamples', restore.maxSamples );
557
+ if ( restore.renderLimitMode !== undefined ) app.settings?.set?.( 'renderLimitMode', restore.renderLimitMode, { reset: false } );
553
558
  if ( restore.finalDenoise !== undefined ) app.denoisingManager?.applyOIDNEnabled( restore.finalDenoise );
554
559
  app.pauseRendering = restore.paused;
555
560