@onerjs/core 8.54.4 → 8.54.5

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 (177) hide show
  1. package/Engines/WebGPU/Extensions/engine.cubeTexture.pure.js +1 -1
  2. package/Engines/WebGPU/Extensions/engine.cubeTexture.pure.js.map +1 -1
  3. package/Engines/WebGPU/Extensions/engine.rawTexture.pure.js +2 -2
  4. package/Engines/WebGPU/Extensions/engine.rawTexture.pure.js.map +1 -1
  5. package/Engines/WebGPU/webgpuCacheRenderPipeline.d.ts +1 -0
  6. package/Engines/WebGPU/webgpuCacheRenderPipeline.js +18 -4
  7. package/Engines/WebGPU/webgpuCacheRenderPipeline.js.map +1 -1
  8. package/Engines/WebGPU/webgpuCacheTextureView.d.ts +13 -0
  9. package/Engines/WebGPU/webgpuCacheTextureView.js +32 -0
  10. package/Engines/WebGPU/webgpuCacheTextureView.js.map +1 -0
  11. package/Engines/WebGPU/webgpuRenderTargetWrapper.d.ts +10 -0
  12. package/Engines/WebGPU/webgpuRenderTargetWrapper.js +10 -0
  13. package/Engines/WebGPU/webgpuRenderTargetWrapper.js.map +1 -1
  14. package/Engines/WebGPU/webgpuShaderProcessorsWGSL.pure.js +57 -4
  15. package/Engines/WebGPU/webgpuShaderProcessorsWGSL.pure.js.map +1 -1
  16. package/Engines/WebGPU/webgpuTextureManager.d.ts +1 -1
  17. package/Engines/WebGPU/webgpuTextureManager.js +4 -2
  18. package/Engines/WebGPU/webgpuTextureManager.js.map +1 -1
  19. package/Engines/abstractEngine.pure.js +2 -2
  20. package/Engines/abstractEngine.pure.js.map +1 -1
  21. package/Engines/thinNativeEngine.pure.d.ts +5 -0
  22. package/Engines/thinNativeEngine.pure.js +29 -6
  23. package/Engines/thinNativeEngine.pure.js.map +1 -1
  24. package/Engines/webgpuEngine.pure.d.ts +1 -0
  25. package/Engines/webgpuEngine.pure.js +21 -8
  26. package/Engines/webgpuEngine.pure.js.map +1 -1
  27. package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.d.ts +95 -1
  28. package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.js +169 -4
  29. package/FlowGraph/Blocks/Data/Math/flowGraphMathBlocks.pure.js.map +1 -1
  30. package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.d.ts +21 -6
  31. package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.js +28 -26
  32. package/FlowGraph/Blocks/Data/Math/flowGraphMathCombineExtractBlocks.pure.js.map +1 -1
  33. package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.d.ts +30 -2
  34. package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.js +111 -29
  35. package/FlowGraph/Blocks/Data/Math/flowGraphMatrixMathBlocks.pure.js.map +1 -1
  36. package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.d.ts +56 -2
  37. package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.js +99 -14
  38. package/FlowGraph/Blocks/Data/Math/flowGraphVectorMathBlocks.pure.js.map +1 -1
  39. package/FlowGraph/Blocks/Data/Transformers/flowGraphJsonPointerParserBlock.pure.js +10 -2
  40. package/FlowGraph/Blocks/Data/Transformers/flowGraphJsonPointerParserBlock.pure.js.map +1 -1
  41. package/FlowGraph/Blocks/Data/Utils/flowGraphArrayIndexBlock.pure.js +21 -1
  42. package/FlowGraph/Blocks/Data/Utils/flowGraphArrayIndexBlock.pure.js.map +1 -1
  43. package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.d.ts +18 -0
  44. package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.js +29 -2
  45. package/FlowGraph/Blocks/Data/flowGraphCachedOperationBlock.js.map +1 -1
  46. package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.d.ts +15 -0
  47. package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.js +44 -7
  48. package/FlowGraph/Blocks/Event/flowGraphReceiveCustomEventBlock.pure.js.map +1 -1
  49. package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.d.ts +9 -0
  50. package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.js +9 -1
  51. package/FlowGraph/Blocks/Event/flowGraphSceneReadyEventBlock.pure.js.map +1 -1
  52. package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.d.ts +7 -0
  53. package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.js +8 -1
  54. package/FlowGraph/Blocks/Event/flowGraphSceneTickEventBlock.pure.js.map +1 -1
  55. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.d.ts +5 -0
  56. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.js +8 -0
  57. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.js.map +1 -0
  58. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.d.ts +37 -0
  59. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.js +44 -0
  60. package/FlowGraph/Blocks/Event/flowGraphStopEventPropagationBlock.pure.js.map +1 -0
  61. package/FlowGraph/Blocks/Event/index.d.ts +1 -0
  62. package/FlowGraph/Blocks/Event/index.js +1 -0
  63. package/FlowGraph/Blocks/Event/index.js.map +1 -1
  64. package/FlowGraph/Blocks/Event/pure.d.ts +1 -0
  65. package/FlowGraph/Blocks/Event/pure.js +1 -0
  66. package/FlowGraph/Blocks/Event/pure.js.map +1 -1
  67. package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.d.ts +4 -1
  68. package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.js +67 -2
  69. package/FlowGraph/Blocks/Execution/Animation/flowGraphBezierCurveEasingBlock.pure.js.map +1 -1
  70. package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.d.ts +13 -0
  71. package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.js +102 -7
  72. package/FlowGraph/Blocks/Execution/Animation/flowGraphPlayAnimationBlock.pure.js.map +1 -1
  73. package/FlowGraph/Blocks/Execution/Animation/flowGraphStopAnimationBlock.pure.js +5 -1
  74. package/FlowGraph/Blocks/Execution/Animation/flowGraphStopAnimationBlock.pure.js.map +1 -1
  75. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphCancelDelayBlock.pure.js +5 -1
  76. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphCancelDelayBlock.pure.js.map +1 -1
  77. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.d.ts +11 -2
  78. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.js +21 -3
  79. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphForLoopBlock.pure.js.map +1 -1
  80. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphSetDelayBlock.pure.js +25 -4
  81. package/FlowGraph/Blocks/Execution/ControlFlow/flowGraphSetDelayBlock.pure.js.map +1 -1
  82. package/FlowGraph/Blocks/flowGraphBlockFactory.js +18 -0
  83. package/FlowGraph/Blocks/flowGraphBlockFactory.js.map +1 -1
  84. package/FlowGraph/Blocks/flowGraphBlockNames.d.ts +9 -0
  85. package/FlowGraph/Blocks/flowGraphBlockNames.js +9 -0
  86. package/FlowGraph/Blocks/flowGraphBlockNames.js.map +1 -1
  87. package/FlowGraph/CustomTypes/flowGraphMatrix.d.ts +17 -3
  88. package/FlowGraph/CustomTypes/flowGraphMatrix.js +27 -21
  89. package/FlowGraph/CustomTypes/flowGraphMatrix.js.map +1 -1
  90. package/FlowGraph/flowGraphContext.d.ts +23 -0
  91. package/FlowGraph/flowGraphContext.js +29 -0
  92. package/FlowGraph/flowGraphContext.js.map +1 -1
  93. package/FlowGraph/flowGraphCoordinator.d.ts +58 -1
  94. package/FlowGraph/flowGraphCoordinator.js +72 -0
  95. package/FlowGraph/flowGraphCoordinator.js.map +1 -1
  96. package/FlowGraph/flowGraphDelayRegistry.d.ts +31 -0
  97. package/FlowGraph/flowGraphDelayRegistry.js +50 -0
  98. package/FlowGraph/flowGraphDelayRegistry.js.map +1 -0
  99. package/FlowGraph/flowGraphHostResolver.d.ts +69 -0
  100. package/FlowGraph/flowGraphHostResolver.js +25 -0
  101. package/FlowGraph/flowGraphHostResolver.js.map +1 -0
  102. package/FlowGraph/flowGraphMath.d.ts +48 -1
  103. package/FlowGraph/flowGraphMath.js +169 -4
  104. package/FlowGraph/flowGraphMath.js.map +1 -1
  105. package/FlowGraph/flowGraphPathConverterComponent.d.ts +14 -3
  106. package/FlowGraph/flowGraphPathConverterComponent.js +110 -18
  107. package/FlowGraph/flowGraphPathConverterComponent.js.map +1 -1
  108. package/FlowGraph/flowGraphSignalConnection.pure.js +7 -1
  109. package/FlowGraph/flowGraphSignalConnection.pure.js.map +1 -1
  110. package/FlowGraph/index.d.ts +2 -0
  111. package/FlowGraph/index.js +2 -0
  112. package/FlowGraph/index.js.map +1 -1
  113. package/FlowGraph/pure.d.ts +2 -0
  114. package/FlowGraph/pure.js +2 -0
  115. package/FlowGraph/pure.js.map +1 -1
  116. package/Maths/math.vector.pure.d.ts +10 -0
  117. package/Maths/math.vector.pure.js +13 -0
  118. package/Maths/math.vector.pure.js.map +1 -1
  119. package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.d.ts +216 -1
  120. package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js +714 -67
  121. package/Meshes/GaussianSplatting/gaussianSplattingMesh.pure.js.map +1 -1
  122. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.d.ts +81 -4
  123. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js +466 -52
  124. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.js.map +1 -1
  125. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.d.ts +19 -0
  126. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.js +24 -0
  127. package/Meshes/GaussianSplatting/gaussianSplattingPartProxyMesh.pure.js.map +1 -1
  128. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js +31 -6
  129. package/Meshes/GaussianSplatting/gaussianSplattingSortWorker.js.map +1 -1
  130. package/Meshes/GreasedLine/greasedLineRibbonMesh.pure.js +5 -1
  131. package/Meshes/GreasedLine/greasedLineRibbonMesh.pure.js.map +1 -1
  132. package/Misc/greasedLineTools.js +1 -1
  133. package/Misc/greasedLineTools.js.map +1 -1
  134. package/Misc/tools.pure.js +14 -2
  135. package/Misc/tools.pure.js.map +1 -1
  136. package/Particles/gpuParticleSystem.pure.d.ts +66 -1
  137. package/Particles/gpuParticleSystem.pure.js +189 -49
  138. package/Particles/gpuParticleSystem.pure.js.map +1 -1
  139. package/Physics/v2/characterController.js +69 -17
  140. package/Physics/v2/characterController.js.map +1 -1
  141. package/XR/features/Layers/WebXRWebGPUCompositionLayer.d.ts +3 -9
  142. package/XR/features/Layers/WebXRWebGPUCompositionLayer.js +4 -11
  143. package/XR/features/Layers/WebXRWebGPUCompositionLayer.js.map +1 -1
  144. package/XR/features/WebXRAbstractFeature.d.ts +9 -0
  145. package/XR/features/WebXRAbstractFeature.js +16 -0
  146. package/XR/features/WebXRAbstractFeature.js.map +1 -1
  147. package/XR/features/WebXRDepthSensing.pure.d.ts +7 -0
  148. package/XR/features/WebXRDepthSensing.pure.js +350 -39
  149. package/XR/features/WebXRDepthSensing.pure.js.map +1 -1
  150. package/XR/features/WebXRImageTracking.pure.js.map +1 -1
  151. package/XR/features/WebXRLayers.pure.d.ts +6 -5
  152. package/XR/features/WebXRLayers.pure.js +65 -8
  153. package/XR/features/WebXRLayers.pure.js.map +1 -1
  154. package/XR/features/WebXRRawCameraAccess.pure.js +9 -2
  155. package/XR/features/WebXRRawCameraAccess.pure.js.map +1 -1
  156. package/XR/features/WebXRSpaceWarp.pure.d.ts +0 -1
  157. package/XR/features/WebXRSpaceWarp.pure.js +13 -4
  158. package/XR/features/WebXRSpaceWarp.pure.js.map +1 -1
  159. package/XR/features/WebXRWalkingLocomotion.pure.js.map +1 -1
  160. package/XR/webXRCamera.js +27 -0
  161. package/XR/webXRCamera.js.map +1 -1
  162. package/XR/webXRFeatureWarningRegistry.d.ts +14 -0
  163. package/XR/webXRFeatureWarningRegistry.js +19 -0
  164. package/XR/webXRFeatureWarningRegistry.js.map +1 -0
  165. package/XR/webXRFeaturesManager.d.ts +5 -0
  166. package/XR/webXRFeaturesManager.js +23 -4
  167. package/XR/webXRFeaturesManager.js.map +1 -1
  168. package/XR/webXRGraphicsBinding.d.ts +17 -10
  169. package/XR/webXRGraphicsBinding.js +8 -5
  170. package/XR/webXRGraphicsBinding.js.map +1 -1
  171. package/XR/webXRSessionManager.d.ts +3 -5
  172. package/XR/webXRSessionManager.js +1 -3
  173. package/XR/webXRSessionManager.js.map +1 -1
  174. package/XR/webXRWebGPURenderTargetTextureProvider.d.ts +21 -5
  175. package/XR/webXRWebGPURenderTargetTextureProvider.js +38 -10
  176. package/XR/webXRWebGPURenderTargetTextureProvider.js.map +1 -1
  177. package/package.json +1 -1
@@ -7,6 +7,8 @@ import { Logger } from "../../Misc/logger.js";
7
7
  import { Observable } from "../../Misc/observable.js";
8
8
  import { GaussianSplattingMaterial } from "../../Materials/GaussianSplatting/gaussianSplattingMaterial.pure.js";
9
9
  import { RawTexture } from "../../Materials/Textures/rawTexture.js";
10
+ import { MultiRenderTarget } from "../../Materials/Textures/multiRenderTarget.pure.js";
11
+ import { Color4 } from "../../Maths/math.color.pure.js";
10
12
 
11
13
  import { ToHalfFloat } from "../../Misc/textureTools.js";
12
14
  import { Scalar } from "../../Maths/math.scalar.js";
@@ -436,14 +438,34 @@ export class GaussianSplattingMeshBase extends Mesh {
436
438
  * @param colors color texture
437
439
  * @param splatPositions stride-4 CPU centers for depth sorting (length vertexCount*4)
438
440
  * @param vertexCount number of splats addressable in the work buffer
441
+ * @param shTextures optional baked higher-order SH textures (packed-u32) produced by the work buffer's SH decode.
442
+ * When provided, the draw path lights the decoded splats with view-dependent SH (the non-SOG `shTexture0..N`
443
+ * path). `shDegree` sets both the max and active SH degree.
444
+ * @param shDegree SH degree of the baked SH textures (0 = none). Ignored when `shTextures` is empty/omitted.
445
+ * @param rotationTextures optional decoded rotation/scale textures ([rotA, rotB, rotScale]) produced by the work
446
+ * buffer's rotation decode. When provided, they drive the voxel-IBL rotation/scale samplers so the streamed
447
+ * splats participate in voxel-based IBL shadowing.
439
448
  */
440
- _setExternalWorkBuffer(centers, covariancesA, covariancesB, colors, splatPositions, vertexCount) {
449
+ _setExternalWorkBuffer(centers, covariancesA, covariancesB, colors, splatPositions, vertexCount, shTextures, shDegree = 0, rotationTextures) {
441
450
  this._covariancesATexture = covariancesA;
442
451
  this._covariancesBTexture = covariancesB;
443
452
  this._centersTexture = centers;
444
453
  this._colorsTexture = colors;
445
454
  this._splatPositions = splatPositions;
446
455
  this._vertexCount = vertexCount;
456
+ if (shTextures && shTextures.length) {
457
+ this._shTextures = shTextures;
458
+ this._maxShDegree = shDegree;
459
+ this._shDegree = shDegree;
460
+ }
461
+ // Point the voxel-IBL rotation/scale samplers at the work buffer's decoded rotation textures ([rotA, rotB,
462
+ // rotScale]) so a standalone stream can cast/receive voxel-IBL shadows.
463
+ if (rotationTextures && rotationTextures.length >= 3) {
464
+ this._rotationsATexture = rotationTextures[0];
465
+ this._rotationsBTexture = rotationTextures[1];
466
+ this._rotationScaleTexture = rotationTextures[2];
467
+ this._needsRotationScaleTextures = true;
468
+ }
447
469
  this._activeSplatRanges = null;
448
470
  this._activeSplatRangeKey = "";
449
471
  this._activeSplatRenderCount = 0;
@@ -731,9 +753,28 @@ export class GaussianSplattingMeshBase extends Mesh {
731
753
  this._covariancesBTexture = null;
732
754
  this._centersTexture = null;
733
755
  this._colorsTexture = null;
756
+ // When a streaming part is reserved, the four core data textures (centers, covA, covB, colors) are backed
757
+ // by this MRT's attachments instead of standalone RawTextures, so a streaming engine can GPU-decode into
758
+ // the reserved region while static parts still CPU-upload into it. Null in the normal
759
+ // (non-streaming) path, which keeps using RawTextures unchanged.
760
+ this._mrtAtlas = null;
761
+ this._useMrtAtlas = false;
762
+ // Higher-order SH atlas. When a streaming part needs baked SH, `_shTextures` become the single
763
+ // attachments of these render-targetable integer MRTs (one per packed-u32 SH texture) so the streaming engine
764
+ // GPU-decodes SH into the reserved region while static SH parts still CPU-upload into them. Null in the normal
765
+ // path (SH stays plain RawTextures). One MRT per attachment keeps within WebGPU's per-sample byte budget.
766
+ this._shMrtAtlas = null;
767
+ this._useShMrtAtlas = false;
768
+ this._shMrtAtlasTextureCount = 0;
734
769
  this._rotationsATexture = null;
735
770
  this._rotationsBTexture = null;
736
771
  this._rotationScaleTexture = null;
772
+ // Rotation/scale atlas. When a streaming part needs voxel-IBL rotation/scale, the three rotation
773
+ // textures become the attachments of this render-targetable half-float MRT so the streaming engine GPU-decodes
774
+ // rotation/scale into the reserved region while static parts still CPU-upload into it. Null in the normal path
775
+ // (rotation stays plain RawTextures created per-splat by `_makeSplat`).
776
+ this._rotMrtAtlas = null;
777
+ this._useRotMrtAtlas = false;
737
778
  this._rotationDataA = null;
738
779
  this._rotationDataB = null;
739
780
  this._rotationScaleData = null;
@@ -773,6 +814,19 @@ export class GaussianSplattingMeshBase extends Mesh {
773
814
  /** Fired after part-removal validation passes but before the mesh is rebuilt.
774
815
  * Payload is the original (pre-removal) part index. */
775
816
  this.onPartRemovedObservable = new Observable();
817
+ /**
818
+ * Fired just before an existing MRT-backed atlas is recreated to grow it (e.g. adding a part while a streaming
819
+ * part exists). Payload is the OLD atlas. Streaming parts subscribe to back up their GPU-only region before it
820
+ * is disposed. Only fires when growing an existing MRT atlas (not on first creation).
821
+ * @internal
822
+ */
823
+ this._onBeforeAtlasRebuildObservable = new Observable();
824
+ /**
825
+ * Fired after the new (grown) MRT-backed atlas has been created and its CPU data uploaded. Payload is the NEW
826
+ * atlas. Streaming parts subscribe to rebind to it and restore their backed-up region.
827
+ * @internal
828
+ */
829
+ this._onAfterAtlasRebuildObservable = new Observable();
776
830
  /**
777
831
  * Cosine value of the angle threshold to update view dependent splat sorting. Default is 0.0001.
778
832
  */
@@ -1887,18 +1941,49 @@ export class GaussianSplattingMeshBase extends Mesh {
1887
1941
  * @param doNotRecurse Set to true to not recurse into each children (recurse into each children by default)
1888
1942
  */
1889
1943
  dispose(doNotRecurse) {
1890
- this._covariancesATexture?.dispose();
1891
- this._covariancesBTexture?.dispose();
1892
- this._centersTexture?.dispose();
1893
- this._colorsTexture?.dispose();
1894
- if (this._shTextures) {
1944
+ if (this._mrtAtlas) {
1945
+ // The four data textures are attachments of this MRT — dispose the MRT, not each attachment.
1946
+ this._mrtAtlas.dispose();
1947
+ this._mrtAtlas = null;
1948
+ this._covariancesATexture = this._covariancesBTexture = this._centersTexture = this._colorsTexture = null;
1949
+ }
1950
+ else {
1951
+ this._covariancesATexture?.dispose();
1952
+ this._covariancesBTexture?.dispose();
1953
+ this._centersTexture?.dispose();
1954
+ this._colorsTexture?.dispose();
1955
+ }
1956
+ if (this._shMrtAtlas) {
1957
+ // SH attachments belong to these MRTs — dispose the MRTs, not each attachment (which is _shTextures[k]
1958
+ // for k < _shMrtAtlas.length).
1959
+ for (const mrt of this._shMrtAtlas) {
1960
+ mrt.dispose();
1961
+ }
1962
+ // Defence in depth: free any _shTextures entries beyond the MRT-backed set (not MRT-owned, so the loop
1963
+ // above misses them). The reuse guard keeps the atlas sized correctly, so this is normally a no-op.
1964
+ if (this._shTextures) {
1965
+ for (let k = this._shMrtAtlas.length; k < this._shTextures.length; k++) {
1966
+ this._shTextures[k].dispose();
1967
+ }
1968
+ }
1969
+ this._shMrtAtlas = null;
1970
+ }
1971
+ else if (this._shTextures) {
1895
1972
  for (const shTexture of this._shTextures) {
1896
1973
  shTexture.dispose();
1897
1974
  }
1898
1975
  }
1899
- this._rotationsATexture?.dispose();
1900
- this._rotationsBTexture?.dispose();
1901
- this._rotationScaleTexture?.dispose();
1976
+ this._shTextures = null;
1977
+ if (this._rotMrtAtlas) {
1978
+ // The three rotation textures are attachments of this MRT — dispose the MRT, not each attachment.
1979
+ this._rotMrtAtlas.dispose();
1980
+ this._rotMrtAtlas = null;
1981
+ }
1982
+ else {
1983
+ this._rotationsATexture?.dispose();
1984
+ this._rotationsBTexture?.dispose();
1985
+ this._rotationScaleTexture?.dispose();
1986
+ }
1902
1987
  this._sogParams?.codebookTexture?.dispose();
1903
1988
  this._rotationsATexture = null;
1904
1989
  this._rotationsBTexture = null;
@@ -1994,8 +2079,10 @@ export class GaussianSplattingMeshBase extends Mesh {
1994
2079
  * @param maximum - accumulated bounding maximum (updated in-place)
1995
2080
  * @param flipY - whether to negate the Y position
1996
2081
  * @param srcIndex - source splat index (defaults to dstIndex when omitted)
2082
+ * @param dstArrayIndex - index into the covA/covB/colorArray transients (defaults to dstIndex). Lets a streaming
2083
+ * write pass count-sized transients indexed from 0 while dstIndex still addresses the atlas-sized _splatPositions.
1997
2084
  */
1998
- _makeSplat(dstIndex, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum, flipY, srcIndex = dstIndex) {
2085
+ _makeSplat(dstIndex, fBuffer, uBuffer, covA, covB, colorArray, minimum, maximum, flipY, srcIndex = dstIndex, dstArrayIndex = dstIndex) {
1999
2086
  const matrixRotation = TmpVectors.Matrix[0];
2000
2087
  const matrixScale = TmpVectors.Matrix[1];
2001
2088
  const quaternion = TmpVectors.Quaternion[0];
@@ -2013,10 +2100,13 @@ export class GaussianSplattingMeshBase extends Mesh {
2013
2100
  quaternion.toRotationMatrix(matrixRotation);
2014
2101
  Matrix.ScalingToRef(fBuffer[8 * srcIndex + 3 + 0] * 2, fBuffer[8 * srcIndex + 3 + 1] * 2, fBuffer[8 * srcIndex + 3 + 2] * 2, matrixScale);
2015
2102
  if (this._needsRotationScaleTextures) {
2016
- if (!this._rotationDataA || this._rotationDataA.length < covA.length) {
2017
- this._rotationDataA = new Uint16Array(covA.length);
2018
- this._rotationDataB = new Uint16Array(covA.length);
2019
- this._rotationScaleData = new Uint16Array(covA.length);
2103
+ // Sized to the atlas (_splatPositions), not covA: these persistent arrays are indexed by the global
2104
+ // dstIndex, so a count-sized covA transient must not shrink them.
2105
+ const rotLength = this._splatPositions.length;
2106
+ if (!this._rotationDataA || this._rotationDataA.length < rotLength) {
2107
+ this._rotationDataA = new Uint16Array(rotLength);
2108
+ this._rotationDataB = new Uint16Array(rotLength);
2109
+ this._rotationScaleData = new Uint16Array(rotLength);
2020
2110
  }
2021
2111
  const rotDataA = this._rotationDataA;
2022
2112
  const rotDataB = this._rotationDataB;
@@ -2051,12 +2141,12 @@ export class GaussianSplattingMeshBase extends Mesh {
2051
2141
  }
2052
2142
  this._splatPositions[4 * dstIndex + 3] = factor;
2053
2143
  const transform = factor;
2054
- covA[dstIndex * 4 + 0] = ToHalfFloat(covariances[0] / transform);
2055
- covA[dstIndex * 4 + 1] = ToHalfFloat(covariances[1] / transform);
2056
- covA[dstIndex * 4 + 2] = ToHalfFloat(covariances[2] / transform);
2057
- covA[dstIndex * 4 + 3] = ToHalfFloat(covariances[3] / transform);
2058
- covB[dstIndex * covBSItemSize + 0] = ToHalfFloat(covariances[4] / transform);
2059
- covB[dstIndex * covBSItemSize + 1] = ToHalfFloat(covariances[5] / transform);
2144
+ covA[dstArrayIndex * 4 + 0] = ToHalfFloat(covariances[0] / transform);
2145
+ covA[dstArrayIndex * 4 + 1] = ToHalfFloat(covariances[1] / transform);
2146
+ covA[dstArrayIndex * 4 + 2] = ToHalfFloat(covariances[2] / transform);
2147
+ covA[dstArrayIndex * 4 + 3] = ToHalfFloat(covariances[3] / transform);
2148
+ covB[dstArrayIndex * covBSItemSize + 0] = ToHalfFloat(covariances[4] / transform);
2149
+ covB[dstArrayIndex * covBSItemSize + 1] = ToHalfFloat(covariances[5] / transform);
2060
2150
  const c0 = covariances[0];
2061
2151
  const c1 = covariances[1];
2062
2152
  const c2 = covariances[2];
@@ -2072,10 +2162,10 @@ export class GaussianSplattingMeshBase extends Mesh {
2072
2162
  this._splatSizeMax = splatSize;
2073
2163
  }
2074
2164
  // colors
2075
- colorArray[dstIndex * 4 + 0] = uBuffer[32 * srcIndex + 24 + 0];
2076
- colorArray[dstIndex * 4 + 1] = uBuffer[32 * srcIndex + 24 + 1];
2077
- colorArray[dstIndex * 4 + 2] = uBuffer[32 * srcIndex + 24 + 2];
2078
- colorArray[dstIndex * 4 + 3] = uBuffer[32 * srcIndex + 24 + 3];
2165
+ colorArray[dstArrayIndex * 4 + 0] = uBuffer[32 * srcIndex + 24 + 0];
2166
+ colorArray[dstArrayIndex * 4 + 1] = uBuffer[32 * srcIndex + 24 + 1];
2167
+ colorArray[dstArrayIndex * 4 + 2] = uBuffer[32 * srcIndex + 24 + 2];
2168
+ colorArray[dstArrayIndex * 4 + 3] = uBuffer[32 * srcIndex + 24 + 3];
2079
2169
  }
2080
2170
  _onUpdateTextures(_textureSize) { }
2081
2171
  /**
@@ -2106,6 +2196,137 @@ export class GaussianSplattingMeshBase extends Mesh {
2106
2196
  _setDelayedTextureUpdate(covA, covB, colorArray, sh) {
2107
2197
  this._delayedTextureUpdate = { covA, covB, colors: colorArray, centers: this._splatPositions, sh };
2108
2198
  }
2199
+ /**
2200
+ * Creates (or recreates) the MRT-backed data atlas at the given size and points the four core data
2201
+ * textures at its attachments. The attachments use the same decoded GS layout as {@link GaussianSplattingWorkBuffer}
2202
+ * — [centers F32, covA HALF_FLOAT, covB HALF_FLOAT, colors U8], all RGBA — so a streaming engine can render
2203
+ * (decode/relayout) directly into the same textures the GS material samples, while static parts CPU-upload
2204
+ * into them via {@link _updateSubTextures}. Covariance B is RGBA here (its extra channels unused), so the
2205
+ * atlas forces {@link _useRGBACovariants}.
2206
+ * @param textureSize atlas dimensions (width, height)
2207
+ */
2208
+ _createMrtAtlas(textureSize) {
2209
+ // covB carries only Sigma12/Sigma22 but MRT attachments are RGBA, so switch to the RGBA covariant layout.
2210
+ this._useRGBACovariants = true;
2211
+ const engine = this.getEngine();
2212
+ // No real GPU (NullEngine): skip MRT allocation. The reservation's part/index/range logic still runs;
2213
+ // texture population requires a real (WebGL2/WebGPU) backend.
2214
+ const updateEngine = this._getTextureDataUpdateEngine();
2215
+ if (!updateEngine._gl && !updateEngine.isWebGPU) {
2216
+ return;
2217
+ }
2218
+ const covType = engine.getCaps().textureHalfFloatRender ? 2 : 1;
2219
+ const mrt = new MultiRenderTarget(this.name + "_gsAtlas", { width: textureSize.x, height: textureSize.y }, 4, this._scene, {
2220
+ types: [1, covType, covType, 0],
2221
+ formats: [5, 5, 5, 5],
2222
+ samplingModes: [
2223
+ 1,
2224
+ 1,
2225
+ 1,
2226
+ 1,
2227
+ ],
2228
+ generateDepthBuffer: false,
2229
+ generateDepthTexture: false,
2230
+ generateMipMaps: false,
2231
+ }, [this.name + "_gsCenters", this.name + "_gsCovA", this.name + "_gsCovB", this.name + "_gsColors"]);
2232
+ mrt.clearColor = new Color4(0, 0, 0, 0);
2233
+ mrt.renderList = [];
2234
+ // Suppress clearing so a streaming engine's decode passes ACCUMULATE into the atlas (each decode writes
2235
+ // only its region and must not wipe static parts or previously-decoded splats). All texels are defined by
2236
+ // the initial full CPU sub-upload below (the reserved region's arrays are zero => invisible padding).
2237
+ mrt.onClearObservable.add(() => { });
2238
+ // Render once so the attachment textures are really allocated on the GPU before the CPU sub-uploads
2239
+ // below (on WebGPU, writeTexture requires the destination texture to already exist).
2240
+ mrt.render();
2241
+ this._mrtAtlas = mrt;
2242
+ this._centersTexture = mrt.textures[0];
2243
+ this._covariancesATexture = mrt.textures[1];
2244
+ this._covariancesBTexture = mrt.textures[2];
2245
+ this._colorsTexture = mrt.textures[3];
2246
+ }
2247
+ /**
2248
+ * Creates the shared higher-order SH atlas: `_shMrtAtlasTextureCount` single-attachment integer render targets
2249
+ * (RGBA_INTEGER / UNSIGNED_INTEGER, packed-u32), sized to the whole atlas, so a streaming engine can GPU-decode
2250
+ * baked SH into the reserved region and static SH parts CPU-upload into the same attachments. `_shTextures` (the
2251
+ * draw path's `shTexture0..N` samplers) are the attachments. One MRT per attachment keeps within WebGPU's
2252
+ * per-sample color-attachment byte budget. No-op without a real GPU backend (mirrors {@link _createMrtAtlas}).
2253
+ * @param textureSize atlas dimensions (width, height)
2254
+ */
2255
+ _createShMrtAtlas(textureSize) {
2256
+ const updateEngine = this._getTextureDataUpdateEngine();
2257
+ if (!updateEngine._gl && !updateEngine.isWebGPU) {
2258
+ return;
2259
+ }
2260
+ const count = this._shMrtAtlasTextureCount;
2261
+ if (count <= 0) {
2262
+ return;
2263
+ }
2264
+ const mrts = [];
2265
+ const shTextures = [];
2266
+ for (let k = 0; k < count; k++) {
2267
+ const name = `${this.name}_gsShAtlas${k}`;
2268
+ const mrt = new MultiRenderTarget(name, { width: textureSize.x, height: textureSize.y }, 1, this._scene, {
2269
+ types: [7],
2270
+ formats: [11],
2271
+ samplingModes: [1],
2272
+ generateDepthBuffer: false,
2273
+ generateDepthTexture: false,
2274
+ generateMipMaps: false,
2275
+ }, [name]);
2276
+ mrt.clearColor = new Color4(0, 0, 0, 0);
2277
+ mrt.renderList = [];
2278
+ // Accumulate: each SH decode writes only its region; the rest stays whatever was there (neutral for a
2279
+ // fresh atlas — 128 == 0 lighting is guaranteed by decoding every splat's region, incl. no-SH files).
2280
+ mrt.onClearObservable.add(() => { });
2281
+ const attachment = mrt.textures[0];
2282
+ attachment.wrapU = 0;
2283
+ attachment.wrapV = 0;
2284
+ // Allocate on the GPU before any CPU sub-upload (WebGPU writeTexture needs the resource to exist).
2285
+ mrt.render();
2286
+ mrts.push(mrt);
2287
+ shTextures.push(attachment);
2288
+ }
2289
+ this._shMrtAtlas = mrts;
2290
+ this._shTextures = shTextures;
2291
+ }
2292
+ /**
2293
+ * Creates the shared rotation/scale atlas: one 3-attachment half-float render target ([rotA, rotB, rotScale])
2294
+ * sized to the whole atlas, so a streaming engine can GPU-decode rotation/scale into the reserved region and
2295
+ * static parts CPU-upload into the same attachments. `_rotationsATexture`/`_rotationsBTexture`/
2296
+ * `_rotationScaleTexture` (the voxel-IBL samplers) become the three attachments. No-op without a real GPU
2297
+ * backend (mirrors {@link _createMrtAtlas}).
2298
+ * @param textureSize atlas dimensions (width, height)
2299
+ */
2300
+ _createRotMrtAtlas(textureSize) {
2301
+ const updateEngine = this._getTextureDataUpdateEngine();
2302
+ if (!updateEngine._gl && !updateEngine.isWebGPU) {
2303
+ return;
2304
+ }
2305
+ const rotType = this.getEngine().getCaps().textureHalfFloatRender ? 2 : 1;
2306
+ const name = this.name + "_gsRotAtlas";
2307
+ const mrt = new MultiRenderTarget(name, { width: textureSize.x, height: textureSize.y }, 3, this._scene, {
2308
+ types: [rotType, rotType, rotType],
2309
+ formats: [5, 5, 5],
2310
+ samplingModes: [1, 1, 1],
2311
+ generateDepthBuffer: false,
2312
+ generateDepthTexture: false,
2313
+ generateMipMaps: false,
2314
+ }, [name + "A", name + "B", name + "Scale"]);
2315
+ mrt.clearColor = new Color4(0, 0, 0, 0);
2316
+ mrt.renderList = [];
2317
+ // Accumulate: each rotation decode writes only its region; the rest stays whatever was there.
2318
+ mrt.onClearObservable.add(() => { });
2319
+ for (const attachment of mrt.textures) {
2320
+ attachment.wrapU = 0;
2321
+ attachment.wrapV = 0;
2322
+ }
2323
+ // Allocate on the GPU before any CPU sub-upload (WebGPU writeTexture needs the resource to exist).
2324
+ mrt.render();
2325
+ this._rotMrtAtlas = mrt;
2326
+ this._rotationsATexture = mrt.textures[0];
2327
+ this._rotationsBTexture = mrt.textures[1];
2328
+ this._rotationScaleTexture = mrt.textures[2];
2329
+ }
2109
2330
  // NB: partIndices is assumed to be padded to a round texture size
2110
2331
  _updateTextures(covA, covB, colorArray, sh) {
2111
2332
  const textureSize = this._getTextureSize(this._vertexCount);
@@ -2124,7 +2345,21 @@ export class GaussianSplattingMeshBase extends Mesh {
2124
2345
  };
2125
2346
  const firstTime = this._covariancesATexture === null;
2126
2347
  const textureSizeChanged = this._textureSize.y != textureSize.y;
2127
- if (!firstTime && !textureSizeChanged) {
2348
+ // The render-backed SH atlas must have one attachment per SH texture the material samples, i.e. sized to the
2349
+ // compound's MERGED SH degree (`sh` spans static + streaming), not just the streaming parts' requested count —
2350
+ // otherwise a lower-degree stream mixed with a higher-degree static part (or a later higher-degree stream)
2351
+ // leaves the atlas undersized. `sh` is always present when a streaming SH part is live (its degree makes it so).
2352
+ if (this._useShMrtAtlas && sh) {
2353
+ this._shMrtAtlasTextureCount = sh.length;
2354
+ }
2355
+ // First streaming-part reservation must convert the existing RawTexture atlas to the MRT-backed atlas,
2356
+ // which requires the full (re)build branch even when the texture height is unchanged. Same for the SH atlas:
2357
+ // a streaming SH part must convert the SH textures to render-targetable integer MRTs (and recreate them when
2358
+ // the required attachment count changes).
2359
+ const needsMrtConversion = (this._useMrtAtlas && !this._mrtAtlas) ||
2360
+ (this._useShMrtAtlas && (!this._shMrtAtlas || this._shMrtAtlas.length !== this._shMrtAtlasTextureCount)) ||
2361
+ (this._useRotMrtAtlas && !this._rotMrtAtlas);
2362
+ if (!firstTime && !textureSizeChanged && !needsMrtConversion) {
2128
2363
  this._setDelayedTextureUpdate(covA, covB, colorArray, sh);
2129
2364
  const positions = Float32Array.from(this._splatPositions);
2130
2365
  const vertexCount = this._vertexCount;
@@ -2133,8 +2368,9 @@ export class GaussianSplattingMeshBase extends Mesh {
2133
2368
  // Re-sync the active interval set in case the source splat count changed.
2134
2369
  this._postIntervalsToWorker();
2135
2370
  }
2136
- // Handle SH textures in update path - create if they don't exist
2137
- if (sh && !this._shTextures) {
2371
+ // Handle SH textures in update path - create if they don't exist. Skip when the SH atlas is
2372
+ // render-backed (_useShMrtAtlas): those targets are created/managed by _createShMrtAtlas, not here.
2373
+ if (sh && !this._shTextures && !this._useShMrtAtlas) {
2138
2374
  this._shTextures = [];
2139
2375
  for (let textureIndex = 0; textureIndex < sh.length; textureIndex++) {
2140
2376
  const shTexture = createEmptyTextureU32(textureSize.x, textureSize.y, 11);
@@ -2169,40 +2405,126 @@ export class GaussianSplattingMeshBase extends Mesh {
2169
2405
  else {
2170
2406
  // Full rebuild: the texture size changed (or this is a size-changing reload), so the existing
2171
2407
  // GPU textures cannot be reused. Dispose them before recreating to avoid leaking the old ones.
2172
- this._covariancesATexture?.dispose();
2173
- this._covariancesBTexture?.dispose();
2174
- this._centersTexture?.dispose();
2175
- this._colorsTexture?.dispose();
2408
+ // Growing an EXISTING MRT atlas: let streaming parts back up their GPU-only region from the old atlas
2409
+ // before it is disposed (they restore it into the new atlas via _onAfterAtlasRebuildObservable).
2410
+ const growingMrtAtlas = this._useMrtAtlas && !!this._mrtAtlas;
2411
+ if (growingMrtAtlas) {
2412
+ this._onBeforeAtlasRebuildObservable.notifyObservers(this._mrtAtlas);
2413
+ }
2414
+ if (this._mrtAtlas) {
2415
+ // The four data textures are attachments of this MRT — dispose the MRT, not each attachment.
2416
+ this._mrtAtlas.dispose();
2417
+ this._mrtAtlas = null;
2418
+ this._covariancesATexture = this._covariancesBTexture = this._centersTexture = this._colorsTexture = null;
2419
+ }
2420
+ else {
2421
+ this._covariancesATexture?.dispose();
2422
+ this._covariancesBTexture?.dispose();
2423
+ this._centersTexture?.dispose();
2424
+ this._colorsTexture?.dispose();
2425
+ }
2426
+ // Dispose+null the rotation/scale textures before the MRT-branch _updateSubTextures below: that helper
2427
+ // also writes the rotation textures, so leaving them at the old (smaller) height would upload the taller
2428
+ // region out of bounds. Nulling them makes it skip rotation; the block further down recreates them.
2429
+ if (this._rotMrtAtlas) {
2430
+ // The three rotation textures are attachments of this MRT — dispose the MRT, not each attachment.
2431
+ this._rotMrtAtlas.dispose();
2432
+ this._rotMrtAtlas = null;
2433
+ this._rotationsATexture = null;
2434
+ this._rotationsBTexture = null;
2435
+ this._rotationScaleTexture = null;
2436
+ }
2437
+ else if (this._rotationsATexture) {
2438
+ this._rotationsATexture.dispose();
2439
+ this._rotationsBTexture?.dispose();
2440
+ this._rotationScaleTexture?.dispose();
2441
+ this._rotationsATexture = null;
2442
+ this._rotationsBTexture = null;
2443
+ this._rotationScaleTexture = null;
2444
+ }
2176
2445
  if (this._shTextures) {
2177
- for (const shTexture of this._shTextures) {
2178
- shTexture.dispose();
2446
+ // SH attachments belong to _shMrtAtlas MRTs (dispose the MRT, not each attachment) when render-backed.
2447
+ if (this._shMrtAtlas) {
2448
+ for (const mrt of this._shMrtAtlas) {
2449
+ mrt.dispose();
2450
+ }
2451
+ // Defence in depth: free any _shTextures entries beyond the MRT-backed set (see dispose()).
2452
+ for (let k = this._shMrtAtlas.length; k < this._shTextures.length; k++) {
2453
+ this._shTextures[k].dispose();
2454
+ }
2455
+ this._shMrtAtlas = null;
2456
+ }
2457
+ else {
2458
+ for (const shTexture of this._shTextures) {
2459
+ shTexture.dispose();
2460
+ }
2179
2461
  }
2180
2462
  this._shTextures = null;
2181
2463
  }
2182
2464
  this._textureSize = textureSize;
2183
- this._covariancesATexture = createTextureFromDataF16(covA, textureSize.x, textureSize.y, 5);
2184
- this._covariancesBTexture = createTextureFromDataF16(covB, textureSize.x, textureSize.y, this._useRGBACovariants ? 5 : 7);
2185
- this._centersTexture = createTextureFromData(this._splatPositions, textureSize.x, textureSize.y, 5);
2186
- this._colorsTexture = createTextureFromDataU8(colorArray, textureSize.x, textureSize.y, 5);
2187
- if (sh) {
2188
- this._shTextures = [];
2189
- for (let textureIndex = 0; textureIndex < sh.length; textureIndex++) {
2190
- const shTexture = createEmptyTextureU32(textureSize.x, textureSize.y, 11);
2191
- shTexture.wrapU = 0;
2192
- shTexture.wrapV = 0;
2193
- this._shTextures.push(shTexture);
2465
+ if (this._useMrtAtlas) {
2466
+ // Back the four data textures with a render-targetable MRT so a streaming engine can decode
2467
+ // into the reserved region; static splats are CPU-uploaded into the same attachments below.
2468
+ this._createMrtAtlas(textureSize);
2469
+ // Same for the higher-order SH atlas (integer render targets), when a streaming part needs baked SH.
2470
+ if (this._useShMrtAtlas) {
2471
+ this._createShMrtAtlas(textureSize);
2194
2472
  }
2195
- for (let textureIndex = 0; textureIndex < sh.length; textureIndex++) {
2196
- this._updateShTextureData(this._shTextures[textureIndex], sh[textureIndex], textureSize.x, 0, textureSize.y);
2473
+ // Same for the rotation/scale atlas (half-float), created before the _updateSubTextures below so
2474
+ // static parts' CPU rotation data lands in the shared attachments (streamed rows are decoded after).
2475
+ if (this._useRotMrtAtlas) {
2476
+ this._createRotMrtAtlas(textureSize);
2477
+ }
2478
+ if (this._mrtAtlas) {
2479
+ this._updateSubTextures(this._splatPositions, covA, covB, colorArray, 0, textureSize.y);
2480
+ // Render-backed SH: CPU-upload the static parts' SH into the shared SH atlas HERE — before the
2481
+ // streaming restore below — so a streamed region's restored SH wins over the bulk (neutral) fill,
2482
+ // exactly like the core atlas's _updateSubTextures-then-restore ordering. (`sh` covers the whole
2483
+ // atlas; streamed rows are neutral in it and get overwritten by the restore.)
2484
+ if (this._useShMrtAtlas && sh) {
2485
+ const shTex = this._shTextures;
2486
+ if (shTex) {
2487
+ for (let textureIndex = 0; textureIndex < sh.length && textureIndex < shTex.length; textureIndex++) {
2488
+ this._updateShTextureData(shTex[textureIndex], sh[textureIndex], textureSize.x, 0, textureSize.y);
2489
+ }
2490
+ }
2491
+ }
2492
+ // New atlas is populated (streamed regions currently zeroed); let streaming parts rebind and
2493
+ // restore their backed-up region into it, overwriting those zeros with the preserved data.
2494
+ if (growingMrtAtlas) {
2495
+ this._onAfterAtlasRebuildObservable.notifyObservers(this._mrtAtlas);
2496
+ }
2497
+ }
2498
+ }
2499
+ else {
2500
+ this._covariancesATexture = createTextureFromDataF16(covA, textureSize.x, textureSize.y, 5);
2501
+ this._covariancesBTexture = createTextureFromDataF16(covB, textureSize.x, textureSize.y, this._useRGBACovariants ? 5 : 7);
2502
+ this._centersTexture = createTextureFromData(this._splatPositions, textureSize.x, textureSize.y, 5);
2503
+ this._colorsTexture = createTextureFromDataU8(colorArray, textureSize.x, textureSize.y, 5);
2504
+ }
2505
+ // Render-backed SH (_useShMrtAtlas) was already CPU-uploaded above (before the streaming restore); only
2506
+ // the plain RawTexture SH path is handled here.
2507
+ if (sh && !this._useShMrtAtlas) {
2508
+ {
2509
+ this._shTextures = [];
2510
+ for (let textureIndex = 0; textureIndex < sh.length; textureIndex++) {
2511
+ const shTexture = createEmptyTextureU32(textureSize.x, textureSize.y, 11);
2512
+ shTexture.wrapU = 0;
2513
+ shTexture.wrapV = 0;
2514
+ this._shTextures.push(shTexture);
2515
+ }
2516
+ for (let textureIndex = 0; textureIndex < sh.length; textureIndex++) {
2517
+ this._updateShTextureData(this._shTextures[textureIndex], sh[textureIndex], textureSize.x, 0, textureSize.y);
2518
+ }
2197
2519
  }
2198
2520
  }
2199
- if (this._needsRotationScaleTextures) {
2521
+ // Render-backed rotation (_useRotMrtAtlas) was already created + CPU-uploaded above (via _updateSubTextures,
2522
+ // before the streaming restore); only the plain RawTexture rotation path is handled here.
2523
+ if (this._needsRotationScaleTextures && !this._useRotMrtAtlas) {
2200
2524
  const rotDataA = this._rotationDataA ?? new Uint16Array(covA.length);
2201
2525
  const rotDataB = this._rotationDataB ?? new Uint16Array(covA.length);
2202
2526
  const rotScaleData = this._rotationScaleData ?? new Uint16Array(covA.length);
2203
- this._rotationsATexture?.dispose();
2204
- this._rotationsBTexture?.dispose();
2205
- this._rotationScaleTexture?.dispose();
2527
+ // Already disposed+nulled in the rebuild's disposal block above; just (re)create at the new size.
2206
2528
  this._rotationsATexture = createTextureFromDataF16(rotDataA, textureSize.x, textureSize.y, 5);
2207
2529
  this._rotationsBTexture = createTextureFromDataF16(rotDataB, textureSize.x, textureSize.y, 5);
2208
2530
  this._rotationScaleTexture = createTextureFromDataF16(rotScaleData, textureSize.x, textureSize.y, 5);
@@ -2235,9 +2557,10 @@ export class GaussianSplattingMeshBase extends Mesh {
2235
2557
  * Requires that the GPU textures already exist and the texture height won't change.
2236
2558
  * @param previousVertexCount - The number of splats previously committed to GPU
2237
2559
  * @param vertexCount - The new total number of splats
2560
+ * @param requiredShTextureCount - SH texture count this update requires (the merged `sh.length`), 0 when no SH
2238
2561
  * @returns true when only the new splat region needs to be uploaded
2239
2562
  */
2240
- _canReuseCachedData(previousVertexCount, vertexCount) {
2563
+ _canReuseCachedData(previousVertexCount, vertexCount, requiredShTextureCount = 0) {
2241
2564
  if (previousVertexCount <= 0 || previousVertexCount > vertexCount) {
2242
2565
  return false;
2243
2566
  }
@@ -2247,6 +2570,23 @@ export class GaussianSplattingMeshBase extends Mesh {
2247
2570
  if (this._covariancesATexture === null) {
2248
2571
  return false;
2249
2572
  }
2573
+ // Switching to (or resizing) a render-backed atlas requires a full rebuild so the affected textures are
2574
+ // recreated as MRT attachments — the incremental sub-upload path cannot convert them. These must mirror the
2575
+ // `needsMrtConversion` checks in _updateTextures: if they disagree, an incremental data prep (which fills only
2576
+ // the new splats) would be paired with a full-rebuild upload (which uploads the whole array), zeroing the
2577
+ // existing parts. Covers the first streaming reservation, a later part that introduces/grows SH, and rotation.
2578
+ if (this._useMrtAtlas && !this._mrtAtlas) {
2579
+ return false;
2580
+ }
2581
+ // Compare against the count this update requires, not `_shMrtAtlasTextureCount` — that field is refreshed
2582
+ // only in _updateTextures (skipped on the incremental path), so a higher-degree addPart would pass the guard
2583
+ // on a stale count and never grow the SH atlas.
2584
+ if (this._useShMrtAtlas && (!this._shMrtAtlas || this._shMrtAtlas.length !== requiredShTextureCount)) {
2585
+ return false;
2586
+ }
2587
+ if (this._useRotMrtAtlas && !this._rotMrtAtlas) {
2588
+ return false;
2589
+ }
2250
2590
  // Can only do an incremental GPU update if texture height doesn't need to grow
2251
2591
  const newTextureSize = this._getTextureSize(vertexCount);
2252
2592
  return newTextureSize.y === this._textureSize.y;
@@ -2286,6 +2626,80 @@ export class GaussianSplattingMeshBase extends Mesh {
2286
2626
  this._worker.postMessage({ command: GaussianSplattingSortWorkerCommand.POSITIONS_UPDATE, offset: floatOffset, data }, [data.buffer]);
2287
2627
  this._sortIsDirty = true;
2288
2628
  }
2629
+ /**
2630
+ * Decodes raw `.splat` bytes (32 bytes/splat) into a contiguous sub-range of the already-allocated atlas,
2631
+ * uploading only the affected texels (never touching neighboring parts' texels) and patching just that
2632
+ * range of positions in the sort worker. Used to populate a region reserved by
2633
+ * {@link GaussianSplattingMesh.reserveStreamingPart} — the CPU path a streaming engine uses when GPU
2634
+ * decode/readback is unavailable, and the seam that verifies reservation + interleaving before the
2635
+ * streaming engine exists.
2636
+ *
2637
+ * The atlas textures and `_splatPositions` must already be sized to cover `[globalOffset, globalOffset+count)`
2638
+ * (guaranteed after `reserveStreamingPart`). Bounds of the written centers are accumulated into `min`/`max`
2639
+ * when provided (so the caller can grow the owning part's bounding info).
2640
+ * @param globalOffset first atlas splat index to write
2641
+ * @param count number of splats to write
2642
+ * @param splatsData raw `.splat` bytes for `count` splats (stride 32)
2643
+ * @param min optional running min accumulator for the written centers
2644
+ * @param max optional running max accumulator for the written centers
2645
+ */
2646
+ _writeStreamingSplats(globalOffset, count, splatsData, min, max) {
2647
+ if (!this._splatPositions) {
2648
+ return;
2649
+ }
2650
+ // Validate the range against the atlas and the input length (independent of GPU state): an out-of-range
2651
+ // offset/count would write over another part's texels or allocate a huge transient (the CPU arrays are
2652
+ // atlas-indexed up to `end`); a short input would read past its end.
2653
+ const capacity = this._splatPositions.length / 4;
2654
+ const uBuffer = GaussianSplattingMeshBase._GetSplatDataBytes(splatsData);
2655
+ if (!Number.isInteger(globalOffset) || !Number.isInteger(count) || globalOffset < 0 || count < 0 || globalOffset + count > capacity) {
2656
+ throw new Error(`_writeStreamingSplats: range [${globalOffset}, ${globalOffset + count}) is outside the atlas bounds [0, ${capacity})`);
2657
+ }
2658
+ if (uBuffer.length < count * 32) {
2659
+ throw new Error(`_writeStreamingSplats: splatsData has ${uBuffer.length} bytes, need ${count * 32} for ${count} splats (stride 32)`);
2660
+ }
2661
+ if (count === 0 || !this._covariancesATexture) {
2662
+ return;
2663
+ }
2664
+ const textureSize = this._getTextureSize(this._vertexCount);
2665
+ const width = textureSize.x;
2666
+ const covBSItemSize = this._useRGBACovariants ? 4 : 2;
2667
+ const end = globalOffset + count;
2668
+ const fBuffer = GaussianSplattingMeshBase._GetSplatDataFloats(splatsData);
2669
+ // Transients are sized to `count`, not `end` (globalOffset + count): sizing by atlas position would allocate
2670
+ // tens of MB for a small write near a large atlas's tail. dstIndex still addresses the atlas _splatPositions.
2671
+ const covA = new Uint16Array(count * 4);
2672
+ const covB = new Uint16Array(count * covBSItemSize);
2673
+ const colorArray = new Uint8Array(count * 4);
2674
+ const localMin = min ?? new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
2675
+ const localMax = max ?? new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
2676
+ for (let i = 0; i < count; i++) {
2677
+ this._makeSplat(globalOffset + i, fBuffer, uBuffer, covA, covB, colorArray, localMin, localMax, this._flipY, i, i);
2678
+ }
2679
+ // Retain the written bytes so future full rebuilds / picking see the region's real data.
2680
+ if (this._splatsData) {
2681
+ const dst = new Uint8Array(GaussianSplattingMeshBase._GetSplatDataBytes(this._splatsData).buffer);
2682
+ dst.set(uBuffer.subarray(0, count * 32), globalOffset * 32);
2683
+ }
2684
+ // Upload only the written texels, one contiguous per-row run at a time (rectangular sub-uploads never
2685
+ // clobber texels outside [globalOffset, end), so neighboring parts on a shared boundary row are safe).
2686
+ const positions = this._splatPositions;
2687
+ let gi = globalOffset;
2688
+ while (gi < end) {
2689
+ const row = Math.floor(gi / width);
2690
+ const runEnd = Math.min(end, (row + 1) * width);
2691
+ const col = gi - row * width;
2692
+ const runLen = runEnd - gi;
2693
+ // covA/covB/colorArray are count-sized (0-based), so rebase by globalOffset; positions stays global.
2694
+ const li = gi - globalOffset;
2695
+ this._updateTextureFromDataRect(this._covariancesATexture, new Uint16Array(covA.buffer, li * 4 * 2, runLen * 4), col, row, runLen, 1);
2696
+ this._updateTextureFromDataRect(this._covariancesBTexture, new Uint16Array(covB.buffer, li * covBSItemSize * 2, runLen * covBSItemSize), col, row, runLen, 1);
2697
+ this._updateTextureFromDataRect(this._centersTexture, new Float32Array(positions.buffer, gi * 4 * 4, runLen * 4), col, row, runLen, 1);
2698
+ this._updateTextureFromDataRect(this._colorsTexture, new Uint8Array(colorArray.buffer, li * 4, runLen * 4), col, row, runLen, 1);
2699
+ gi = runEnd;
2700
+ }
2701
+ this._postWorkerPositionsRange(globalOffset, count);
2702
+ }
2289
2703
  *_updateData(data, isAsync, sh, partIndices, { flipY = false, previousVertexCount = 0 } = {}, shDegree) {
2290
2704
  if (!this._covariancesATexture) {
2291
2705
  this._readyToDisplay = false;
@@ -2333,7 +2747,7 @@ export class GaussianSplattingMeshBase extends Mesh {
2333
2747
  // Incremental path: only upload the rows that contain new splats, leaving the already-committed
2334
2748
  // GPU region untouched. Falls through to the full-rebuild path when textures don't exist yet
2335
2749
  // or the texture height needs to grow.
2336
- const incremental = this._canReuseCachedData(previousVertexCount, vertexCount);
2750
+ const incremental = this._canReuseCachedData(previousVertexCount, vertexCount, sh?.length ?? 0);
2337
2751
  if (!incremental) {
2338
2752
  this._splatSizeMin = Infinity;
2339
2753
  this._splatSizeMax = -Infinity;