@cesium/engine 22.3.0 → 23.0.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 (193) hide show
  1. package/Build/Workers/{chunk-EAAH543Q.js → chunk-2GL2EUEE.js} +11 -11
  2. package/Build/Workers/{chunk-EUDHHS7K.js → chunk-2WI4HGJU.js} +6 -6
  3. package/Build/Workers/{chunk-LWZHJWYK.js → chunk-36LBFITA.js} +665 -366
  4. package/Build/Workers/{chunk-7U5YNLF3.js → chunk-3B65YFXC.js} +1 -1
  5. package/Build/Workers/{chunk-D63Z7KKL.js → chunk-4LGFX2P5.js} +11 -11
  6. package/Build/Workers/{chunk-D6OKR3BQ.js → chunk-4UNQX2MP.js} +2 -2
  7. package/Build/Workers/{chunk-BO22JHBX.js → chunk-52K3GJKG.js} +3 -3
  8. package/Build/Workers/{chunk-XLIJV3YZ.js → chunk-56OHFSLH.js} +10 -10
  9. package/Build/Workers/{chunk-AWELU5C6.js → chunk-5A7YSAXT.js} +14 -14
  10. package/Build/Workers/{chunk-T5MJDW43.js → chunk-5JP5FG7U.js} +7 -7
  11. package/Build/Workers/{chunk-NVL7JXAQ.js → chunk-5R7SBUTW.js} +10 -10
  12. package/Build/Workers/chunk-5VKZVPO5.js +1829 -0
  13. package/Build/Workers/{chunk-YHPBT4I5.js → chunk-6FVFM2KB.js} +5 -5
  14. package/Build/Workers/{chunk-WHLGBMBC.js → chunk-6SLGTBB2.js} +5 -5
  15. package/Build/Workers/{chunk-VIQIKMTZ.js → chunk-7KRN6JPQ.js} +14 -14
  16. package/Build/Workers/{chunk-TCOUGKXF.js → chunk-AA33B6PU.js} +4 -4
  17. package/Build/Workers/{chunk-S2KVL7OJ.js → chunk-AP6XWET5.js} +8 -8
  18. package/Build/Workers/{chunk-5PJB7ASO.js → chunk-APDKMX7W.js} +6 -6
  19. package/Build/Workers/{chunk-IXL46IYV.js → chunk-ARCFIOKA.js} +12 -12
  20. package/Build/Workers/{chunk-KIJR26N3.js → chunk-BNBD2YZK.js} +16 -16
  21. package/Build/Workers/{chunk-4R6FGUFP.js → chunk-DT5SJBKA.js} +29 -15
  22. package/Build/Workers/{chunk-FUDC3KOT.js → chunk-ESD4IEOU.js} +5 -5
  23. package/Build/Workers/{chunk-K2PLKJJG.js → chunk-FSDVXW7M.js} +5 -5
  24. package/Build/Workers/{chunk-4BBANFU6.js → chunk-G3YT7M7A.js} +1 -1
  25. package/Build/Workers/{chunk-K4PGSAR5.js → chunk-GN5NEUDT.js} +8 -8
  26. package/Build/Workers/{chunk-OOYICVOK.js → chunk-HYLEKKO5.js} +1 -1
  27. package/Build/Workers/{chunk-FDOLXORR.js → chunk-HYTFXWMY.js} +7 -7
  28. package/Build/Workers/{chunk-7SLNBIZS.js → chunk-IFA7VHHP.js} +560 -277
  29. package/Build/Workers/{chunk-N66PIK46.js → chunk-IYVDOB5V.js} +4 -4
  30. package/Build/Workers/{chunk-ZWWN2CQH.js → chunk-JUGY5AZN.js} +4 -4
  31. package/Build/Workers/{chunk-4O2ARYS7.js → chunk-KPYU6YZG.js} +1 -1
  32. package/Build/Workers/{chunk-TY5JXMVO.js → chunk-L7RDJ4V7.js} +1 -1
  33. package/Build/Workers/{chunk-L4OFWHNI.js → chunk-LDQWNMEC.js} +1 -1
  34. package/Build/Workers/{chunk-N6XBJBI7.js → chunk-MQJG522T.js} +6 -6
  35. package/Build/Workers/{chunk-FS42VX2H.js → chunk-MT5M7RLZ.js} +2 -2
  36. package/Build/Workers/{chunk-7ATKQRBM.js → chunk-N4AWE7TZ.js} +10 -10
  37. package/Build/Workers/{chunk-EOSX23OS.js → chunk-NAYRNPEP.js} +4 -4
  38. package/Build/Workers/{chunk-NO3LMU7T.js → chunk-NNHU33JK.js} +7 -7
  39. package/Build/Workers/{chunk-HWGP4GVC.js → chunk-NQCAZU7S.js} +13 -13
  40. package/Build/Workers/{chunk-6GBGEUWR.js → chunk-NUYLNQKV.js} +2 -2
  41. package/Build/Workers/{chunk-E4YDCSQK.js → chunk-OC6X22PG.js} +9 -9
  42. package/Build/Workers/{chunk-ZGGVEGTA.js → chunk-OHBM4YG4.js} +12 -12
  43. package/Build/Workers/{chunk-6WDMPXQ7.js → chunk-OMXN7YF4.js} +12 -12
  44. package/Build/Workers/{chunk-LK7GEFGB.js → chunk-OQ3EAJNW.js} +2 -2
  45. package/Build/Workers/{chunk-M5DVYXJJ.js → chunk-OQBPOKPZ.js} +6 -6
  46. package/Build/Workers/{chunk-WUWPKBOZ.js → chunk-PQVNT5CJ.js} +6 -6
  47. package/Build/Workers/{chunk-W5NV72VR.js → chunk-QA24Q62E.js} +17 -17
  48. package/Build/Workers/{chunk-PJLRXIMS.js → chunk-QQX7S5QT.js} +17 -17
  49. package/Build/Workers/{chunk-D2PEV362.js → chunk-QTOHIXGF.js} +10 -10
  50. package/Build/Workers/{chunk-MASEF4OV.js → chunk-R7M4XNPP.js} +8 -8
  51. package/Build/Workers/{chunk-2MLJ46ZZ.js → chunk-SGKTBHFQ.js} +13 -13
  52. package/Build/Workers/{chunk-EY5HOJFQ.js → chunk-SQTRWKO2.js} +5 -5
  53. package/Build/Workers/{chunk-AFGP5BXS.js → chunk-T43FXHNC.js} +7 -7
  54. package/Build/Workers/{chunk-IHFAFGR2.js → chunk-UEEGWJJP.js} +2 -2
  55. package/Build/Workers/{chunk-TMGLPEZW.js → chunk-WKXDCVBO.js} +4 -4
  56. package/Build/Workers/{chunk-VRAAQ3FS.js → chunk-Y5AJSEEN.js} +2 -2
  57. package/Build/Workers/{chunk-Y5MRBKQN.js → chunk-ZIW5FCIW.js} +18 -18
  58. package/Build/Workers/combineGeometry.js +24 -24
  59. package/Build/Workers/createBoxGeometry.js +18 -18
  60. package/Build/Workers/createBoxOutlineGeometry.js +16 -16
  61. package/Build/Workers/createCircleGeometry.js +26 -26
  62. package/Build/Workers/createCircleOutlineGeometry.js +19 -19
  63. package/Build/Workers/createCoplanarPolygonGeometry.js +33 -33
  64. package/Build/Workers/createCoplanarPolygonOutlineGeometry.js +31 -31
  65. package/Build/Workers/createCorridorGeometry.js +29 -29
  66. package/Build/Workers/createCorridorOutlineGeometry.js +28 -28
  67. package/Build/Workers/createCylinderGeometry.js +20 -20
  68. package/Build/Workers/createCylinderOutlineGeometry.js +18 -18
  69. package/Build/Workers/createEllipseGeometry.js +26 -26
  70. package/Build/Workers/createEllipseOutlineGeometry.js +19 -19
  71. package/Build/Workers/createEllipsoidGeometry.js +19 -19
  72. package/Build/Workers/createEllipsoidOutlineGeometry.js +18 -18
  73. package/Build/Workers/createFrustumGeometry.js +18 -18
  74. package/Build/Workers/createFrustumOutlineGeometry.js +18 -18
  75. package/Build/Workers/createGeometry.js +24 -24
  76. package/Build/Workers/createGroundPolylineGeometry.js +22 -22
  77. package/Build/Workers/createPlaneGeometry.js +16 -16
  78. package/Build/Workers/createPlaneOutlineGeometry.js +15 -15
  79. package/Build/Workers/createPolygonGeometry.js +32 -32
  80. package/Build/Workers/createPolygonOutlineGeometry.js +30 -30
  81. package/Build/Workers/createPolylineGeometry.js +25 -25
  82. package/Build/Workers/createPolylineVolumeGeometry.js +31 -31
  83. package/Build/Workers/createPolylineVolumeOutlineGeometry.js +27 -27
  84. package/Build/Workers/createRectangleGeometry.js +27 -27
  85. package/Build/Workers/createRectangleOutlineGeometry.js +20 -20
  86. package/Build/Workers/createSimplePolylineGeometry.js +23 -23
  87. package/Build/Workers/createSphereGeometry.js +19 -19
  88. package/Build/Workers/createSphereOutlineGeometry.js +18 -18
  89. package/Build/Workers/createTaskProcessorWorker.js +3 -3
  90. package/Build/Workers/createVectorTileClampedPolylines.js +14 -14
  91. package/Build/Workers/createVectorTileGeometries.js +24 -24
  92. package/Build/Workers/createVectorTilePoints.js +13 -13
  93. package/Build/Workers/createVectorTilePolygons.js +22 -22
  94. package/Build/Workers/createVectorTilePolylines.js +14 -14
  95. package/Build/Workers/createVerticesFromCesium3DTilesTerrain.js +25 -25
  96. package/Build/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js +22 -22
  97. package/Build/Workers/createVerticesFromHeightmap.js +22 -22
  98. package/Build/Workers/createVerticesFromQuantizedTerrainMesh.js +20 -20
  99. package/Build/Workers/createWallGeometry.js +24 -24
  100. package/Build/Workers/createWallOutlineGeometry.js +23 -23
  101. package/Build/Workers/decodeDraco.js +10 -10
  102. package/Build/Workers/decodeGoogleEarthEnterprisePacket.js +5 -5
  103. package/Build/Workers/decodeI3S.js +10 -10
  104. package/Build/Workers/gaussianSplatSorter.js +4 -4
  105. package/Build/Workers/gaussianSplatTextureGenerator.js +4 -4
  106. package/Build/Workers/incrementallyBuildTerrainPicker.js +10 -10
  107. package/Build/Workers/transcodeKTX2.js +6 -6
  108. package/Build/Workers/transferTypedArrayTest.js +1 -1
  109. package/Build/Workers/upsampleQuantizedTerrainMesh.js +22 -22
  110. package/Build/Workers/upsampleVerticesFromCesium3DTilesTerrain.js +25 -25
  111. package/Source/Core/AttributeCompression.js +53 -6
  112. package/Source/Core/Cartesian2.js +689 -684
  113. package/Source/Core/Cartesian3.js +1044 -1043
  114. package/Source/Core/Cartesian4.js +816 -813
  115. package/Source/Core/GoogleMaps.js +23 -2
  116. package/Source/Core/Ion.js +1 -1
  117. package/Source/Core/OpenCageGeocoderService.js +2 -3
  118. package/Source/Core/globalTypes.js +16 -0
  119. package/Source/Renderer/ShaderDestination.js +54 -13
  120. package/Source/Scene/ArcGisMapService.js +1 -1
  121. package/Source/Scene/Billboard.js +7 -4
  122. package/Source/Scene/BillboardCollection.js +64 -25
  123. package/Source/Scene/BufferLoader.js +65 -74
  124. package/Source/Scene/CubeMapPanorama.js +351 -0
  125. package/Source/Scene/DerivedCommand.js +8 -31
  126. package/Source/Scene/DracoLoader.js +79 -78
  127. package/Source/Scene/EquirectangularPanorama.js +264 -0
  128. package/Source/Scene/FrameState.js +7 -0
  129. package/Source/Scene/GaussianSplatPrimitive.js +906 -167
  130. package/Source/Scene/GlobeSurfaceShaderSet.js +36 -32
  131. package/Source/Scene/GltfBufferViewLoader.js +109 -118
  132. package/Source/Scene/GltfDracoLoader.js +167 -176
  133. package/Source/Scene/GltfImageLoader.js +91 -101
  134. package/Source/Scene/GltfIndexBufferLoader.js +228 -241
  135. package/Source/Scene/GltfJsonLoader.js +90 -99
  136. package/Source/Scene/GltfLoader.js +313 -324
  137. package/Source/Scene/GltfSpzLoader.js +127 -134
  138. package/Source/Scene/GltfStructuralMetadataLoader.js +181 -186
  139. package/Source/Scene/GltfTextureLoader.js +217 -231
  140. package/Source/Scene/GltfVertexBufferLoader.js +267 -276
  141. package/Source/Scene/GoogleStreetViewCubeMapPanoramaProvider.js +341 -0
  142. package/Source/Scene/Label.js +6 -4
  143. package/Source/Scene/LabelCollection.js +59 -1
  144. package/Source/Scene/MetadataClassProperty.js +212 -7
  145. package/Source/Scene/MetadataComponentType.js +162 -127
  146. package/Source/Scene/MetadataSchemaLoader.js +60 -67
  147. package/Source/Scene/MetadataTable.js +14 -0
  148. package/Source/Scene/MetadataTableProperty.js +14 -0
  149. package/Source/Scene/Model/B3dmLoader.js +216 -224
  150. package/Source/Scene/Model/CustomShader.js +6 -1
  151. package/Source/Scene/Model/CustomShaderPipelineStage.js +122 -7
  152. package/Source/Scene/Model/GeoJsonLoader.js +53 -61
  153. package/Source/Scene/Model/GeometryPipelineStage.js +17 -3
  154. package/Source/Scene/Model/I3dmLoader.js +232 -240
  155. package/Source/Scene/Model/MaterialPipelineStage.js +1 -1
  156. package/Source/Scene/Model/MetadataPipelineStage.js +438 -22
  157. package/Source/Scene/Model/PntsLoader.js +78 -89
  158. package/Source/Scene/Panorama.js +53 -0
  159. package/Source/Scene/PanoramaProvider.js +43 -0
  160. package/Source/Scene/PointPrimitiveCollection.js +14 -22
  161. package/Source/Scene/PropertyTable.js +43 -0
  162. package/Source/Scene/PropertyTexture.js +1 -1
  163. package/Source/Scene/PropertyTextureProperty.js +0 -97
  164. package/Source/Scene/ResourceLoader.js +84 -92
  165. package/Source/Scene/Scene.js +12 -0
  166. package/Source/Scene/SkyBox.js +29 -164
  167. package/Source/Scene/StructuralMetadata.js +18 -3
  168. package/Source/Scene/parseStructuralMetadata.js +244 -0
  169. package/Source/Shaders/BillboardCollectionFS.glsl +66 -40
  170. package/Source/Shaders/BillboardCollectionFS.js +67 -40
  171. package/Source/Shaders/BillboardCollectionVS.glsl +22 -31
  172. package/Source/Shaders/BillboardCollectionVS.js +22 -31
  173. package/Source/Shaders/Builtin/CzmBuiltins.js +4 -0
  174. package/Source/Shaders/Builtin/Functions/decodeRGB8.glsl +22 -0
  175. package/Source/Shaders/Builtin/Functions/decodeRGB8.js +24 -0
  176. package/Source/Shaders/Builtin/Functions/unpackTexture.glsl +35 -0
  177. package/Source/Shaders/Builtin/Functions/unpackTexture.js +37 -0
  178. package/Source/Shaders/CubeMapPanoramaVS.glsl +11 -0
  179. package/Source/Shaders/CubeMapPanoramaVS.js +13 -0
  180. package/Source/Shaders/Model/MetadataStageFS.glsl +2 -1
  181. package/Source/Shaders/Model/MetadataStageFS.js +2 -1
  182. package/Source/Shaders/Model/MetadataStageVS.glsl +2 -1
  183. package/Source/Shaders/Model/MetadataStageVS.js +2 -1
  184. package/Source/Shaders/Model/ModelFS.glsl +2 -2
  185. package/Source/Shaders/Model/ModelFS.js +2 -2
  186. package/Source/Shaders/Model/ModelVS.glsl +2 -2
  187. package/Source/Shaders/Model/ModelVS.js +2 -2
  188. package/Source/Shaders/PointPrimitiveCollectionVS.glsl +8 -39
  189. package/Source/Shaders/PointPrimitiveCollectionVS.js +8 -39
  190. package/index.d.ts +564 -189
  191. package/index.js +127 -119
  192. package/package.json +1 -1
  193. package/Build/Workers/chunk-LYBNPUEI.js +0 -1443
@@ -23,6 +23,7 @@ import BufferUsage from "../Renderer/BufferUsage.js";
23
23
  import RenderState from "../Renderer/RenderState.js";
24
24
  import clone from "../Core/clone.js";
25
25
  import defined from "../Core/defined.js";
26
+ import DeveloperError from "../Core/DeveloperError.js";
26
27
  import VertexAttributeSemantic from "./VertexAttributeSemantic.js";
27
28
  import AttributeType from "./AttributeType.js";
28
29
  import ModelComponents from "./ModelComponents.js";
@@ -39,14 +40,568 @@ const scratchMatrix4B = new Matrix4();
39
40
  const scratchMatrix4C = new Matrix4();
40
41
  const scratchMatrix4D = new Matrix4();
41
42
 
43
+ /**
44
+ * Runtime state machine for steady-state re-sorting of an already committed snapshot.
45
+ *
46
+ * The transition points are in {@link GaussianSplatPrimitive#update}:
47
+ * - IDLE -> WAITING/SORTING when a new steady sort request is scheduled
48
+ * - WAITING -> SORTING when a sorter promise becomes available
49
+ * - SORTING -> SORTED when the active promise resolves with a valid result
50
+ * - SORTED -> IDLE after rebuilding the draw command with fresh indexes
51
+ * - Any state -> ERROR when the active sort promise rejects
52
+ *
53
+ * @private
54
+ */
42
55
  const GaussianSplatSortingState = {
56
+ // No steady sort request is in flight. update() may decide to start one.
43
57
  IDLE: 0,
58
+ // A steady sort was requested, but sorter capacity was unavailable this frame.
44
59
  WAITING: 1,
60
+ // A steady sort promise is in flight and results are pending.
45
61
  SORTING: 2,
62
+ // A valid sorted index buffer is available and waiting to be committed to draw command.
46
63
  SORTED: 3,
64
+ // The active sort request failed; update() throws the captured error.
47
65
  ERROR: 4,
48
66
  };
49
67
 
68
+ /**
69
+ * Snapshot lifecycle for rebuilding aggregated splat data.
70
+ *
71
+ * Transition order:
72
+ * BUILDING -> TEXTURE_PENDING -> TEXTURE_READY -> SORTING -> READY
73
+ *
74
+ * The transition points are split across two functions:
75
+ * - {@link GaussianSplatPrimitive#update} drives BUILDING/SORTING/READY
76
+ * - {@link GaussianSplatPrimitive.generateSplatTexture} drives TEXTURE_PENDING/TEXTURE_READY
77
+ *
78
+ * A snapshot is committed only when it reaches READY, so all GPU resources
79
+ * and sorted indexes swap atomically as one unit.
80
+ *
81
+ * @private
82
+ */
83
+ const SnapshotState = {
84
+ // CPU aggregation is complete and this snapshot is ready to start async texture generation.
85
+ BUILDING: "BUILDING",
86
+ // Async texture generation/upload is in flight for this snapshot.
87
+ TEXTURE_PENDING: "TEXTURE_PENDING",
88
+ // Attribute textures are ready; snapshot can now request index sorting.
89
+ TEXTURE_READY: "TEXTURE_READY",
90
+ // Sort request is in flight for this snapshot generation.
91
+ SORTING: "SORTING",
92
+ // Sorted indexes were validated for this generation and may be committed.
93
+ READY: "READY",
94
+ };
95
+
96
+ /**
97
+ * Aggregated Gaussian splat snapshot data that is built asynchronously and
98
+ * atomically committed once all required resources are ready.
99
+ *
100
+ * @typedef {object} GaussianSplatPrimitive.Snapshot
101
+ * @property {number} generation Monotonic data generation token.
102
+ * @property {Float32Array} positions Packed splat positions (xyz).
103
+ * @property {Float32Array} rotations Packed splat rotations (xyzw).
104
+ * @property {Float32Array} scales Packed splat scales (xyz).
105
+ * @property {Uint8Array} colors Packed splat colors (rgba).
106
+ * @property {Uint32Array|undefined} shData Packed spherical harmonics data.
107
+ * @property {number} sphericalHarmonicsDegree Spherical harmonics degree.
108
+ * @property {number} shCoefficientCount Coefficients per splat.
109
+ * @property {number} numSplats Total splat count in this snapshot.
110
+ * @property {Uint32Array|undefined} indexes Sorted index buffer when READY.
111
+ * @property {Texture|undefined} gaussianSplatTexture Packed splat attribute texture.
112
+ * @property {Texture|undefined} sphericalHarmonicsTexture Packed SH texture.
113
+ * @property {number} lastTextureWidth Last splat texture width.
114
+ * @property {number} lastTextureHeight Last splat texture height.
115
+ * @property {string} state Current snapshot lifecycle state from {@link SnapshotState}.
116
+ * @private
117
+ */
118
+
119
+ /**
120
+ * Packed spherical harmonics texture payload.
121
+ *
122
+ * @typedef {object} GaussianSplatPrimitive.SphericalHarmonicsTextureData
123
+ * @property {number} width Texture width in texels.
124
+ * @property {number} height Texture height in texels.
125
+ * @property {ArrayBufferView} data Packed unsigned integer texture data.
126
+ * @private
127
+ */
128
+
129
+ /**
130
+ * Packed Gaussian splat attribute texture payload.
131
+ *
132
+ * @typedef {object} GaussianSplatPrimitive.AttributeTextureData
133
+ * @property {number} width Texture width in texels.
134
+ * @property {number} height Texture height in texels.
135
+ * @property {ArrayBufferView} data Packed unsigned integer texture data.
136
+ * @private
137
+ */
138
+
139
+ // Two stable frames avoids rebuilding during brief selected-tile jitter.
140
+ const DEFAULT_STABLE_FRAMES = 2;
141
+ // If selection keeps changing, force a rebuild after ~0.5s at 60fps to guarantee progress.
142
+ // Lower values react faster but can thrash on noisy LOD transitions.
143
+ // Higher values reduce rebuild churn but keep stale snapshots visible longer.
144
+ const DEFAULT_MAX_SNAPSHOT_STALL_FRAMES = 30;
145
+ // Minimum delay between steady re-sort requests once the camera is moving.
146
+ const DEFAULT_SORT_MIN_FRAME_INTERVAL = 3;
147
+ // ~0.5 degree camera direction change threshold before triggering steady re-sort.
148
+ const DEFAULT_SORT_MIN_ANGLE_RADIANS = 0.008726646259971648;
149
+ // Minimum camera movement in world units before triggering steady re-sort.
150
+ const DEFAULT_SORT_MIN_POSITION_DELTA = 1.0;
151
+
152
+ /**
153
+ * Determines whether the camera has moved or rotated enough since the last
154
+ * steady sort to justify scheduling a new one.
155
+ *
156
+ * Returns {@code true} when any of the following hold:
157
+ * - No previous steady sort has been recorded yet.
158
+ * - The camera position has moved by at least {@link DEFAULT_SORT_MIN_POSITION_DELTA} world units.
159
+ * - The camera direction has changed by at least {@link DEFAULT_SORT_MIN_ANGLE_RADIANS} radians.
160
+ *
161
+ * A minimum frame interval ({@link DEFAULT_SORT_MIN_FRAME_INTERVAL}) is
162
+ * enforced to prevent re-sorting every single frame.
163
+ *
164
+ * @param {GaussianSplatPrimitive} primitive The splat primitive to check.
165
+ * @param {FrameState} frameState The current frame state.
166
+ * @returns {boolean} Whether a new steady sort should begin.
167
+ * @private
168
+ */
169
+ function shouldStartSteadySort(primitive, frameState) {
170
+ const framesSinceLastSort =
171
+ primitive._lastSteadySortFrameNumber >= 0
172
+ ? frameState.frameNumber - primitive._lastSteadySortFrameNumber
173
+ : Number.POSITIVE_INFINITY;
174
+ if (
175
+ primitive._lastSteadySortFrameNumber >= 0 &&
176
+ framesSinceLastSort < DEFAULT_SORT_MIN_FRAME_INTERVAL
177
+ ) {
178
+ return false;
179
+ }
180
+
181
+ const camera = frameState.camera;
182
+ if (!defined(camera)) {
183
+ return false;
184
+ }
185
+ if (
186
+ !primitive._hasLastSteadySortCameraPosition ||
187
+ !primitive._hasLastSteadySortCameraDirection
188
+ ) {
189
+ return true;
190
+ }
191
+
192
+ const positionDelta = Cartesian3.distance(
193
+ camera.positionWC,
194
+ primitive._lastSteadySortCameraPosition,
195
+ );
196
+ if (positionDelta >= DEFAULT_SORT_MIN_POSITION_DELTA) {
197
+ return true;
198
+ }
199
+
200
+ const angleDelta = Cartesian3.angleBetween(
201
+ camera.directionWC,
202
+ primitive._lastSteadySortCameraDirection,
203
+ );
204
+ return angleDelta >= DEFAULT_SORT_MIN_ANGLE_RADIANS;
205
+ }
206
+
207
+ /**
208
+ * Records the frame number and camera pose at the start of a steady sort so
209
+ * that {@link shouldStartSteadySort} can later compute deltas.
210
+ *
211
+ * @param {GaussianSplatPrimitive} primitive The splat primitive to update.
212
+ * @param {FrameState} frameState The current frame state.
213
+ * @private
214
+ */
215
+ function markSteadySortStart(primitive, frameState) {
216
+ primitive._lastSteadySortFrameNumber = frameState.frameNumber;
217
+ const camera = frameState.camera;
218
+ if (!defined(camera)) {
219
+ return;
220
+ }
221
+ Cartesian3.clone(camera.positionWC, primitive._lastSteadySortCameraPosition);
222
+ primitive._hasLastSteadySortCameraPosition = true;
223
+ Cartesian3.clone(
224
+ camera.directionWC,
225
+ primitive._lastSteadySortCameraDirection,
226
+ );
227
+ primitive._hasLastSteadySortCameraDirection = true;
228
+ }
229
+
230
+ /**
231
+ * Checks whether the set of currently selected tiles differs from the set
232
+ * recorded on the primitive. This is used to detect LOD transitions that
233
+ * require a snapshot rebuild.
234
+ *
235
+ * @param {GaussianSplatPrimitive} primitive The splat primitive.
236
+ * @param {Cesium3DTile[]} selectedTiles The tiles selected this frame.
237
+ * @returns {boolean} {@code true} if the tile set has changed.
238
+ * @private
239
+ */
240
+ function haveSelectedTilesChanged(primitive, selectedTiles) {
241
+ const prevSet = primitive._selectedTileSet;
242
+ if (!defined(prevSet) || prevSet.size !== selectedTiles.length) {
243
+ return true;
244
+ }
245
+
246
+ for (let i = 0; i < selectedTiles.length; i++) {
247
+ if (!prevSet.has(selectedTiles[i])) {
248
+ return true;
249
+ }
250
+ }
251
+
252
+ return false;
253
+ }
254
+
255
+ /**
256
+ * Returns whether the given sort result still matches the primitive's current
257
+ * sort request and data generation, i.e. it has not been superseded.
258
+ *
259
+ * @param {GaussianSplatPrimitive} primitive The splat primitive.
260
+ * @param {object} activeSort The sort result to validate.
261
+ * @returns {boolean} {@code true} if the sort result is still current.
262
+ * @private
263
+ */
264
+ function isActiveSort(primitive, activeSort) {
265
+ return (
266
+ defined(activeSort) &&
267
+ activeSort.requestId === primitive._sortRequestId &&
268
+ activeSort.dataGeneration === primitive._splatDataGeneration
269
+ );
270
+ }
271
+
272
+ /**
273
+ * Destroys the GPU textures owned by a snapshot, if any, and clears the
274
+ * references so they are not used after destruction.
275
+ *
276
+ * @param {GaussianSplatPrimitive.Snapshot|undefined} snapshot The snapshot whose textures should be destroyed.
277
+ * @private
278
+ */
279
+ function destroySnapshotTextures(snapshot) {
280
+ if (!defined(snapshot)) {
281
+ return;
282
+ }
283
+ if (defined(snapshot.gaussianSplatTexture)) {
284
+ snapshot.gaussianSplatTexture.destroy();
285
+ snapshot.gaussianSplatTexture = undefined;
286
+ }
287
+ if (defined(snapshot.sphericalHarmonicsTexture)) {
288
+ snapshot.sphericalHarmonicsTexture.destroy();
289
+ snapshot.sphericalHarmonicsTexture = undefined;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Schedules a GPU texture for deferred destruction. The texture is kept alive
295
+ * for one additional frame so that any in-flight draw commands that reference
296
+ * it can finish before the underlying GPU resource is released.
297
+ *
298
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
299
+ * @param {Texture|undefined} texture The texture to retire.
300
+ * @param {number} frameNumber The frame number at which the texture was retired.
301
+ * @private
302
+ */
303
+ function retireTexture(primitive, texture, frameNumber) {
304
+ if (!defined(texture)) {
305
+ return;
306
+ }
307
+ const retired = primitive._retiredTextures;
308
+ retired.push({
309
+ texture: texture,
310
+ frameNumber: frameNumber,
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Destroys any retired textures whose grace period (one frame) has elapsed.
316
+ * Called once per frame to reclaim GPU memory from textures that were replaced
317
+ * by a newer snapshot.
318
+ *
319
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
320
+ * @param {number} frameNumber The current frame number.
321
+ * @private
322
+ */
323
+ function releaseRetiredTextures(primitive, frameNumber) {
324
+ const retired = primitive._retiredTextures;
325
+ if (!defined(retired) || retired.length === 0) {
326
+ return;
327
+ }
328
+ const next = [];
329
+ for (let i = 0; i < retired.length; i++) {
330
+ const entry = retired[i];
331
+ if (frameNumber - entry.frameNumber > 0) {
332
+ entry.texture.destroy();
333
+ } else {
334
+ next.push(entry);
335
+ }
336
+ }
337
+ primitive._retiredTextures = next;
338
+ }
339
+
340
+ /**
341
+ * Atomically promotes a fully-built snapshot to be the active splat data for
342
+ * the primitive. This includes swapping attribute arrays, GPU textures, and
343
+ * sorted indexes, as well as retiring any previously active textures so they
344
+ * can be safely destroyed after the current frame finishes.
345
+ *
346
+ * The snapshot <b>must</b> be in the {@link SnapshotState.READY} state;
347
+ * otherwise a {@link DeveloperError} is thrown.
348
+ *
349
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
350
+ * @param {GaussianSplatPrimitive.Snapshot} snapshot The snapshot to commit.
351
+ * @param {FrameState} frameState The current frame state.
352
+ * @throws {DeveloperError} If the snapshot is not READY.
353
+ * @private
354
+ */
355
+ function commitSnapshot(primitive, snapshot, frameState) {
356
+ if (!defined(snapshot.indexes) || snapshot.state !== SnapshotState.READY) {
357
+ throw new DeveloperError("Committing snapshot before it is READY.");
358
+ }
359
+
360
+ const frameNumber = frameState.frameNumber;
361
+ const currentSnapshot = primitive._snapshot;
362
+ const splatTexture = defined(currentSnapshot)
363
+ ? currentSnapshot.gaussianSplatTexture
364
+ : primitive.gaussianSplatTexture;
365
+ if (defined(splatTexture) && splatTexture !== snapshot.gaussianSplatTexture) {
366
+ retireTexture(primitive, splatTexture, frameNumber);
367
+ }
368
+
369
+ const sphericalHarmonicsTexture = defined(currentSnapshot)
370
+ ? currentSnapshot.sphericalHarmonicsTexture
371
+ : primitive.sphericalHarmonicsTexture;
372
+ if (
373
+ defined(sphericalHarmonicsTexture) &&
374
+ sphericalHarmonicsTexture !== snapshot.sphericalHarmonicsTexture
375
+ ) {
376
+ retireTexture(primitive, sphericalHarmonicsTexture, frameNumber);
377
+ }
378
+
379
+ primitive._snapshot = snapshot;
380
+ primitive._positions = snapshot.positions;
381
+ primitive._rotations = snapshot.rotations;
382
+ primitive._scales = snapshot.scales;
383
+ primitive._colors = snapshot.colors;
384
+ primitive._shData = snapshot.shData;
385
+ primitive._sphericalHarmonicsDegree = snapshot.sphericalHarmonicsDegree;
386
+ primitive._numSplats = snapshot.numSplats;
387
+ primitive._indexes = snapshot.indexes;
388
+ primitive.gaussianSplatTexture = snapshot.gaussianSplatTexture;
389
+ primitive.sphericalHarmonicsTexture = snapshot.sphericalHarmonicsTexture;
390
+ primitive._lastTextureWidth = snapshot.lastTextureWidth;
391
+ primitive._lastTextureHeight = snapshot.lastTextureHeight;
392
+ primitive._hasGaussianSplatTexture = defined(snapshot.gaussianSplatTexture);
393
+ primitive._needsGaussianSplatTexture = false;
394
+ primitive._gaussianSplatTexturePending = false;
395
+
396
+ primitive._vertexArray = undefined;
397
+ primitive._vertexArrayLen = -1;
398
+ primitive._drawCommand = undefined;
399
+ primitive._sorterPromise = undefined;
400
+ primitive._activeSort = undefined;
401
+ primitive._sorterState = GaussianSplatSortingState.IDLE;
402
+ primitive._dirty = false;
403
+ }
404
+
405
+ /**
406
+ * Finalizes async splat texture generation for a snapshot. The resolved data
407
+ * updates or recreates GPU textures, and the snapshot transitions to
408
+ * {@link SnapshotState.TEXTURE_READY} when complete.
409
+ *
410
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
411
+ * @param {FrameState} frameState The current frame state.
412
+ * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
413
+ * @param {Promise<GaussianSplatPrimitive.AttributeTextureData>} promise Promise that resolves to packed splat texture data.
414
+ * @returns {Promise<void>}
415
+ * @private
416
+ */
417
+ async function processGeneratedSplatTextureData(
418
+ primitive,
419
+ frameState,
420
+ snapshot,
421
+ promise,
422
+ ) {
423
+ try {
424
+ const splatTextureData = await promise;
425
+ if (primitive._pendingSnapshot !== snapshot) {
426
+ snapshot.state = SnapshotState.BUILDING;
427
+ return;
428
+ }
429
+ if (!defined(snapshot.gaussianSplatTexture)) {
430
+ snapshot.gaussianSplatTexture = createGaussianSplatTexture(
431
+ frameState.context,
432
+ splatTextureData,
433
+ );
434
+ } else if (
435
+ snapshot.lastTextureHeight !== splatTextureData.height ||
436
+ snapshot.lastTextureWidth !== splatTextureData.width
437
+ ) {
438
+ const oldTex = snapshot.gaussianSplatTexture;
439
+ snapshot.gaussianSplatTexture = createGaussianSplatTexture(
440
+ frameState.context,
441
+ splatTextureData,
442
+ );
443
+ oldTex.destroy();
444
+ } else {
445
+ snapshot.gaussianSplatTexture.copyFrom({
446
+ source: {
447
+ width: splatTextureData.width,
448
+ height: splatTextureData.height,
449
+ arrayBufferView: splatTextureData.data,
450
+ },
451
+ });
452
+ }
453
+ snapshot.lastTextureHeight = splatTextureData.height;
454
+ snapshot.lastTextureWidth = splatTextureData.width;
455
+
456
+ if (defined(snapshot.shData) && snapshot.sphericalHarmonicsDegree > 0) {
457
+ const oldTex = snapshot.sphericalHarmonicsTexture;
458
+ const width = ContextLimits.maximumTextureSize;
459
+ const dims = snapshot.shCoefficientCount / 3;
460
+ const splatsPerRow = Math.floor(width / dims);
461
+ const floatsPerRow = splatsPerRow * (dims * 2);
462
+ const texBuf = new Uint32Array(
463
+ width * Math.ceil(snapshot.numSplats / splatsPerRow) * 2,
464
+ );
465
+
466
+ let dataIndex = 0;
467
+ for (let i = 0; dataIndex < snapshot.shData.length; i += width * 2) {
468
+ texBuf.set(
469
+ snapshot.shData.subarray(dataIndex, dataIndex + floatsPerRow),
470
+ i,
471
+ );
472
+ dataIndex += floatsPerRow;
473
+ }
474
+ snapshot.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
475
+ frameState.context,
476
+ {
477
+ data: texBuf,
478
+ width: width,
479
+ height: Math.ceil(snapshot.numSplats / splatsPerRow),
480
+ },
481
+ );
482
+ if (defined(oldTex)) {
483
+ oldTex.destroy();
484
+ }
485
+ }
486
+
487
+ snapshot.state = SnapshotState.TEXTURE_READY;
488
+ } catch (error) {
489
+ console.error("Error generating Gaussian splat texture:", error);
490
+ snapshot.state = SnapshotState.BUILDING;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Resolves an in-flight sort for a pending snapshot, validates that the result
496
+ * still matches the active generation/request, and commits the snapshot when
497
+ * valid.
498
+ *
499
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
500
+ * @param {FrameState} frameState The current frame state.
501
+ * @param {object|undefined} pendingSort Pending sort metadata.
502
+ * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
503
+ * @returns {Promise<void>}
504
+ * @private
505
+ */
506
+ async function resolvePendingSnapshotSort(
507
+ primitive,
508
+ frameState,
509
+ pendingSort,
510
+ sortPromise,
511
+ ) {
512
+ try {
513
+ const sortedData = await sortPromise;
514
+ if (
515
+ !defined(pendingSort) ||
516
+ pendingSort.snapshot !== primitive._pendingSnapshot
517
+ ) {
518
+ return;
519
+ }
520
+ const expectedCount = pendingSort.expectedCount;
521
+ const currentCount = expectedCount;
522
+ const sortedLen = sortedData?.length;
523
+ if (expectedCount !== currentCount || sortedLen !== expectedCount) {
524
+ primitive._pendingSortPromise = undefined;
525
+ primitive._pendingSort = undefined;
526
+ if (pendingSort.snapshot.state === SnapshotState.SORTING) {
527
+ pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
528
+ }
529
+ return;
530
+ }
531
+
532
+ const pending = pendingSort.snapshot;
533
+ pending.indexes = sortedData;
534
+ pending.state = SnapshotState.READY;
535
+ primitive._pendingSortPromise = undefined;
536
+ primitive._pendingSort = undefined;
537
+ commitSnapshot(primitive, pending, frameState);
538
+ primitive._pendingSnapshot = undefined;
539
+ GaussianSplatPrimitive.buildGSplatDrawCommand(primitive, frameState);
540
+ } catch (err) {
541
+ if (
542
+ !defined(pendingSort) ||
543
+ pendingSort.snapshot !== primitive._pendingSnapshot
544
+ ) {
545
+ return;
546
+ }
547
+ primitive._pendingSortPromise = undefined;
548
+ primitive._pendingSort = undefined;
549
+ if (pendingSort.snapshot.state === SnapshotState.SORTING) {
550
+ pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
551
+ }
552
+ primitive._sorterState = GaussianSplatSortingState.ERROR;
553
+ primitive._sorterError = err;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Resolves an in-flight steady-state sort for the current committed snapshot.
559
+ * Results are ignored when superseded; otherwise, they advance sorting state
560
+ * to {@link GaussianSplatSortingState.SORTED}.
561
+ *
562
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
563
+ * @param {object|undefined} activeSort Active sort metadata.
564
+ * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
565
+ * @returns {Promise<void>}
566
+ * @private
567
+ */
568
+ async function resolveSteadySort(primitive, activeSort, sortPromise) {
569
+ try {
570
+ const sortedData = await sortPromise;
571
+ const isActive = isActiveSort(primitive, activeSort);
572
+ const expectedCount = activeSort?.expectedCount;
573
+ const currentCount = expectedCount;
574
+ const sortedLen = sortedData?.length;
575
+ const isMismatch =
576
+ expectedCount !== currentCount || sortedLen !== expectedCount;
577
+ if (!isActive || isMismatch) {
578
+ if (isActive) {
579
+ primitive._sorterPromise = undefined;
580
+ primitive._sorterState = GaussianSplatSortingState.IDLE;
581
+ }
582
+ return;
583
+ }
584
+ primitive._indexes = sortedData;
585
+ primitive._sorterState = GaussianSplatSortingState.SORTED;
586
+ } catch (err) {
587
+ if (!isActiveSort(primitive, activeSort)) {
588
+ return;
589
+ }
590
+ primitive._sorterState = GaussianSplatSortingState.ERROR;
591
+ primitive._sorterError = err;
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Creates a GPU texture that stores packed spherical harmonics coefficient
597
+ * data for all splats. The texture uses a two-channel unsigned-integer format
598
+ * ({@link PixelFormat.RG_INTEGER}) and nearest-neighbor sampling.
599
+ *
600
+ * @param {Context} context The WebGL context.
601
+ * @param {GaussianSplatPrimitive.SphericalHarmonicsTextureData} shData Packed SH texture payload.
602
+ * @returns {Texture} The created texture.
603
+ * @private
604
+ */
50
605
  function createSphericalHarmonicsTexture(context, shData) {
51
606
  const texture = new Texture({
52
607
  context: context,
@@ -66,6 +621,17 @@ function createSphericalHarmonicsTexture(context, shData) {
66
621
  return texture;
67
622
  }
68
623
 
624
+ /**
625
+ * Creates a GPU texture that stores the packed Gaussian splat attributes
626
+ * (positions, scales, rotations, colors). The texture uses an RGBA
627
+ * unsigned-integer format ({@link PixelFormat.RGBA_INTEGER}) and
628
+ * nearest-neighbor sampling.
629
+ *
630
+ * @param {Context} context The WebGL context.
631
+ * @param {GaussianSplatPrimitive.AttributeTextureData} splatTextureData Packed splat texture payload.
632
+ * @returns {Texture} The created texture.
633
+ * @private
634
+ */
69
635
  function createGaussianSplatTexture(context, splatTextureData) {
70
636
  return new Texture({
71
637
  context: context,
@@ -144,6 +710,21 @@ function GaussianSplatPrimitive(options) {
144
710
  * @private
145
711
  */
146
712
  this._needsGaussianSplatTexture = true;
713
+ this._snapshot = undefined;
714
+ this._pendingSnapshot = undefined;
715
+ this._retiredTextures = [];
716
+
717
+ /**
718
+ * Scratch buffer re-used across frames for aggregating packed spherical
719
+ * harmonics data so that a fresh typed-array allocation is avoided on
720
+ * every tile-selection change.
721
+ * @type {undefined|Uint32Array}
722
+ * @private
723
+ */
724
+ this._scratchAggregateShBuffer = undefined;
725
+ this._selectedTilesStableFrames = 0;
726
+ this._needsSnapshotRebuild = false;
727
+ this._snapshotRebuildStallFrames = 0;
147
728
 
148
729
  /**
149
730
  * The previous view matrix used to determine if the primitive needs to be updated.
@@ -230,6 +811,7 @@ function GaussianSplatPrimitive(options) {
230
811
  * @private
231
812
  */
232
813
  this.selectedTileLength = 0;
814
+ this._selectedTileSet = new Set();
233
815
 
234
816
  /**
235
817
  * Indicates whether or not the primitive is ready for use.
@@ -258,6 +840,7 @@ function GaussianSplatPrimitive(options) {
258
840
  * @private
259
841
  */
260
842
  this._drawCommand = undefined;
843
+ this._drawCommandModelMatrix = new Matrix4();
261
844
  /**
262
845
  * The root transform of the tileset.
263
846
  * This is used to transform the splats into world space.
@@ -298,6 +881,16 @@ function GaussianSplatPrimitive(options) {
298
881
  * @private
299
882
  */
300
883
  this._sorterPromise = undefined;
884
+ this._splatDataGeneration = 0;
885
+ this._sortRequestId = 0;
886
+ this._activeSort = undefined;
887
+ this._pendingSortPromise = undefined;
888
+ this._pendingSort = undefined;
889
+ this._lastSteadySortFrameNumber = -1;
890
+ this._lastSteadySortCameraPosition = new Cartesian3();
891
+ this._hasLastSteadySortCameraPosition = false;
892
+ this._lastSteadySortCameraDirection = new Cartesian3();
893
+ this._hasLastSteadySortCameraDirection = false;
301
894
 
302
895
  /**
303
896
  * An error that occurred during the Gaussian splat sorting operation.
@@ -321,6 +914,23 @@ Object.defineProperties(GaussianSplatPrimitive.prototype, {
321
914
  },
322
915
  },
323
916
 
917
+ /**
918
+ * Indicates whether the primitive has completed loading and sorting.
919
+ * @memberof GaussianSplatPrimitive.prototype
920
+ * @type {boolean}
921
+ * @private
922
+ * @readonly
923
+ */
924
+ isStable: {
925
+ get: function () {
926
+ return (
927
+ !this._dirty &&
928
+ (!defined(this._pendingSnapshot) ||
929
+ this._pendingSnapshot.state === SnapshotState.READY)
930
+ );
931
+ },
932
+ },
933
+
324
934
  /**
325
935
  * The {@link SplitDirection} to apply to this point.
326
936
  * @memberof GaussianSplatPrimitive.prototype
@@ -362,10 +972,18 @@ GaussianSplatPrimitive.prototype.destroy = function () {
362
972
  this._scales = undefined;
363
973
  this._colors = undefined;
364
974
  this._indexes = undefined;
365
- if (defined(this.gaussianSplatTexture)) {
366
- this.gaussianSplatTexture.destroy();
367
- this.gaussianSplatTexture = undefined;
975
+ destroySnapshotTextures(this._pendingSnapshot);
976
+ destroySnapshotTextures(this._snapshot);
977
+ if (defined(this._retiredTextures)) {
978
+ for (let i = 0; i < this._retiredTextures.length; i++) {
979
+ this._retiredTextures[i].texture.destroy();
980
+ }
368
981
  }
982
+ this._retiredTextures = [];
983
+ this._pendingSnapshot = undefined;
984
+ this._snapshot = undefined;
985
+ this.gaussianSplatTexture = undefined;
986
+ this.sphericalHarmonicsTexture = undefined;
369
987
 
370
988
  const drawCommand = this._drawCommand;
371
989
  if (defined(drawCommand)) {
@@ -528,74 +1146,39 @@ GaussianSplatPrimitive.transformTile = function (tile) {
528
1146
  *
529
1147
  * @see {@link GaussianSplatTextureGenerator}
530
1148
  *
531
- * @param {GaussianSplatPrimitive} primitive
532
- * @param {FrameState} frameState
1149
+ * @param {GaussianSplatPrimitive} primitive The owning primitive.
1150
+ * @param {FrameState} frameState The current frame state.
1151
+ * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
533
1152
  * @private
534
1153
  */
535
- GaussianSplatPrimitive.generateSplatTexture = function (primitive, frameState) {
536
- primitive._gaussianSplatTexturePending = true;
1154
+ GaussianSplatPrimitive.generateSplatTexture = function (
1155
+ primitive,
1156
+ frameState,
1157
+ snapshot,
1158
+ ) {
1159
+ if (!defined(snapshot) || snapshot.state !== SnapshotState.BUILDING) {
1160
+ return;
1161
+ }
1162
+ snapshot.state = SnapshotState.TEXTURE_PENDING;
537
1163
  const promise = GaussianSplatTextureGenerator.generateFromAttributes({
538
1164
  attributes: {
539
- positions: new Float32Array(primitive._positions),
540
- scales: new Float32Array(primitive._scales),
541
- rotations: new Float32Array(primitive._rotations),
542
- colors: new Uint8Array(primitive._colors),
1165
+ positions: new Float32Array(snapshot.positions),
1166
+ scales: new Float32Array(snapshot.scales),
1167
+ rotations: new Float32Array(snapshot.rotations),
1168
+ colors: new Uint8Array(snapshot.colors),
543
1169
  },
544
- count: primitive._numSplats,
1170
+ count: snapshot.numSplats,
545
1171
  });
546
1172
  if (!defined(promise)) {
547
- primitive._gaussianSplatTexturePending = false;
1173
+ snapshot.state = SnapshotState.BUILDING;
548
1174
  return;
549
1175
  }
550
- promise
551
- .then((splatTextureData) => {
552
- if (!primitive._gaussianSplatTexture) {
553
- // First frame, so create the texture.
554
- primitive.gaussianSplatTexture = createGaussianSplatTexture(
555
- frameState.context,
556
- splatTextureData,
557
- );
558
- } else if (
559
- primitive._lastTextureHeight !== splatTextureData.height ||
560
- primitive._lastTextureWidth !== splatTextureData.width
561
- ) {
562
- const oldTex = primitive.gaussianSplatTexture;
563
- primitive._gaussianSplatTexture = createGaussianSplatTexture(
564
- frameState.context,
565
- splatTextureData,
566
- );
567
- oldTex.destroy();
568
- } else {
569
- primitive.gaussianSplatTexture.copyFrom({
570
- source: {
571
- width: splatTextureData.width,
572
- height: splatTextureData.height,
573
- arrayBufferView: splatTextureData.data,
574
- },
575
- });
576
- }
577
- primitive._vertexArray = undefined;
578
- primitive._lastTextureHeight = splatTextureData.height;
579
- primitive._lastTextureWidth = splatTextureData.width;
580
-
581
- primitive._hasGaussianSplatTexture = true;
582
- primitive._needsGaussianSplatTexture = false;
583
- primitive._gaussianSplatTexturePending = false;
584
-
585
- if (
586
- !defined(primitive._indexes) ||
587
- primitive._indexes.length < primitive._numSplats
588
- ) {
589
- primitive._indexes = new Uint32Array(primitive._numSplats);
590
- }
591
- for (let i = 0; i < primitive._numSplats; ++i) {
592
- primitive._indexes[i] = i;
593
- }
594
- })
595
- .catch((error) => {
596
- console.error("Error generating Gaussian splat texture:", error);
597
- primitive._gaussianSplatTexturePending = false;
598
- });
1176
+ void processGeneratedSplatTextureData(
1177
+ primitive,
1178
+ frameState,
1179
+ snapshot,
1180
+ promise,
1181
+ );
599
1182
  };
600
1183
 
601
1184
  /**
@@ -706,7 +1289,10 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
706
1289
  return primitive.splitDirection;
707
1290
  };
708
1291
 
709
- renderResources.instanceCount = primitive._numSplats;
1292
+ const instanceCount = defined(primitive._indexes)
1293
+ ? primitive._indexes.length
1294
+ : primitive._numSplats;
1295
+ renderResources.instanceCount = instanceCount;
710
1296
  renderResources.count = 4;
711
1297
  renderResources.primitiveType = PrimitiveType.TRIANGLE_STRIP;
712
1298
  shaderBuilder.addVertexLines(GaussianSplatVS);
@@ -738,10 +1324,10 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
738
1324
  idxAttr.constant = 0;
739
1325
  idxAttr.instanceDivisor = 1;
740
1326
 
741
- if (
1327
+ const needsRebuild =
742
1328
  !defined(primitive._vertexArray) ||
743
- primitive._indexes.length > primitive._vertexArrayLen
744
- ) {
1329
+ primitive._indexes.length > primitive._vertexArrayLen;
1330
+ if (needsRebuild) {
745
1331
  const geometry = new Geometry({
746
1332
  attributes: {
747
1333
  screenQuadPosition: new GeometryAttribute({
@@ -774,7 +1360,7 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
774
1360
  const modelMatrix = Matrix4.multiply(
775
1361
  tileset.modelMatrix,
776
1362
  primitive._rootTransform,
777
- scratchMatrix4B,
1363
+ primitive._drawCommandModelMatrix,
778
1364
  );
779
1365
 
780
1366
  const vertexArrayCache = primitive._vertexArray;
@@ -812,7 +1398,9 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
812
1398
  GaussianSplatPrimitive.prototype.update = function (frameState) {
813
1399
  const tileset = this._tileset;
814
1400
 
815
- if (!tileset.show || tileset._selectedTiles.length === 0) {
1401
+ releaseRetiredTextures(this, frameState.frameNumber);
1402
+
1403
+ if (!tileset.show) {
816
1404
  return;
817
1405
  }
818
1406
 
@@ -824,6 +1412,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
824
1412
  this._dirty = true;
825
1413
  return;
826
1414
  }
1415
+ const hasRootTransform = defined(this._rootTransform);
827
1416
 
828
1417
  if (frameState.passes.pick === true) {
829
1418
  return;
@@ -832,29 +1421,72 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
832
1421
  if (this.splitDirection !== tileset.splitDirection) {
833
1422
  this.splitDirection = tileset.splitDirection;
834
1423
  }
1424
+ const camera = frameState.camera;
1425
+ if (!defined(camera)) {
1426
+ return;
1427
+ }
835
1428
 
836
1429
  if (this._sorterState === GaussianSplatSortingState.IDLE) {
1430
+ const selectedTilesChanged =
1431
+ tileset._selectedTiles.length !== 0 &&
1432
+ haveSelectedTilesChanged(this, tileset._selectedTiles);
1433
+ if (tileset._selectedTiles.length === 0) {
1434
+ this._selectedTilesStableFrames = 0;
1435
+ this._needsSnapshotRebuild = false;
1436
+ this._snapshotRebuildStallFrames = 0;
1437
+ } else if (selectedTilesChanged) {
1438
+ this._selectedTilesStableFrames = 0;
1439
+ } else {
1440
+ this._selectedTilesStableFrames++;
1441
+ }
1442
+ if (selectedTilesChanged || this._dirty) {
1443
+ this._needsSnapshotRebuild = true;
1444
+ }
1445
+ const isStable = this._selectedTilesStableFrames >= DEFAULT_STABLE_FRAMES;
1446
+ const isBootstrap =
1447
+ !defined(this._snapshot) &&
1448
+ !defined(this._pendingSnapshot) &&
1449
+ !defined(this._drawCommand);
1450
+ // This prevents an indefinite wait if selected tiles never settle completely.
1451
+ // In practice, this is the upper bound on "wait-for-stability" before forcing
1452
+ // a rebuild to avoid visible starvation.
1453
+ if (this._needsSnapshotRebuild && tileset._selectedTiles.length !== 0) {
1454
+ this._snapshotRebuildStallFrames++;
1455
+ } else {
1456
+ this._snapshotRebuildStallFrames = 0;
1457
+ }
1458
+ const allowRebuild =
1459
+ isStable ||
1460
+ isBootstrap ||
1461
+ this._snapshotRebuildStallFrames >= DEFAULT_MAX_SNAPSHOT_STALL_FRAMES;
1462
+ const hasPendingWork =
1463
+ this._dirty ||
1464
+ this._needsSnapshotRebuild ||
1465
+ selectedTilesChanged ||
1466
+ defined(this._pendingSnapshot) ||
1467
+ defined(this._pendingSortPromise) ||
1468
+ !defined(this._drawCommand);
837
1469
  if (
838
- !this._dirty &&
839
- Matrix4.equals(frameState.camera.viewMatrix, this._prevViewMatrix)
1470
+ !hasPendingWork &&
1471
+ Matrix4.equals(camera.viewMatrix, this._prevViewMatrix)
840
1472
  ) {
841
- // No need to update if the view matrix hasn't changed and the primitive isn't dirty.
842
1473
  return;
843
1474
  }
844
1475
 
845
1476
  if (
846
1477
  tileset._selectedTiles.length !== 0 &&
847
- tileset._selectedTiles.length !== this.selectedTileLength
1478
+ this._needsSnapshotRebuild &&
1479
+ allowRebuild
848
1480
  ) {
849
- this._numSplats = 0;
850
- this._positions = undefined;
851
- this._rotations = undefined;
852
- this._scales = undefined;
853
- this._colors = undefined;
854
- this._indexes = undefined;
855
- this._shData = undefined;
856
- this._needsGaussianSplatTexture = true;
857
- this._gaussianSplatTexturePending = false;
1481
+ this._splatDataGeneration++;
1482
+ this._activeSort = undefined;
1483
+ this._sorterPromise = undefined;
1484
+ this._sorterState = GaussianSplatSortingState.IDLE;
1485
+ this._pendingSortPromise = undefined;
1486
+ this._pendingSort = undefined;
1487
+ if (defined(this._pendingSnapshot)) {
1488
+ destroySnapshotTextures(this._pendingSnapshot);
1489
+ }
858
1490
 
859
1491
  const tiles = tileset._selectedTiles;
860
1492
  const totalElements = tiles.reduce(
@@ -890,49 +1522,78 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
890
1522
  };
891
1523
 
892
1524
  const aggregateShData = () => {
893
- let offset = 0;
1525
+ // Determine the SH degree from the first tile with SH data so we can
1526
+ // pre-allocate the aggregate buffer once, outside the tile loop.
1527
+ let coefs = 0;
894
1528
  for (const tile of tiles) {
895
- const shData = tile.content.packedSphericalHarmonicsData;
896
1529
  if (tile.content.sphericalHarmonicsDegree > 0) {
897
- if (!defined(this._shData)) {
898
- let coefs;
899
- switch (tile.content.sphericalHarmonicsDegree) {
900
- case 1:
901
- coefs = 9;
902
- break;
903
- case 2:
904
- coefs = 24;
905
- break;
906
- case 3:
907
- coefs = 45;
908
- }
909
- this._shData = new Uint32Array(totalElements * (coefs * (2 / 3)));
1530
+ switch (tile.content.sphericalHarmonicsDegree) {
1531
+ case 1:
1532
+ coefs = 9;
1533
+ break;
1534
+ case 2:
1535
+ coefs = 24;
1536
+ break;
1537
+ case 3:
1538
+ coefs = 45;
1539
+ break;
910
1540
  }
911
- this._shData.set(shData, offset);
912
- offset += shData.length;
1541
+ break;
913
1542
  }
914
1543
  }
1544
+
1545
+ if (coefs === 0) {
1546
+ return undefined;
1547
+ }
1548
+
1549
+ const requiredLength = totalElements * (coefs * (2 / 3));
1550
+
1551
+ // Re-use the class-level scratch buffer when it is already large
1552
+ // enough, avoiding a fresh allocation (and eventual GC) every frame.
1553
+ if (
1554
+ !defined(this._scratchAggregateShBuffer) ||
1555
+ this._scratchAggregateShBuffer.length < requiredLength
1556
+ ) {
1557
+ this._scratchAggregateShBuffer = new Uint32Array(requiredLength);
1558
+ }
1559
+ const aggregate = this._scratchAggregateShBuffer;
1560
+
1561
+ let offset = 0;
1562
+ for (const tile of tiles) {
1563
+ const tileShData = tile.content.packedSphericalHarmonicsData;
1564
+ if (tile.content.sphericalHarmonicsDegree > 0) {
1565
+ aggregate.set(tileShData, offset);
1566
+ offset += tileShData.length;
1567
+ }
1568
+ }
1569
+
1570
+ // Return a correctly-sized view so downstream consumers see the
1571
+ // exact element count they expect.
1572
+ if (offset < aggregate.length) {
1573
+ return aggregate.subarray(0, offset);
1574
+ }
1575
+ return aggregate;
915
1576
  };
916
1577
 
917
- this._positions = aggregateAttributeValues(
1578
+ const positions = aggregateAttributeValues(
918
1579
  ComponentDatatype.FLOAT,
919
1580
  (content) => content.positions,
920
1581
  3,
921
1582
  );
922
1583
 
923
- this._scales = aggregateAttributeValues(
1584
+ const scales = aggregateAttributeValues(
924
1585
  ComponentDatatype.FLOAT,
925
1586
  (content) => content.scales,
926
1587
  3,
927
1588
  );
928
1589
 
929
- this._rotations = aggregateAttributeValues(
1590
+ const rotations = aggregateAttributeValues(
930
1591
  ComponentDatatype.FLOAT,
931
1592
  (content) => content.rotations,
932
1593
  4,
933
1594
  );
934
1595
 
935
- this._colors = aggregateAttributeValues(
1596
+ const colors = aggregateAttributeValues(
936
1597
  ComponentDatatype.UNSIGNED_BYTE,
937
1598
  (content) =>
938
1599
  ModelUtility.getAttributeBySemantic(
@@ -941,66 +1602,133 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
941
1602
  ),
942
1603
  );
943
1604
 
944
- aggregateShData();
945
- this._sphericalHarmonicsDegree =
1605
+ const sphericalHarmonicsDegree =
946
1606
  tiles[0].content.sphericalHarmonicsDegree;
1607
+ const shCoefficientCount =
1608
+ sphericalHarmonicsDegree > 0
1609
+ ? tiles[0].content.sphericalHarmonicsCoefficientCount
1610
+ : 0;
1611
+ const shData = aggregateShData();
1612
+
1613
+ this._pendingSnapshot = {
1614
+ generation: this._splatDataGeneration,
1615
+ positions: positions,
1616
+ rotations: rotations,
1617
+ scales: scales,
1618
+ colors: colors,
1619
+ shData: shData,
1620
+ sphericalHarmonicsDegree: sphericalHarmonicsDegree,
1621
+ shCoefficientCount: shCoefficientCount,
1622
+ numSplats: totalElements,
1623
+ indexes: undefined,
1624
+ gaussianSplatTexture: undefined,
1625
+ sphericalHarmonicsTexture: undefined,
1626
+ lastTextureWidth: 0,
1627
+ lastTextureHeight: 0,
1628
+ state: SnapshotState.BUILDING,
1629
+ };
947
1630
 
948
- this._numSplats = totalElements;
949
1631
  this.selectedTileLength = tileset._selectedTiles.length;
1632
+ this._selectedTileSet = new Set(tileset._selectedTiles);
1633
+ this._dirty = false;
1634
+ this._needsSnapshotRebuild = false;
1635
+ this._snapshotRebuildStallFrames = 0;
1636
+ }
1637
+
1638
+ if (defined(this._pendingSnapshot)) {
1639
+ const pending = this._pendingSnapshot;
1640
+ if (pending.state === SnapshotState.BUILDING) {
1641
+ GaussianSplatPrimitive.generateSplatTexture(this, frameState, pending);
1642
+ return;
1643
+ }
1644
+ if (pending.state === SnapshotState.TEXTURE_PENDING) {
1645
+ return;
1646
+ }
1647
+ if (
1648
+ pending.state === SnapshotState.TEXTURE_READY &&
1649
+ !defined(pending.gaussianSplatTexture)
1650
+ ) {
1651
+ return;
1652
+ }
1653
+
1654
+ if (!hasRootTransform) {
1655
+ return;
1656
+ }
1657
+
1658
+ Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
1659
+ Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
1660
+
1661
+ if (
1662
+ pending.state === SnapshotState.TEXTURE_READY &&
1663
+ !defined(this._pendingSortPromise)
1664
+ ) {
1665
+ const requestId = ++this._sortRequestId;
1666
+ const dataGeneration = this._splatDataGeneration;
1667
+ this._pendingSort = {
1668
+ requestId: requestId,
1669
+ dataGeneration: dataGeneration,
1670
+ expectedCount: pending.numSplats,
1671
+ snapshot: pending,
1672
+ };
1673
+ const sortPromise = GaussianSplatSorter.radixSortIndexes({
1674
+ primitive: {
1675
+ positions: new Float32Array(pending.positions),
1676
+ modelView: Float32Array.from(scratchMatrix4A),
1677
+ count: pending.numSplats,
1678
+ },
1679
+ sortType: "Index",
1680
+ });
1681
+ if (!defined(sortPromise)) {
1682
+ this._pendingSortPromise = undefined;
1683
+ this._pendingSort = undefined;
1684
+ pending.state = SnapshotState.TEXTURE_READY;
1685
+ return;
1686
+ }
1687
+ this._pendingSortPromise = sortPromise;
1688
+ pending.state = SnapshotState.SORTING;
1689
+ const pendingSort = this._pendingSort;
1690
+ void resolvePendingSnapshotSort(
1691
+ this,
1692
+ frameState,
1693
+ pendingSort,
1694
+ sortPromise,
1695
+ );
1696
+ return;
1697
+ }
1698
+
1699
+ if (!defined(this._pendingSortPromise)) {
1700
+ if (pending.state === SnapshotState.SORTING) {
1701
+ pending.state = SnapshotState.TEXTURE_READY;
1702
+ }
1703
+ return;
1704
+ }
1705
+ return;
950
1706
  }
951
1707
 
952
1708
  if (this._numSplats === 0) {
953
1709
  return;
954
1710
  }
955
1711
 
956
- if (this._needsGaussianSplatTexture) {
957
- if (!this._gaussianSplatTexturePending) {
958
- GaussianSplatPrimitive.generateSplatTexture(this, frameState);
959
- if (defined(this._shData)) {
960
- const oldTex = this.sphericalHarmonicsTexture;
961
- const width = ContextLimits.maximumTextureSize;
962
- const dims =
963
- tileset._selectedTiles[0].content
964
- .sphericalHarmonicsCoefficientCount / 3;
965
- const splatsPerRow = Math.floor(width / dims);
966
- const floatsPerRow = splatsPerRow * (dims * 2);
967
- const texBuf = new Uint32Array(
968
- width * Math.ceil(this._numSplats / splatsPerRow) * 2,
969
- );
970
-
971
- let dataIndex = 0;
972
- for (let i = 0; dataIndex < this._shData.length; i += width * 2) {
973
- texBuf.set(
974
- this._shData.subarray(dataIndex, dataIndex + floatsPerRow),
975
- i,
976
- );
977
- dataIndex += floatsPerRow;
978
- }
979
- this.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
980
- frameState.context,
981
- {
982
- data: texBuf,
983
- width: width,
984
- height: Math.ceil(this._numSplats / splatsPerRow),
985
- },
986
- );
987
- if (defined(oldTex)) {
988
- oldTex.destroy();
989
- }
990
- }
991
- }
1712
+ if (!hasRootTransform) {
992
1713
  return;
993
1714
  }
994
1715
 
995
- Matrix4.clone(frameState.camera.viewMatrix, this._prevViewMatrix);
996
- Matrix4.multiply(
997
- frameState.camera.viewMatrix,
998
- this._rootTransform,
999
- scratchMatrix4A,
1000
- );
1716
+ Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
1717
+ Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
1001
1718
 
1002
1719
  if (!defined(this._sorterPromise)) {
1003
- this._sorterPromise = GaussianSplatSorter.radixSortIndexes({
1720
+ if (!shouldStartSteadySort(this, frameState)) {
1721
+ return;
1722
+ }
1723
+ const requestId = ++this._sortRequestId;
1724
+ const dataGeneration = this._splatDataGeneration;
1725
+ const expectedCount = this._numSplats;
1726
+ this._activeSort = {
1727
+ requestId: requestId,
1728
+ dataGeneration: dataGeneration,
1729
+ expectedCount: expectedCount,
1730
+ };
1731
+ const rawPromise = GaussianSplatSorter.radixSortIndexes({
1004
1732
  primitive: {
1005
1733
  positions: new Float32Array(this._positions),
1006
1734
  modelView: Float32Array.from(scratchMatrix4A),
@@ -1008,23 +1736,33 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
1008
1736
  },
1009
1737
  sortType: "Index",
1010
1738
  });
1739
+ this._sorterPromise = rawPromise;
1740
+ if (defined(rawPromise)) {
1741
+ markSteadySortStart(this, frameState);
1742
+ const activeSort = this._activeSort;
1743
+ this._sorterState = GaussianSplatSortingState.SORTING;
1744
+ void resolveSteadySort(this, activeSort, rawPromise);
1745
+ return;
1746
+ }
1011
1747
  }
1012
1748
 
1013
1749
  if (!defined(this._sorterPromise)) {
1014
1750
  this._sorterState = GaussianSplatSortingState.WAITING;
1015
1751
  return;
1016
1752
  }
1017
- this._sorterPromise.catch((err) => {
1018
- this._sorterState = GaussianSplatSortingState.ERROR;
1019
- this._sorterError = err;
1020
- });
1021
- this._sorterPromise.then((sortedData) => {
1022
- this._indexes = sortedData;
1023
- this._sorterState = GaussianSplatSortingState.SORTED;
1024
- });
1753
+ this._sorterState = GaussianSplatSortingState.SORTING;
1754
+ return;
1025
1755
  } else if (this._sorterState === GaussianSplatSortingState.WAITING) {
1026
1756
  if (!defined(this._sorterPromise)) {
1027
- this._sorterPromise = GaussianSplatSorter.radixSortIndexes({
1757
+ const requestId = ++this._sortRequestId;
1758
+ const dataGeneration = this._splatDataGeneration;
1759
+ const expectedCount = this._numSplats;
1760
+ this._activeSort = {
1761
+ requestId: requestId,
1762
+ dataGeneration: dataGeneration,
1763
+ expectedCount: expectedCount,
1764
+ };
1765
+ const rawPromise = GaussianSplatSorter.radixSortIndexes({
1028
1766
  primitive: {
1029
1767
  positions: new Float32Array(this._positions),
1030
1768
  modelView: Float32Array.from(scratchMatrix4A),
@@ -1032,21 +1770,21 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
1032
1770
  },
1033
1771
  sortType: "Index",
1034
1772
  });
1773
+ this._sorterPromise = rawPromise;
1774
+ if (defined(rawPromise)) {
1775
+ markSteadySortStart(this, frameState);
1776
+ const activeSort = this._activeSort;
1777
+ this._sorterState = GaussianSplatSortingState.SORTING;
1778
+ void resolveSteadySort(this, activeSort, rawPromise);
1779
+ return;
1780
+ }
1035
1781
  }
1036
1782
  if (!defined(this._sorterPromise)) {
1037
1783
  this._sorterState = GaussianSplatSortingState.WAITING;
1038
1784
  return;
1039
1785
  }
1040
- this._sorterPromise.catch((err) => {
1041
- this._sorterState = GaussianSplatSortingState.ERROR;
1042
- this._sorterError = err;
1043
- });
1044
- this._sorterPromise.then((sortedData) => {
1045
- this._indexes = sortedData;
1046
- this._sorterState = GaussianSplatSortingState.SORTED;
1047
- });
1048
-
1049
- this._sorterState = GaussianSplatSortingState.SORTING; //set state to sorting
1786
+ this._sorterState = GaussianSplatSortingState.SORTING;
1787
+ return;
1050
1788
  } else if (this._sorterState === GaussianSplatSortingState.SORTING) {
1051
1789
  return; //still sorting, wait for next frame
1052
1790
  } else if (this._sorterState === GaussianSplatSortingState.SORTED) {
@@ -1055,6 +1793,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
1055
1793
  this._sorterState = GaussianSplatSortingState.IDLE; //reset state for next frame
1056
1794
  this._dirty = false;
1057
1795
  this._sorterPromise = undefined; //reset promise for next frame
1796
+ this._activeSort = undefined;
1058
1797
  } else if (this._sorterState === GaussianSplatSortingState.ERROR) {
1059
1798
  throw this._sorterError;
1060
1799
  }