rayzee 11.2.1 → 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.
Files changed (76) hide show
  1. package/README.md +91 -34
  2. package/dist/ArchiveImporter-Cgyx5AF7.js +7860 -0
  3. package/dist/BidirectionalIntegrator-BKF-cwH2.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
  6. package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-DV5TtURP.js} +11 -5
  7. package/dist/{PathTracer-D6nbknj5.js → PathTracer-D3MvEaZK.js} +4384 -4618
  8. package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-CY4jiz6a.js} +2 -2
  9. package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-D23m6NqG.js} +486 -391
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
  12. package/dist/addons/archives.es.js +1 -1
  13. package/dist/addons/bidirectional.es.js +1 -1
  14. package/dist/addons/physical-sky.es.js +1 -1
  15. package/dist/rayzee-core.es.js +7 -7
  16. package/dist/rayzee.es.js +1327 -838
  17. package/dist/rayzee.umd.js +146 -134
  18. package/dist/sceneCachePolicy-AKOEu2lf.js +1602 -0
  19. package/package.json +1 -1
  20. package/src/EngineDefaults.js +2 -2
  21. package/src/Passes/AIUpscaler.js +2 -0
  22. package/src/PathTracerApp.js +126 -19
  23. package/src/Pipeline/CompletionTracker.js +7 -3
  24. package/src/Processor/ArchiveImporter.js +173 -94
  25. package/src/Processor/ArchiveReader.js +20 -23
  26. package/src/Processor/AssetLoader.js +48 -1
  27. package/src/Processor/BVHRefitter.js +109 -13
  28. package/src/Processor/BufferLayout.js +115 -0
  29. package/src/Processor/ChunkedRecords.js +3 -2
  30. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  31. package/src/Processor/GeometryExtractor.js +189 -79
  32. package/src/Processor/HostMemory.js +39 -9
  33. package/src/Processor/InstanceTable.js +714 -42
  34. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  35. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  36. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  37. package/src/Processor/PackedRayBuffer.js +5 -2
  38. package/src/Processor/SceneProcessor.js +747 -141
  39. package/src/Processor/TLASBuilder.js +32 -17
  40. package/src/Processor/ToneMapCPU.js +16 -4
  41. package/src/Processor/ToneMapGPU.js +30 -14
  42. package/src/Processor/USD/USDLayer.js +1149 -0
  43. package/src/Processor/USD/USDScene.js +1655 -0
  44. package/src/Processor/USD/USDStage.js +682 -0
  45. package/src/Processor/USD/USDText.js +674 -0
  46. package/src/Processor/USD/index.js +138 -0
  47. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  48. package/src/RayzeeRenderer.js +221 -33
  49. package/src/RenderSettings.js +4 -2
  50. package/src/Stages/AutoExposure.js +484 -494
  51. package/src/Stages/Compositor.js +27 -6
  52. package/src/Stages/LocalExposure.js +546 -0
  53. package/src/Stages/NormalDepth.js +7 -12
  54. package/src/Stages/PathTracer.js +18 -21
  55. package/src/Stages/PathTracerStage.js +282 -103
  56. package/src/Storage/GeometrySpill.js +46 -14
  57. package/src/Storage/SceneGraphCodec.js +17 -3
  58. package/src/Storage/SpillStore.js +9 -0
  59. package/src/TSL/BVHTraversal.js +183 -7
  60. package/src/TSL/Common.js +57 -13
  61. package/src/TSL/ShadeKernel.js +3 -2
  62. package/src/TSL/TextureSampling.js +11 -8
  63. package/src/TSL/patches.js +84 -0
  64. package/src/index.js +1 -0
  65. package/src/integrators/BidirectionalIntegrator.js +20 -2
  66. package/src/managers/DenoisingManager.js +15 -19
  67. package/src/managers/InteractionManager.js +19 -6
  68. package/src/managers/MaterialDataManager.js +22 -5
  69. package/src/managers/UniformManager.js +3 -0
  70. package/src/managers/VideoRenderManager.js +30 -4
  71. package/src/neural/NeuralRetouch.js +1 -0
  72. package/src/neural/NeuralSuperRes.js +6 -1
  73. package/dist/ArchiveImporter-D_x863yu.js +0 -5198
  74. package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
  75. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  76. package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
@@ -147,6 +147,90 @@ export function uploadStorageChunks( renderer, attr, chunks ) {
147
147
 
148
148
  }
149
149
 
150
+ /**
151
+ * GPU-only storage attributes standing for one array too large for a single binding: lane l lives in part
152
+ * ⌊l / lanesPerPart⌋, and a part holds whole records, so no record straddles two. Read through `splitStorage`
153
+ * (TSL/Common.js).
154
+ */
155
+ export class StorageParts {
156
+
157
+ /**
158
+ * @param {number} totalLanes
159
+ * @param {number} lanesPerRecord
160
+ * @param {number} maxPartBytes - at most maxStorageBufferBindingSize
161
+ * @param {Function} [LaneType]
162
+ * @param {number} [itemSize] - lanes an element: 4 for vec4 storage, 2 for vec2
163
+ */
164
+ constructor( totalLanes, lanesPerRecord, maxPartBytes, LaneType = Float32Array, itemSize = 4 ) {
165
+
166
+ this.LaneType = LaneType;
167
+ this.itemSize = itemSize;
168
+ this.lanesPerPart = Math.max( 1, Math.floor( maxPartBytes / ( lanesPerRecord * LaneType.BYTES_PER_ELEMENT ) ) ) * lanesPerRecord;
169
+ this.totalLanes = totalLanes;
170
+ this.attrs = [];
171
+ for ( let at = 0; at < totalLanes || this.attrs.length === 0; at += this.lanesPerPart ) {
172
+
173
+ const lanes = Math.max( lanesPerRecord, Math.min( this.lanesPerPart, totalLanes - at ) );
174
+ this.attrs.push( gpuOnlyStorageAttribute( lanes / itemSize, itemSize, LaneType ) );
175
+
176
+ }
177
+
178
+ }
179
+
180
+ /** Elements a full part holds. */
181
+ get elementsPerPart() {
182
+
183
+ return this.lanesPerPart / this.itemSize;
184
+
185
+ }
186
+
187
+ /** Creates every part's GPU buffer now. */
188
+ create( backend ) {
189
+
190
+ for ( const attr of this.attrs ) backend.createStorageAttribute( attr );
191
+
192
+ }
193
+
194
+ /** Writes `count` lanes of `src` from `srcOffset` to lane `lane`, across a part boundary if need be. */
195
+ write( backend, lane, src, srcOffset, count ) {
196
+
197
+ const bytes = this.LaneType.BYTES_PER_ELEMENT;
198
+ while ( count > 0 ) {
199
+
200
+ const p = Math.floor( lane / this.lanesPerPart );
201
+ const local = lane - p * this.lanesPerPart;
202
+ const n = Math.min( count, this.lanesPerPart - local );
203
+ const attr = this.attrs[ p ];
204
+ backend.createStorageAttribute( attr );
205
+ backend.device.queue.writeBuffer( backend.get( attr ).buffer, local * bytes, src, srcOffset, n );
206
+ lane += n;
207
+ srcOffset += n;
208
+ count -= n;
209
+
210
+ }
211
+
212
+ }
213
+
214
+ /** Writes lanes [startLane, startLane + laneCount) of chunked `records`. */
215
+ writeRecords( backend, records, startLane, laneCount ) {
216
+
217
+ const lanesPerChunk = records.recordsPerChunk * records.lanesPerRecord;
218
+ for ( let lane = startLane, end = startLane + laneCount; lane < end; ) {
219
+
220
+ const ci = Math.floor( lane / lanesPerChunk );
221
+ const chunk = records.chunks[ ci ];
222
+ if ( ! chunk ) throw new Error( `chunk ${ci} is on disk and was never uploaded` );
223
+ const local = lane - ci * lanesPerChunk;
224
+ const n = Math.min( chunk.length - local, end - lane );
225
+ this.write( backend, lane, chunk, local, n );
226
+ lane += n;
227
+
228
+ }
229
+
230
+ }
231
+
232
+ }
233
+
150
234
  /**
151
235
  * Re-upload one lane range of a chunked storage attribute, spanning chunks as needed.
152
236
  *
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';
@@ -13,7 +13,7 @@ import { LIGHT_FLOATS } from '../managers/UniformManager.js';
13
13
  import { HIT_STRIDE_BIDIRECTIONAL, LIGHT_VERTEX_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
14
14
  import { COUNTER, ENERGY_SCALE } from '../Processor/QueueManager.js';
15
15
  import { CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
16
- import { BVH_MAX_INDEX } from '../Processor/BufferLayout.js';
16
+ import { BVH_MAX_INDEX, BVH_LEAF_MARKERS, CLUSTER_COUNT_SHIFT, CLUSTER_COPY_HIDDEN } from '../Processor/BufferLayout.js';
17
17
  import { BOUNCE_KERNELS, LIST_WG_SIZE } from '../Stages/PathTracer.js';
18
18
  import { buildLightGenerateKernel, LIGHT_GENERATE_WG_SIZE } from '../TSL/LightGenerateKernel.js';
19
19
  import { buildGuideKernel, buildGuideClearKernel, guidedDiscPdf, recordEscape, GUIDE_BINS, GUIDE_ROW_STRIDE, GUIDE_TEXTURE_WIDTH } from '../TSL/LightGuide.js';
@@ -652,6 +652,14 @@ export class BidirectionalIntegrator {
652
652
  const records = this.pt._bvhRecords;
653
653
  const flat = records ? null : this.pt.bvhStorageAttr?.array;
654
654
  if ( ! records && ! ( flat?.length >= 16 ) ) return null;
655
+ // A spilled TLAS comes back for the next frame's disc.
656
+ const blasBase = this.pt._instanceTable?.blasBase ?? 1;
657
+ if ( records && ! records.isResident( 0, blasBase ) ) {
658
+
659
+ this.pt._whenTLASResident( () => {} );
660
+ return null;
661
+
662
+ }
655
663
 
656
664
  const view = ( f ) => {
657
665
 
@@ -668,6 +676,16 @@ export class BidirectionalIntegrator {
668
676
 
669
677
  };
670
678
 
679
+ // A copy cluster is visible while any of its copies is (BufferLayout CLUSTER_LEAF).
680
+ const leafVisible = ( n ) => {
681
+
682
+ if ( n.u[ n.o + 3 ] !== BVH_LEAF_MARKERS.CLUSTER_LEAF ) return n.f[ n.o + 2 ] !== 0;
683
+ const count = ( ( n.u[ n.o ] >>> CLUSTER_COUNT_SHIFT ) & 3 ) + 1;
684
+ for ( let k = 0; k < count; k ++ ) if ( ! ( n.u[ n.o + 12 + k ] & CLUSTER_COPY_HIDDEN ) ) return true;
685
+ return false;
686
+
687
+ };
688
+
671
689
  const root = node( 0 );
672
690
  if ( root.u[ root.o + 3 ] >= BVH_MAX_INDEX ) return null;
673
691
  // A tree that reaches a node twice is malformed; no search visits more nodes than the tree holds.
@@ -699,7 +717,7 @@ export class BidirectionalIntegrator {
699
717
  const n = node( index );
700
718
  if ( n.u[ n.o + 3 ] >= BVH_MAX_INDEX ) {
701
719
 
702
- if ( n.f[ n.o + 2 ] !== 0 ) best = s;
720
+ if ( leafVisible( n ) ) best = s;
703
721
 
704
722
  } else push( n );
705
723
 
@@ -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;
@@ -409,6 +411,7 @@ export class DenoisingManager extends EventDispatcher {
409
411
  getExposure: () => this._getEffectiveExposure(),
410
412
  getToneMapping: () => this._getToneMapping(),
411
413
  getSaturation: () => this._getSaturation(),
414
+ getDisplayGain: () => this._getDisplayGain?.() ?? null,
412
415
  } );
413
416
 
414
417
  this.upscaler.enabled = DENOISER_DEFAULTS.enableUpscaler || false;
@@ -546,21 +549,11 @@ export class DenoisingManager extends EventDispatcher {
546
549
 
547
550
  /**
548
551
  * @param {boolean} enabled
549
- * @param {number} manualExposure - Restored to renderer.toneMappingExposure when disabling.
552
+ * @param {number} manualExposure - the compensation while on, the exposure again once off
550
553
  */
551
554
  setAutoExposureEnabled( enabled, manualExposure ) {
552
555
 
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
- }
556
+ this._stages.autoExposure?.setEnabled( enabled, manualExposure );
564
557
 
565
558
  }
566
559
 
@@ -988,10 +981,10 @@ export class DenoisingManager extends EventDispatcher {
988
981
  * @param {Float32Array} world - the placement matrix pool
989
982
  * @param {number} offset - where this placement's matrix starts in it
990
983
  */
991
- notePlacementMoving( leaf, world, offset ) {
984
+ notePlacementMoving( leaf, matrixOf ) {
992
985
 
993
986
  if ( leaf < 0 || ! this.historyActive || this._movedPlacements.has( leaf ) ) return;
994
- this._movedPlacements.set( leaf, { world, offset, prev: world.slice( offset, offset + 16 ) } );
987
+ this._movedPlacements.set( leaf, { matrixOf, prev: matrixOf().slice() } );
995
988
 
996
989
  }
997
990
 
@@ -1013,8 +1006,8 @@ export class DenoisingManager extends EventDispatcher {
1013
1006
  const leaves = [ ...moved.keys() ].sort( ( a, b ) => a - b );
1014
1007
  for ( let i = 0; i < leaves.length; i ++ ) {
1015
1008
 
1016
- const { world, offset, prev } = moved.get( leaves[ i ] );
1017
- this._matB.fromArray( world, offset ).invert();
1009
+ const { matrixOf, prev } = moved.get( leaves[ i ] );
1010
+ this._matB.fromArray( matrixOf() ).invert();
1018
1011
  this._matA.fromArray( prev ).multiply( this._matB ).toArray( upload.toPrev, i * 16 );
1019
1012
  upload.leaves[ i ] = leaves[ i ];
1020
1013
 
@@ -1246,6 +1239,7 @@ export class DenoisingManager extends EventDispatcher {
1246
1239
  exposure: this._getEffectiveExposure(),
1247
1240
  toneMapping: this._getToneMapping(),
1248
1241
  saturation: this._getSaturation?.() ?? 1,
1242
+ gain: this._getDisplayGain?.() ?? null,
1249
1243
  };
1250
1244
 
1251
1245
  // Scene-referred throughout. Exposure reaches the detail pass through its own `paper_white`
@@ -1561,6 +1555,9 @@ export class DenoisingManager extends EventDispatcher {
1561
1555
  this._onReset = null;
1562
1556
  this._onPostProcessRefresh = null;
1563
1557
  this._onDisplayRefresh = null;
1558
+ this._getExposure = null;
1559
+ this._getSaturation = null;
1560
+ this._getDisplayGain = null;
1564
1561
  this._issues = null;
1565
1562
 
1566
1563
  if ( this.upscalerCanvas?.parentNode ) {
@@ -1764,13 +1761,12 @@ export class DenoisingManager extends EventDispatcher {
1764
1761
  // ── Convenience (match DenoisingAPI names with reset) ────────
1765
1762
 
1766
1763
  /**
1767
- * Enables or disables auto-exposure (convenience wrapper).
1764
+ * Enables or disables auto-exposure. A finished render is metered as it stands, not restarted.
1768
1765
  * @param {boolean} enabled
1769
1766
  */
1770
1767
  setAutoExposure( enabled ) {
1771
1768
 
1772
1769
  this.setAutoExposureEnabled( enabled, this._getExposure() );
1773
- this._onReset?.();
1774
1770
 
1775
1771
  }
1776
1772
 
@@ -38,12 +38,13 @@ export function focusDistanceOfHit( hit, camera, panorama = false ) {
38
38
  */
39
39
  export class InteractionManager extends EventDispatcher {
40
40
 
41
- constructor( { scene, camera, canvas, assetLoader, pathTracer, floorPlane } ) {
41
+ constructor( { scene, camera, canvas, assetLoader, pathTracer, floorPlane, isGeometryOnDisk = null } ) {
42
42
 
43
43
  super();
44
44
 
45
45
  // Core dependencies
46
46
  this.scene = scene;
47
+ this.isGeometryOnDisk = isGeometryOnDisk;
47
48
  this.camera = camera;
48
49
  this.canvas = canvas;
49
50
  this.assetLoader = assetLoader;
@@ -125,7 +126,8 @@ export class InteractionManager extends EventDispatcher {
125
126
  select( object ) {
126
127
 
127
128
  const outline = this._overlayManager?.getHelper( 'outline' );
128
- if ( outline ) outline.setSelectedObjects( object ? [ object ] : [] );
129
+ // A spilled model's geometry is on disk: there is nothing to draw the outline from.
130
+ if ( outline ) outline.setSelectedObjects( object && ! this.isGeometryOnDisk?.() ? [ object ] : [] );
129
131
 
130
132
  this.selectedObject = object || null;
131
133
 
@@ -221,7 +223,7 @@ export class InteractionManager extends EventDispatcher {
221
223
  const mouseCoords = this.getMouseCoordinates( event );
222
224
  this.raycaster.setFromCamera( mouseCoords, this.camera );
223
225
 
224
- const intersects = this.raycaster.intersectObjects( this.scene.children, true );
226
+ const intersects = this._intersectScene();
225
227
 
226
228
  if ( intersects.length > 0 ) {
227
229
 
@@ -490,7 +492,7 @@ export class InteractionManager extends EventDispatcher {
490
492
  const mouseCoords = this.getMouseCoordinates( event );
491
493
  this.raycaster.setFromCamera( mouseCoords, this.camera );
492
494
 
493
- const intersects = this.raycaster.intersectObjects( this.scene.children, true );
495
+ const intersects = this._intersectScene();
494
496
  const validIntersects = this.filterValidIntersects( intersects );
495
497
  const lightHit = this._pickLightHelper();
496
498
 
@@ -576,7 +578,7 @@ export class InteractionManager extends EventDispatcher {
576
578
  const mouseCoords = this.getMouseCoordinates( event );
577
579
  this.raycaster.setFromCamera( mouseCoords, this.camera );
578
580
 
579
- const intersects = this.raycaster.intersectObjects( this.scene.children, true );
581
+ const intersects = this._intersectScene();
580
582
  const validIntersects = this.filterValidIntersects( intersects );
581
583
 
582
584
  if ( validIntersects.length > 0 ) {
@@ -695,11 +697,22 @@ export class InteractionManager extends EventDispatcher {
695
697
 
696
698
  }
697
699
 
700
+ /**
701
+ * The scene under the raycaster. Nothing while a spilled model's geometry is on disk: its arrays are empty, and
702
+ * testing tens of millions of empty copies held the page for seconds a pick.
703
+ * @private
704
+ */
705
+ _intersectScene() {
706
+
707
+ return this.isGeometryOnDisk?.() ? [] : this.raycaster.intersectObjects( this.scene.children, true );
708
+
709
+ }
710
+
698
711
  /** The nearest scene surface under an NDC point, skipping the floor and helpers. */
699
712
  pickSurface( ndcX, ndcY ) {
700
713
 
701
714
  this.raycaster.setFromCamera( { x: ndcX, y: ndcY }, this.camera );
702
- return this.filterValidIntersects( this.raycaster.intersectObjects( this.scene.children, true ) )[ 0 ] ?? null;
715
+ return this.filterValidIntersects( this._intersectScene() )[ 0 ] ?? null;
703
716
 
704
717
  }
705
718
 
@@ -173,8 +173,17 @@ export class MaterialDataManager {
173
173
  this._convertColorsToWorkingSpace( matImageData );
174
174
 
175
175
  const vec4Count = matImageData.length / 4;
176
+ this.materialCount = Math.floor( vec4Count / PIXELS_PER_MATERIAL );
177
+
178
+ // The owning stage keeps materials at the front of its scene data buffer, the light data behind them: one
179
+ // binding for both, which the shading kernel's ten could not spare.
180
+ const shared = this.callbacks.adoptMaterials?.( matImageData );
181
+ if ( shared ) {
176
182
 
177
- if ( this.materialStorageNode ) {
183
+ this.materialStorageAttr = shared.attr;
184
+ this.materialStorageNode = shared.node;
185
+
186
+ } else if ( this.materialStorageNode ) {
178
187
 
179
188
  this.materialStorageAttr = new StorageInstancedBufferAttribute( matImageData, 4 );
180
189
  this.materialStorageNode.value = this.materialStorageAttr;
@@ -187,7 +196,6 @@ export class MaterialDataManager {
187
196
 
188
197
  }
189
198
 
190
- this.materialCount = Math.floor( vec4Count / PIXELS_PER_MATERIAL );
191
199
  this._sources = [ ...sources ];
192
200
  this._hostSet = [];
193
201
  this.callbacks.onMaterialFeaturesChanged?.();
@@ -195,6 +203,15 @@ export class MaterialDataManager {
195
203
 
196
204
  }
197
205
 
206
+ /** Sends the materials to the GPU: only their region when the buffer carries the light data too. @private */
207
+ _uploadMaterials() {
208
+
209
+ const attr = this.materialStorageAttr;
210
+ if ( this.callbacks.adoptMaterials ) attr.addUpdateRange( 0, this.materialCount * PIXELS_PER_MATERIAL * 4 );
211
+ attr.needsUpdate = true;
212
+
213
+ }
214
+
198
215
  /**
199
216
  * Get the material storage attribute (for dependent stages).
200
217
  * @returns {StorageInstancedBufferAttribute|null}
@@ -538,7 +555,7 @@ export class MaterialDataManager {
538
555
 
539
556
  }
540
557
 
541
- this.materialStorageAttr.needsUpdate = true;
558
+ this._uploadMaterials();
542
559
  ( this._hostSet[ materialIndex ] ??= new Map() ).set( property, toPortable( value ) );
543
560
 
544
561
  // Recompute triangle-data opaque-blocker flag when any input to it changes.
@@ -588,7 +605,7 @@ export class MaterialDataManager {
588
605
  this._recomputeOpaqueBlockerForMaterial( materialIndex );
589
606
  this.callbacks.onMaterialFeaturesChanged?.();
590
607
 
591
- this.materialStorageAttr.needsUpdate = true;
608
+ this._uploadMaterials();
592
609
  this._notifyReset();
593
610
 
594
611
  }
@@ -668,7 +685,7 @@ export class MaterialDataManager {
668
685
 
669
686
  }
670
687
 
671
- this.materialStorageAttr.needsUpdate = true;
688
+ this._uploadMaterials();
672
689
  this._notifyReset();
673
690
 
674
691
  }
@@ -333,6 +333,9 @@ export class UniformManager {
333
333
  u( 'emissiveTriangleCount', 0, 'int' );
334
334
  u( 'emissiveTotalPower', 0.0, 'float' );
335
335
  u( 'lightBVHNodeCount', 0, 'int' );
336
+ // Where the light data starts in the scene data buffer, after the materials (vec4 elements). Every light read
337
+ // adds it; the offsets below are within the light data.
338
+ u( 'lightVec4Offset', 0, 'int' );
336
339
  // Offset (in vec4 elements) within the packed light buffer where emissive
337
340
  // triangle data starts. Equals lightBVHNodeCount * LBVH_STRIDE; computed on upload.
338
341
  u( 'emissiveVec4Offset', 0, 'int' );
@@ -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