rayzee 8.8.2 → 9.1.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 (104) hide show
  1. package/README.md +210 -20
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10795 -5324
  6. package/dist/rayzee.umd.js +279 -268
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +60 -76
  10. package/src/EngineEvents.js +7 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/Passes/OIDNTemporalHistory.js +25 -7
  16. package/src/PathTracerApp.js +313 -25
  17. package/src/Processor/ArchiveCache.js +120 -0
  18. package/src/Processor/ArchiveReader.js +179 -32
  19. package/src/Processor/AssetLoader.js +523 -76
  20. package/src/Processor/AtmosphereModel.js +465 -0
  21. package/src/Processor/BVHBuilder.js +4 -1
  22. package/src/Processor/BVHLeafFold.js +100 -0
  23. package/src/Processor/BVHRefitter.js +98 -214
  24. package/src/Processor/ChunkedRecords.js +189 -4
  25. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  26. package/src/Processor/GeometryExtractor.js +238 -40
  27. package/src/Processor/InstanceTable.js +55 -37
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +49 -8
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/MotionVector.js +8 -42
  48. package/src/Stages/NRD.js +30 -21
  49. package/src/Stages/NormalDepth.js +12 -23
  50. package/src/Stages/PathTracer.js +114 -6
  51. package/src/Stages/PathTracerStage.js +251 -104
  52. package/src/Storage/BLASCache.js +190 -0
  53. package/src/Storage/CDFCache.js +77 -0
  54. package/src/Storage/DownloadCache.js +266 -0
  55. package/src/Storage/GeometrySpill.js +217 -0
  56. package/src/Storage/SceneGraphCodec.js +503 -0
  57. package/src/Storage/SpillStore.js +112 -0
  58. package/src/Storage/StorageManager.js +934 -0
  59. package/src/Storage/StorageOps.js +137 -0
  60. package/src/Storage/StorageWorker.js +37 -0
  61. package/src/Storage/events.js +29 -0
  62. package/src/Storage/identity.js +77 -0
  63. package/src/Storage/index.js +9 -0
  64. package/src/Storage/inlineTransport.js +24 -0
  65. package/src/Storage/locks.js +99 -0
  66. package/src/Storage/openStorage.js +87 -0
  67. package/src/Storage/sceneCachePolicy.js +12 -0
  68. package/src/Storage/shared.js +9 -0
  69. package/src/Storage/transport.js +62 -0
  70. package/src/TSL/Atmosphere.js +684 -0
  71. package/src/TSL/BVHTraversal.js +288 -239
  72. package/src/TSL/CameraRay.js +99 -14
  73. package/src/TSL/Common.js +11 -2
  74. package/src/TSL/DebugKernel.js +2 -2
  75. package/src/TSL/Displacement.js +6 -6
  76. package/src/TSL/EmissiveSampling.js +5 -5
  77. package/src/TSL/EnvironmentCDF.js +176 -0
  78. package/src/TSL/GenerateKernel.js +5 -5
  79. package/src/TSL/HitFacet.js +4 -4
  80. package/src/TSL/LightsDirect.js +8 -8
  81. package/src/TSL/LightsSampling.js +55 -37
  82. package/src/TSL/Random.js +1 -0
  83. package/src/TSL/ShadeKernel.js +75 -19
  84. package/src/TSL/Sun.js +40 -0
  85. package/src/TSL/TextureSampling.js +6 -7
  86. package/src/index.js +22 -0
  87. package/src/managers/CameraManager.js +412 -56
  88. package/src/managers/DenoisingManager.js +4 -3
  89. package/src/managers/EnvironmentManager.js +263 -33
  90. package/src/managers/GoboManager.js +19 -8
  91. package/src/managers/IESManager.js +3 -3
  92. package/src/managers/InteractionManager.js +30 -2
  93. package/src/managers/LightManager.js +98 -5
  94. package/src/managers/MaterialDataManager.js +22 -2
  95. package/src/managers/TransformManager.js +26 -0
  96. package/src/managers/UniformManager.js +11 -7
  97. package/src/managers/VideoRenderManager.js +38 -17
  98. package/src/managers/ViewCamera.js +77 -0
  99. package/src/managers/WalkControls.js +262 -0
  100. package/src/managers/helpers/OutlineHelper.js +22 -2
  101. package/src/managers/timeline/CameraTrack.js +235 -0
  102. package/src/managers/timeline/TimelineManager.js +146 -0
  103. package/src/version.js +3 -0
  104. package/src/Processor/ProceduralSky.js +0 -291
@@ -24,10 +24,15 @@ import { SceneProcessor } from '../Processor/SceneProcessor.js';
24
24
  import { LightSerializer } from '../Processor/LightSerializer';
25
25
 
26
26
  // Constants
27
- import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
27
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
28
+ import { TRI_GEO_ROWS, TRI_SHADE_ROWS } from '../TSL/Common.js';
28
29
  import { getAssetConfig } from '../AssetConfig.js';
30
+ import { cachedObjectURL, DOWNLOAD_POLICY } from '../Storage/DownloadCache.js';
29
31
  import { createLogger, fmt } from '../utils/Logger.js';
30
32
 
33
+ // Triangles converted per staging pass: 256K is 12 + 8 MB of staging, kept for the stage's life.
34
+ const TRIANGLE_UPLOAD_SLICE = 1 << 18;
35
+
31
36
  const log = createLogger( 'pathtracer' );
32
37
 
33
38
  // How long after the last move the view still counts as moving. Matches CameraOptimizer's own
@@ -114,26 +119,22 @@ export class PathTracerStage extends RenderStage {
114
119
  // Initialize material data manager
115
120
  this.materialData = new MaterialDataManager( this.sdfs );
116
121
  this.materialData.callbacks.onReset = () => this.reset();
117
- // Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The authoritative
118
- // CPU copy is `_triangleRecords` when the scene is chunked and triangleStorageAttr.array
119
- // otherwise (not sdfs.triangleData, which isn't populated on the PathTracerApp build
120
- // path). The patch mutates it in place, then the GPU copy is refreshed.
122
+ // Material edits patch the per-triangle flags lane in the CPU records (`_triangleRecords`
123
+ // when chunked, `_triangleFlat` otherwise — not sdfs.triangleData, which the PathTracerApp
124
+ // build path never fills). The flags live in the shade rows, so only those go up again.
121
125
  this.materialData.callbacks.getTriangleData = () => ( {
122
- array: this.triangleStorageAttr?.array,
126
+ array: this._triangleFlat,
123
127
  records: this._triangleRecords,
124
128
  count: this.triangleCount,
125
129
  } );
126
130
  this.materialData.callbacks.onTriangleDataChanged = () => {
127
131
 
128
- if ( ! this.triangleStorageAttr ) return;
129
- // A chunked attribute owns no CPU array for three.js to re-upload.
130
- if ( this._triangleRecords ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
131
- else this.triangleStorageAttr.needsUpdate = true;
132
+ if ( this.triangleShadeAttr ) this._uploadTriangles( 0, this._triangleRecordCount, { geo: false } );
132
133
 
133
134
  };
134
135
 
135
136
  // Initialize environment manager
136
- this.environment = new EnvironmentManager( this.scene, this.uniforms );
137
+ this.environment = new EnvironmentManager( this.scene, this.uniforms, this.renderer );
137
138
  this.environment.callbacks.onReset = () => this.reset();
138
139
  this.environment.callbacks.getSceneTextureNodes = () => this.shaderBuilder.getSceneTextureNodes();
139
140
 
@@ -183,9 +184,13 @@ export class PathTracerStage extends RenderStage {
183
184
  */
184
185
  _initDataTextures() {
185
186
 
186
- // Triangle data (storage buffer for WebGPU)
187
- this.triangleStorageAttr = null;
187
+ // Triangle data: two GPU buffers (TRI_GEO_ROWS / TRI_SHADE_ROWS rows a triangle), one
188
+ // { geo, shade } node pair, and the CPU records they were written from.
189
+ this.triangleGeoAttr = null;
190
+ this.triangleShadeAttr = null;
188
191
  this._triangleRecords = null;
192
+ this._triangleFlat = null;
193
+ this._triangleRecordCount = 0;
189
194
  this.triangleStorageNode = null;
190
195
  this.triangleCount = 0;
191
196
 
@@ -238,6 +243,8 @@ export class PathTracerStage extends RenderStage {
238
243
  // Per-triangle bit-trail map (root→leaf Light BVH path, 1 float per triangleIndex); packed
239
244
  // after the emissive entries so the bounce-hit MIS path can re-walk the descent pdf.
240
245
  this._bitTrailMapCache = null;
246
+ this._bitTrailOffset = 0;
247
+ this._bitTrailLength = 0;
241
248
 
242
249
  // Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
243
250
  // (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
@@ -384,7 +391,22 @@ export class PathTracerStage extends RenderStage {
384
391
 
385
392
  const { stbnScalarAtlas, stbnVec2Atlas } = getAssetConfig();
386
393
 
387
- const scalarLoad = loader.loadAsync( stbnScalarAtlas ).then( ( tex ) => {
394
+ const load = async ( url ) => {
395
+
396
+ const source = await cachedObjectURL( url, { policy: DOWNLOAD_POLICY.REVALIDATE } );
397
+ try {
398
+
399
+ return await loader.loadAsync( source.url );
400
+
401
+ } finally {
402
+
403
+ source.release();
404
+
405
+ }
406
+
407
+ };
408
+
409
+ const scalarLoad = load( stbnScalarAtlas ).then( ( tex ) => {
388
410
 
389
411
  this.stbnScalarTexture = configure( tex );
390
412
  stbnScalarTextureNode.value = tex;
@@ -392,7 +414,7 @@ export class PathTracerStage extends RenderStage {
392
414
 
393
415
  } );
394
416
 
395
- const vec2Load = loader.loadAsync( stbnVec2Atlas ).then( ( tex ) => {
417
+ const vec2Load = load( stbnVec2Atlas ).then( ( tex ) => {
396
418
 
397
419
  this.stbnVec2Texture = configure( tex );
398
420
  stbnVec2TextureNode.value = tex;
@@ -543,36 +565,6 @@ export class PathTracerStage extends RenderStage {
543
565
  this.spotLightsData = mockMaterial.uniforms.spotLights.value;
544
566
  this.areaLightsData = mockMaterial.uniforms.areaLights.value;
545
567
 
546
- // Add sun as directional light if procedural sky is active
547
- if ( this.hasSun.value ) {
548
-
549
- const scaledSunIntensity = this.environment.envParams.skySunIntensity * 950.0;
550
-
551
- const sunLight = {
552
- intensity: scaledSunIntensity,
553
- color: { r: 1.0, g: 1.0, b: 1.0 },
554
- userData: {
555
- angle: this.sunAngularSize.value
556
- },
557
- updateMatrixWorld: () => {},
558
- getWorldPosition: ( target ) => {
559
-
560
- const sunDir = this.sunDirection.value;
561
- return target.set( sunDir.x, sunDir.y, sunDir.z ).multiplyScalar( 1e10 );
562
-
563
- }
564
- };
565
-
566
- this.lightSerializer.addDirectionalLight( sunLight );
567
- this.lightSerializer.preprocessLights();
568
- this.lightSerializer.updateShaderUniforms( mockMaterial );
569
-
570
- this.directionalLightsData = mockMaterial.uniforms.directionalLights.value;
571
-
572
- log.debug( `sun added as directional light · intensity ${scaledSunIntensity.toFixed( 2 )}` );
573
-
574
- }
575
-
576
568
  // Update TSL uniform buffer nodes from raw Float32Array data
577
569
  this._updateLightBufferNodes();
578
570
 
@@ -729,54 +721,212 @@ export class PathTracerStage extends RenderStage {
729
721
  // ===== TEXTURE SETTERS =====
730
722
 
731
723
  /**
732
- * Sets the triangle data from raw Float32Array via storage buffer.
733
- * On first call, creates the storage buffer and node.
734
- * On subsequent calls, creates a new attribute with the correct size
735
- * and updates the storage node's value to preserve shader graph references.
736
- * @param {Uint32Array} triangleData - Packed triangle records (uvec4 lanes)
724
+ * Sets the triangle data. The records stay whole on the CPU and are split into the geo and
725
+ * shade GPU buffers as they go up. Later calls swap the nodes' attributes, which keeps the
726
+ * compiled shader graph.
727
+ * @param {Uint32Array|import('../Processor/ChunkedRecords.js').ChunkedRecords} triangleData
737
728
  * @param {number} triangleCount - Number of triangles
738
729
  */
739
730
  setTriangleData( triangleData, triangleCount ) {
740
731
 
741
732
  if ( ! triangleData ) return;
742
733
 
743
- // Past the ~2 GB array cap the records arrive as several chunks; they still become one
744
- // GPU buffer, written at their running byte offsets, so no binding or shader changes.
745
734
  const chunked = triangleData.chunks && triangleData.chunks.length > 1 ? triangleData : null;
746
735
  const flat = chunked ? null : ( triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData );
747
- const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
748
- const vec4Count = lanes / 4;
749
-
750
- const makeAttr = () => chunked
751
- ? gpuOnlyStorageAttribute( vec4Count, 4, Uint32Array )
752
- : new StorageInstancedBufferAttribute( flat, 4 );
736
+ const records = chunked ? chunked.recordCount : Math.ceil( flat.length / TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
737
+ const pre = chunked?._gpuUpload?.triangles ? chunked._gpuUpload : null;
738
+ const { geo, shade } = pre ?? this._allocateTriangleBuffers( records );
753
739
 
754
740
  if ( this.triangleStorageNode ) {
755
741
 
756
- // Create new attribute with correct size (old one is GC'd, backend WeakMap cleans up GPU buffer)
757
- this.triangleStorageAttr = makeAttr();
758
-
759
- // Update storage node references (preserves compiled shader graph)
760
- this.triangleStorageNode.value = this.triangleStorageAttr;
761
- this.triangleStorageNode.bufferCount = vec4Count;
742
+ this.triangleStorageNode.geo.value = geo;
743
+ this.triangleStorageNode.geo.bufferCount = geo.count;
744
+ this.triangleStorageNode.shade.value = shade;
745
+ this.triangleStorageNode.shade.bufferCount = shade.count;
762
746
 
763
747
  } else {
764
748
 
765
- // First time: create storage buffer and node
766
- this.triangleStorageAttr = makeAttr();
767
- this.triangleStorageNode = storage( this.triangleStorageAttr, 'uvec4', vec4Count ).toReadOnly();
749
+ this.triangleStorageNode = {
750
+ geo: storage( geo, 'uvec4', geo.count ).toReadOnly(),
751
+ shade: storage( shade, 'uvec4', shade.count ).toReadOnly(),
752
+ };
768
753
 
769
754
  }
770
755
 
756
+ this.triangleGeoAttr = geo;
757
+ this.triangleShadeAttr = shade;
771
758
  this._triangleRecords = chunked;
772
- if ( chunked ) uploadStorageChunks( this.renderer, this.triangleStorageAttr, chunked.chunks );
773
-
759
+ this._triangleFlat = flat;
760
+ this._triangleRecordCount = records;
774
761
  this.triangleCount = triangleCount;
762
+ this._uploadTriangles( 0, records, { skipChunks: pre?.uploaded } );
775
763
 
776
764
  log.debug( `${fmt.n( this.triangleCount )} triangles (storage buffer)` );
777
765
 
778
766
  }
779
767
 
768
+ /** GPU-only geo and shade buffers for `records` triangles, created now. @private */
769
+ _allocateTriangleBuffers( records ) {
770
+
771
+ const n = Math.max( 1, records );
772
+ this._assertFitsGPU( n * TRI_GEO_ROWS * 16 );
773
+ const geo = gpuOnlyStorageAttribute( n * TRI_GEO_ROWS, 4, Uint32Array );
774
+ const shade = gpuOnlyStorageAttribute( n * TRI_SHADE_ROWS, 4, Uint32Array );
775
+ this.renderer.backend.createStorageAttribute( geo );
776
+ this.renderer.backend.createStorageAttribute( shade );
777
+ return { geo, shade };
778
+
779
+ }
780
+
781
+ /**
782
+ * Writes triangles [start, start + count) of the CPU records into the geo and shade buffers:
783
+ * the first TRI_GEO_ROWS rows of each record to one, the rest to the other.
784
+ * @param {number} start
785
+ * @param {number} count
786
+ * @param {{ geo?: boolean, shade?: boolean, skipChunks?: Set<number>, source?: Uint32Array, sourceStart?: number }} [options]
787
+ * `source` writes those records instead of reading the stage's own (a chunk being handed over).
788
+ * @private
789
+ */
790
+ _uploadTriangles( start, count, { geo = true, shade = true, skipChunks = null, source = null, sourceStart = 0, geoAttr = this.triangleGeoAttr, shadeAttr = this.triangleShadeAttr } = {} ) {
791
+
792
+ if ( count <= 0 || ! geoAttr ) return;
793
+
794
+ const backend = this.renderer.backend;
795
+ const geoBuffer = backend.get( geoAttr ).buffer;
796
+ const shadeBuffer = backend.get( shadeAttr ).buffer;
797
+ const LANES = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
798
+ const GEO = TRI_GEO_ROWS * 4, SHADE = TRI_SHADE_ROWS * 4;
799
+ const records = this._triangleRecords;
800
+ const perChunk = source ? count : records ? records.recordsPerChunk : this._triangleRecordCount;
801
+ // One staging pair for every upload: a fresh one per chunk was churn that failed to
802
+ // allocate on a scene already at the edge of the address space.
803
+ const slice = TRIANGLE_UPLOAD_SLICE;
804
+ this._triangleStaging ??= { geo: new Uint32Array( slice * GEO ), shade: new Uint32Array( slice * SHADE ) };
805
+ const geoStage = this._triangleStaging.geo;
806
+ const shadeStage = this._triangleStaging.shade;
807
+
808
+ for ( let at = start, end = start + count; at < end; ) {
809
+
810
+ const k = source ? 0 : Math.floor( at / perChunk );
811
+ const chunkStart = source ? start : k * perChunk;
812
+ const chunkEnd = Math.min( end, chunkStart + perChunk );
813
+ if ( skipChunks?.has( k ) ) {
814
+
815
+ at = chunkEnd;
816
+ continue;
817
+
818
+ }
819
+
820
+ const src = source ?? ( records ? records.chunks[ k ] : this._triangleFlat );
821
+ if ( ! src ) throw new Error( `triangle chunk ${k} is on disk and was never uploaded` );
822
+
823
+ const n = Math.min( slice, chunkEnd - at );
824
+ let s = ( source ? at - start + sourceStart : at - chunkStart ) * LANES;
825
+ for ( let i = 0, g = 0, h = 0; i < n; i ++, s += LANES ) {
826
+
827
+ if ( geo ) for ( let l = 0; l < GEO; l ++ ) geoStage[ g ++ ] = src[ s + l ];
828
+ if ( shade ) for ( let l = GEO; l < LANES; l ++ ) shadeStage[ h ++ ] = src[ s + l ];
829
+
830
+ }
831
+
832
+ if ( geo ) backend.device.queue.writeBuffer( geoBuffer, at * GEO * 4, geoStage, 0, n * GEO );
833
+ if ( shade ) backend.device.queue.writeBuffer( shadeBuffer, at * SHADE * 4, shadeStage, 0, n * SHADE );
834
+ at += n;
835
+
836
+ }
837
+
838
+ }
839
+
840
+ /**
841
+ * Allocates the GPU buffer for chunked `records` now and returns a function that writes one
842
+ * chunk into it, so a build can hand chunks over as it finishes them and let them go (the
843
+ * memory spill). setTriangleData / setBVHData then adopt that buffer rather than upload again.
844
+ * @param {import('../Processor/ChunkedRecords.js').ChunkedRecords} records
845
+ * @returns {function(number): void} uploads chunk k from `records.chunks[ k ]`
846
+ */
847
+ createChunkUploader( records ) {
848
+
849
+ this._assertFitsGPU( records.recordCount * records.lanesPerRecord * records.LaneType.BYTES_PER_ELEMENT );
850
+ const attr = gpuOnlyStorageAttribute( records.recordCount * records.lanesPerRecord / 4, 4, records.LaneType );
851
+ const backend = this.renderer.backend;
852
+ backend.createStorageAttribute( attr );
853
+ const buffer = backend.get( attr ).buffer;
854
+ const stride = records.recordsPerChunk * records.lanesPerRecord * records.LaneType.BYTES_PER_ELEMENT;
855
+ const uploaded = new Set();
856
+ records._gpuUpload = { attr, uploaded };
857
+
858
+ return k => {
859
+
860
+ const chunk = records.chunks[ k ];
861
+ backend.device.queue.writeBuffer( buffer, k * stride, chunk, 0, chunk.length );
862
+ uploaded.add( k );
863
+
864
+ };
865
+
866
+ }
867
+
868
+ /**
869
+ * createChunkUploader for triangle records: both GPU buffers are allocated now and each
870
+ * chunk is split into them as it is handed over.
871
+ * @param {import('../Processor/ChunkedRecords.js').ChunkedRecords} records
872
+ * @returns {function(number): void}
873
+ */
874
+ createTriangleChunkUploader( records ) {
875
+
876
+ const { geo, shade } = this._allocateTriangleBuffers( records.recordCount );
877
+ const uploaded = new Set();
878
+ records._gpuUpload = { triangles: true, geo, shade, uploaded };
879
+ const perChunk = records.recordsPerChunk;
880
+
881
+ return k => {
882
+
883
+ const first = k * perChunk;
884
+ const count = Math.min( perChunk, records.recordCount - first );
885
+ this._uploadTriangles( first, count, { source: records.chunks[ k ], geoAttr: geo, shadeAttr: shade } );
886
+ uploaded.add( k );
887
+
888
+ };
889
+
890
+ }
891
+
892
+ /**
893
+ * One storage buffer holds all of a scene's triangles, and one its BVH. Past the device's
894
+ * limit WebGPU hands back an invalid buffer and every write to it fails quietly.
895
+ * @private
896
+ */
897
+ _assertFitsGPU( bytes ) {
898
+
899
+ const limits = this.renderer?.backend?.device?.limits;
900
+ const max = Math.min( limits?.maxBufferSize ?? Infinity, limits?.maxStorageBufferBindingSize ?? Infinity );
901
+ if ( bytes > max ) throw new Error( `the scene needs a ${fmt.mb( bytes )} GPU buffer, past the ${fmt.mb( max )} this GPU allows for one — load fewer triangles` );
902
+
903
+ }
904
+
905
+ /** Uploads chunked records, or — for a buffer a build already filled — whatever it has not. @private */
906
+ _uploadChunked( attr, records ) {
907
+
908
+ const pre = records._gpuUpload;
909
+ if ( ! pre || pre.attr !== attr ) {
910
+
911
+ uploadStorageChunks( this.renderer, attr, records.chunks );
912
+ return;
913
+
914
+ }
915
+
916
+ const buffer = this.renderer.backend.get( attr ).buffer;
917
+ const stride = records.recordsPerChunk * records.lanesPerRecord * records.LaneType.BYTES_PER_ELEMENT;
918
+ for ( let k = 0; k < records.chunks.length; k ++ ) {
919
+
920
+ if ( pre.uploaded.has( k ) ) continue;
921
+ const chunk = records.chunks[ k ];
922
+ if ( ! chunk ) throw new Error( `chunk ${k} is on disk and was never uploaded` );
923
+ this.renderer.backend.device.queue.writeBuffer( buffer, k * stride, chunk, 0, chunk.length );
924
+ pre.uploaded.add( k );
925
+
926
+ }
927
+
928
+ }
929
+
780
930
  /**
781
931
  * Sets the BVH data from raw Float32Array via storage buffer.
782
932
  * @param {Float32Array} bvhImageData - Raw BVH data from DataTexture.image.data
@@ -791,9 +941,10 @@ export class PathTracerStage extends RenderStage {
791
941
  const flat = chunked ? null : ( bvhImageData.chunks ? bvhImageData.chunks[ 0 ] : bvhImageData );
792
942
  const lanes = chunked ? chunked.recordCount * chunked.lanesPerRecord : flat.length;
793
943
  const vec4Count = lanes / 4;
944
+ this._assertFitsGPU( lanes * 4 );
794
945
 
795
946
  const makeAttr = () => chunked
796
- ? gpuOnlyStorageAttribute( vec4Count, 4, Float32Array )
947
+ ? chunked._gpuUpload?.attr ?? gpuOnlyStorageAttribute( vec4Count, 4, Float32Array )
797
948
  : new StorageInstancedBufferAttribute( flat, 4 );
798
949
 
799
950
  if ( this.bvhStorageNode ) {
@@ -809,8 +960,9 @@ export class PathTracerStage extends RenderStage {
809
960
 
810
961
  }
811
962
 
963
+ this.bvhStorageAttr.foldedLeaves = bvhImageData.foldedLeaves === true;
812
964
  this._bvhRecords = chunked;
813
- if ( chunked ) uploadStorageChunks( this.renderer, this.bvhStorageAttr, chunked.chunks );
965
+ if ( chunked ) this._uploadChunked( this.bvhStorageAttr, chunked );
814
966
 
815
967
  this.bvhNodeCount = Math.floor( vec4Count / BVH_VEC4_PER_NODE );
816
968
  log.debug( `${fmt.n( this.bvhNodeCount )} BVH nodes (storage buffer)` );
@@ -1019,17 +1171,12 @@ export class PathTracerStage extends RenderStage {
1019
1171
 
1020
1172
  }
1021
1173
 
1022
- /** Update triangle positions in the existing GPU buffer (full). */
1174
+ /** Re-uploads every triangle from the CPU records, adopting `triangleData` as them when given. */
1023
1175
  updateTriangleData( triangleData ) {
1024
1176
 
1025
- if ( this._triangleRecords ) {
1026
-
1027
- uploadStorageChunks( this.renderer, this.triangleStorageAttr, this._triangleRecords.chunks );
1028
- return;
1029
-
1030
- }
1031
-
1032
- this._updateStorageBuffer( this.triangleStorageAttr, triangleData?.chunks ? triangleData.chunks[ 0 ] : triangleData );
1177
+ if ( triangleData?.chunks?.length > 1 ) this._triangleRecords = triangleData;
1178
+ else if ( triangleData ) this._triangleFlat = triangleData.chunks ? triangleData.chunks[ 0 ] : triangleData;
1179
+ this._uploadTriangles( 0, this._triangleRecordCount );
1033
1180
 
1034
1181
  }
1035
1182
 
@@ -1056,29 +1203,14 @@ export class PathTracerStage extends RenderStage {
1056
1203
  */
1057
1204
  updateBufferRanges( triRanges, bvhRanges ) {
1058
1205
 
1059
- if ( this.triangleStorageAttr && triRanges.length > 0 ) {
1060
-
1061
- if ( this._triangleRecords ) {
1062
-
1063
- // Chunked: three.js cannot re-upload an attribute with no CPU array, so write
1064
- // the dirty lanes straight into the GPU buffer.
1065
- for ( const r of triRanges ) {
1066
-
1067
- uploadStorageChunkRange( this.renderer, this.triangleStorageAttr, this._triangleRecords, r.offset, r.count );
1206
+ if ( this.triangleGeoAttr && triRanges.length > 0 ) {
1068
1207
 
1069
- }
1070
-
1071
- } else {
1208
+ // Ranges are in lanes of the CPU records; the GPU copy is written a triangle at a time.
1209
+ const lanes = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
1210
+ for ( const r of triRanges ) {
1072
1211
 
1073
- this.triangleStorageAttr.clearUpdateRanges();
1074
-
1075
- for ( const r of triRanges ) {
1076
-
1077
- this.triangleStorageAttr.addUpdateRange( r.offset, r.count );
1078
-
1079
- }
1080
-
1081
- this.triangleStorageAttr.version ++;
1212
+ const first = Math.floor( r.offset / lanes );
1213
+ this._uploadTriangles( first, Math.ceil( ( r.offset + r.count ) / lanes ) - first );
1082
1214
 
1083
1215
  }
1084
1216
 
@@ -1438,7 +1570,12 @@ export class PathTracerStage extends RenderStage {
1438
1570
  const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
1439
1571
  const lbvh = this._lbvhDataCache;
1440
1572
  const emis = this._emissiveDataCache;
1441
- const trail = this._bitTrailMapCache;
1573
+ // The bit-trail map (4 B a triangle) lives only in the light buffer: without a new one, the
1574
+ // current one is carried over from there rather than kept as a second copy.
1575
+ const held = this.lightStorageAttr?.array;
1576
+ const trail = this._bitTrailMapCache
1577
+ ?? ( held && this._bitTrailLength ? held.slice( this._bitTrailOffset, this._bitTrailOffset + this._bitTrailLength ) : null );
1578
+ this._bitTrailMapCache = null;
1442
1579
  const lbvhLen = lbvh ? lbvh.length : 0;
1443
1580
  const emisLen = emis ? emis.length : 0;
1444
1581
  // Bit-trail map packs 4 trails per vec4 → pad to a vec4 boundary.
@@ -1479,6 +1616,8 @@ export class PathTracerStage extends RenderStage {
1479
1616
  // Offset (in vec4 elements) where the bit-trail map starts (lbvhLen + emisLen are float
1480
1617
  // counts, both multiples of 4, so this divides cleanly).
1481
1618
  this.reverseMapVec4Offset.value = ( lbvhLen + emisLen ) / 4;
1619
+ this._bitTrailOffset = lbvhLen + emisLen;
1620
+ this._bitTrailLength = trail ? trail.length : 0;
1482
1621
 
1483
1622
  }
1484
1623
 
@@ -1548,7 +1687,11 @@ export class PathTracerStage extends RenderStage {
1548
1687
  log.debug( 'material rebuild started' );
1549
1688
 
1550
1689
  await this.sdfs.rebuildMaterials( scene );
1551
- this.updateSceneUniforms();
1690
+ // Materials and textures only. `sdfs` is not the processor that built the scene
1691
+ // (PathTracerApp._sdf is), so re-uploading everything from it put its empty emissive
1692
+ // data and instance table in place of the scene's: emitters stopped being sampled.
1693
+ this.materialData.setMaterialData( this.sdfs.materialData, this.sdfs.materials?.map( m => m.sources ) );
1694
+ this.materialData.loadTexturesFromSdfs();
1552
1695
  this.shaderBuilder.updateSceneTextures( this );
1553
1696
  this.updateLights();
1554
1697
  this.reset();
@@ -1607,8 +1750,12 @@ export class PathTracerStage extends RenderStage {
1607
1750
  this.placeholderTexture?.dispose();
1608
1751
 
1609
1752
  // Clear data references
1610
- this.triangleStorageAttr = null;
1753
+ this.triangleGeoAttr = null;
1754
+ this.triangleShadeAttr = null;
1755
+ this._triangleStaging = null;
1611
1756
  this._triangleRecords = null;
1757
+ this._triangleFlat = null;
1758
+ this._triangleRecordCount = 0;
1612
1759
  this.triangleStorageNode = null;
1613
1760
  this.bvhStorageAttr = null;
1614
1761
  this._bvhRecords = null;