智慧水务管理系统 - 精河县供水工程综合管理平台

GaussianSplatPrimitive.js 71KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147
  1. import Frozen from "../Core/Frozen.js";
  2. import Matrix4 from "../Core/Matrix4.js";
  3. import Matrix3 from "../Core/Matrix3.js";
  4. import ModelUtility from "./Model/ModelUtility.js";
  5. import GaussianSplatSorter from "./GaussianSplatSorter.js";
  6. import GaussianSplatTextureGenerator from "./GaussianSplatTextureGenerator.js";
  7. import ComponentDatatype from "../Core/ComponentDatatype.js";
  8. import PixelDatatype from "../Renderer/PixelDatatype.js";
  9. import PixelFormat from "../Core/PixelFormat.js";
  10. import Sampler from "../Renderer/Sampler.js";
  11. import Texture from "../Renderer/Texture.js";
  12. import GaussianSplatRenderResources from "./GaussianSplatRenderResources.js";
  13. import BlendingState from "./BlendingState.js";
  14. import Pass from "../Renderer/Pass.js";
  15. import ShaderDestination from "../Renderer/ShaderDestination.js";
  16. import GaussianSplatVS from "../Shaders/PrimitiveGaussianSplatVS.js";
  17. import GaussianSplatFS from "../Shaders/PrimitiveGaussianSplatFS.js";
  18. import PrimitiveType from "../Core/PrimitiveType.js";
  19. import DrawCommand from "../Renderer/DrawCommand.js";
  20. import Geometry from "../Core/Geometry.js";
  21. import GeometryAttribute from "../Core/GeometryAttribute.js";
  22. import VertexArray from "../Renderer/VertexArray.js";
  23. import BufferUsage from "../Renderer/BufferUsage.js";
  24. import RenderState from "../Renderer/RenderState.js";
  25. import clone from "../Core/clone.js";
  26. import defined from "../Core/defined.js";
  27. import DeveloperError from "../Core/DeveloperError.js";
  28. import VertexAttributeSemantic from "./VertexAttributeSemantic.js";
  29. import AttributeType from "./AttributeType.js";
  30. import ModelComponents from "./ModelComponents.js";
  31. import Axis from "./Axis.js";
  32. import Cartesian3 from "../Core/Cartesian3.js";
  33. import Quaternion from "../Core/Quaternion.js";
  34. import SplitDirection from "./SplitDirection.js";
  35. import destroyObject from "../Core/destroyObject.js";
  36. import ContextLimits from "../Renderer/ContextLimits.js";
  37. import Transforms from "../Core/Transforms.js";
  38. const scratchMatrix4A = new Matrix4();
  39. const scratchMatrix4C = new Matrix4();
  40. const scratchMatrix4D = new Matrix4();
  41. const scratchMatrix3 = new Matrix3();
  42. const scratchTransformQuat = new Quaternion();
  43. const scratchTransformPosition = new Cartesian3();
  44. const scratchTransformRotation = new Quaternion();
  45. const scratchTransformScale = new Cartesian3();
  46. const TRANSFORM_CACHE_EPSILON = 1e-12;
  47. const RIGID_TRANSFORM_EPSILON = 1e-5;
  48. const UNIT_SCALE_FAST_PATH_EPSILON = 1e-7;
  49. /**
  50. * Runtime state machine for steady-state re-sorting of an already committed snapshot.
  51. *
  52. * The transition points are in {@link GaussianSplatPrimitive#update}:
  53. * - IDLE -> WAITING/SORTING when a new steady sort request is scheduled
  54. * - WAITING -> SORTING when a sorter promise becomes available
  55. * - SORTING -> SORTED when the active promise resolves with a valid result
  56. * - SORTED -> IDLE after rebuilding the draw command with fresh indexes
  57. * - Any state -> ERROR when the active sort promise rejects
  58. *
  59. * @private
  60. */
  61. const GaussianSplatSortingState = {
  62. // No steady sort request is in flight. update() may decide to start one.
  63. IDLE: 0,
  64. // A steady sort was requested, but sorter capacity was unavailable this frame.
  65. WAITING: 1,
  66. // A steady sort promise is in flight and results are pending.
  67. SORTING: 2,
  68. // A valid sorted index buffer is available and waiting to be committed to draw command.
  69. SORTED: 3,
  70. // The active sort request failed; update() throws the captured error.
  71. ERROR: 4,
  72. };
  73. /**
  74. * Snapshot lifecycle for rebuilding aggregated splat data.
  75. *
  76. * Transition order:
  77. * BUILDING -> TEXTURE_PENDING -> TEXTURE_READY -> SORTING -> READY
  78. *
  79. * The transition points are split across two functions:
  80. * - {@link GaussianSplatPrimitive#update} drives BUILDING/SORTING/READY
  81. * - {@link GaussianSplatPrimitive.generateSplatTexture} drives TEXTURE_PENDING/TEXTURE_READY
  82. *
  83. * A snapshot is committed only when it reaches READY, so all GPU resources
  84. * and sorted indexes swap atomically as one unit.
  85. *
  86. * @private
  87. */
  88. const SnapshotState = {
  89. // CPU aggregation is complete and this snapshot is ready to start async texture generation.
  90. BUILDING: "BUILDING",
  91. // Async texture generation/upload is in flight for this snapshot.
  92. TEXTURE_PENDING: "TEXTURE_PENDING",
  93. // Attribute textures are ready; snapshot can now request index sorting.
  94. TEXTURE_READY: "TEXTURE_READY",
  95. // Sort request is in flight for this snapshot generation.
  96. SORTING: "SORTING",
  97. // Sorted indexes were validated for this generation and may be committed.
  98. READY: "READY",
  99. };
  100. /**
  101. * Aggregated Gaussian splat snapshot data that is built asynchronously and
  102. * atomically committed once all required resources are ready.
  103. *
  104. * @typedef {object} GaussianSplatPrimitive.Snapshot
  105. * @property {number} generation Monotonic data generation token.
  106. * @property {Float32Array} positions Packed splat positions (xyz).
  107. * @property {Float32Array} rotations Packed splat rotations (xyzw).
  108. * @property {Float32Array} scales Packed splat scales (xyz).
  109. * @property {Uint8Array} colors Packed splat colors (rgba).
  110. * @property {Uint32Array|undefined} shData Packed spherical harmonics data.
  111. * @property {number} sphericalHarmonicsDegree Spherical harmonics degree.
  112. * @property {number} shCoefficientCount Coefficients per splat.
  113. * @property {number} numSplats Total splat count in this snapshot.
  114. * @property {Uint32Array|undefined} indexes Sorted index buffer when READY.
  115. * @property {Texture|undefined} gaussianSplatTexture Packed splat attribute texture.
  116. * @property {Texture|undefined} sphericalHarmonicsTexture Packed SH texture.
  117. * @property {number} lastTextureWidth Last splat texture width.
  118. * @property {number} lastTextureHeight Last splat texture height.
  119. * @property {string} state Current snapshot lifecycle state from {@link SnapshotState}.
  120. * @private
  121. */
  122. /**
  123. * Packed spherical harmonics texture payload.
  124. *
  125. * @typedef {object} GaussianSplatPrimitive.SphericalHarmonicsTextureData
  126. * @property {number} width Texture width in texels.
  127. * @property {number} height Texture height in texels.
  128. * @property {ArrayBufferView} data Packed unsigned integer texture data.
  129. * @private
  130. */
  131. /**
  132. * Packed Gaussian splat attribute texture payload.
  133. *
  134. * @typedef {object} GaussianSplatPrimitive.AttributeTextureData
  135. * @property {number} width Texture width in texels.
  136. * @property {number} height Texture height in texels.
  137. * @property {ArrayBufferView} data Packed unsigned integer texture data.
  138. * @private
  139. */
  140. // Two stable frames avoids rebuilding during brief selected-tile jitter.
  141. const DEFAULT_STABLE_FRAMES = 2;
  142. // If selection keeps changing, force a rebuild after ~0.5s at 60fps to guarantee progress.
  143. // Lower values react faster but can thrash on noisy LOD transitions.
  144. // Higher values reduce rebuild churn but keep stale snapshots visible longer.
  145. const DEFAULT_MAX_SNAPSHOT_STALL_FRAMES = 30;
  146. // Minimum delay between steady re-sort requests once the camera is moving.
  147. const DEFAULT_SORT_MIN_FRAME_INTERVAL = 3;
  148. // ~0.5 degree camera direction change threshold before triggering steady re-sort.
  149. const DEFAULT_SORT_MIN_ANGLE_RADIANS = 0.008726646259971648;
  150. // Minimum camera movement in world units before triggering steady re-sort.
  151. const DEFAULT_SORT_MIN_POSITION_DELTA = 1.0;
  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. const camera = frameState.camera;
  181. if (!defined(camera)) {
  182. return false;
  183. }
  184. if (
  185. !primitive._hasLastSteadySortCameraPosition ||
  186. !primitive._hasLastSteadySortCameraDirection
  187. ) {
  188. return true;
  189. }
  190. const positionDelta = Cartesian3.distance(
  191. camera.positionWC,
  192. primitive._lastSteadySortCameraPosition,
  193. );
  194. if (positionDelta >= DEFAULT_SORT_MIN_POSITION_DELTA) {
  195. return true;
  196. }
  197. const angleDelta = Cartesian3.angleBetween(
  198. camera.directionWC,
  199. primitive._lastSteadySortCameraDirection,
  200. );
  201. return angleDelta >= DEFAULT_SORT_MIN_ANGLE_RADIANS;
  202. }
  203. /**
  204. * Records the frame number and camera pose at the start of a steady sort so
  205. * that {@link shouldStartSteadySort} can later compute deltas.
  206. *
  207. * @param {GaussianSplatPrimitive} primitive The splat primitive to update.
  208. * @param {FrameState} frameState The current frame state.
  209. * @private
  210. */
  211. function markSteadySortStart(primitive, frameState) {
  212. primitive._lastSteadySortFrameNumber = frameState.frameNumber;
  213. const camera = frameState.camera;
  214. if (!defined(camera)) {
  215. return;
  216. }
  217. Cartesian3.clone(camera.positionWC, primitive._lastSteadySortCameraPosition);
  218. primitive._hasLastSteadySortCameraPosition = true;
  219. Cartesian3.clone(
  220. camera.directionWC,
  221. primitive._lastSteadySortCameraDirection,
  222. );
  223. primitive._hasLastSteadySortCameraDirection = true;
  224. }
  225. /**
  226. * Checks whether the set of currently selected tiles differs from the set
  227. * recorded on the primitive. This is used to detect LOD transitions that
  228. * require a snapshot rebuild.
  229. *
  230. * @param {GaussianSplatPrimitive} primitive The splat primitive.
  231. * @param {Cesium3DTile[]} selectedTiles The tiles selected this frame.
  232. * @returns {boolean} {@code true} if the tile set has changed.
  233. * @private
  234. */
  235. function haveSelectedTilesChanged(primitive, selectedTiles) {
  236. const prevSet = primitive._selectedTileSet;
  237. if (!defined(prevSet) || prevSet.size !== selectedTiles.length) {
  238. return true;
  239. }
  240. for (let i = 0; i < selectedTiles.length; i++) {
  241. if (!prevSet.has(selectedTiles[i])) {
  242. return true;
  243. }
  244. }
  245. return false;
  246. }
  247. /**
  248. * Returns whether the given sort result still matches the primitive's current
  249. * sort request and data generation, i.e. it has not been superseded.
  250. *
  251. * @param {GaussianSplatPrimitive} primitive The splat primitive.
  252. * @param {object} activeSort The sort result to validate.
  253. * @returns {boolean} {@code true} if the sort result is still current.
  254. * @private
  255. */
  256. function isActiveSort(primitive, activeSort) {
  257. return (
  258. defined(activeSort) &&
  259. activeSort.requestId === primitive._sortRequestId &&
  260. activeSort.dataGeneration === primitive._splatDataGeneration
  261. );
  262. }
  263. /**
  264. * Destroys the GPU textures owned by a snapshot, if any, and clears the
  265. * references so they are not used after destruction.
  266. *
  267. * @param {GaussianSplatPrimitive.Snapshot|undefined} snapshot The snapshot whose textures should be destroyed.
  268. * @private
  269. */
  270. function destroySnapshotTextures(snapshot) {
  271. if (!defined(snapshot)) {
  272. return;
  273. }
  274. if (defined(snapshot.gaussianSplatTexture)) {
  275. snapshot.gaussianSplatTexture.destroy();
  276. snapshot.gaussianSplatTexture = undefined;
  277. }
  278. if (defined(snapshot.sphericalHarmonicsTexture)) {
  279. snapshot.sphericalHarmonicsTexture.destroy();
  280. snapshot.sphericalHarmonicsTexture = undefined;
  281. }
  282. }
  283. /**
  284. * Schedules a GPU texture for deferred destruction. The texture is kept alive
  285. * for one additional frame so that any in-flight draw commands that reference
  286. * it can finish before the underlying GPU resource is released.
  287. *
  288. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  289. * @param {Texture|undefined} texture The texture to retire.
  290. * @param {number} frameNumber The frame number at which the texture was retired.
  291. * @private
  292. */
  293. function retireTexture(primitive, texture, frameNumber) {
  294. if (!defined(texture)) {
  295. return;
  296. }
  297. const retired = primitive._retiredTextures;
  298. retired.push({
  299. texture: texture,
  300. frameNumber: frameNumber,
  301. });
  302. }
  303. /**
  304. * Destroys any retired textures whose grace period (one frame) has elapsed.
  305. * Called once per frame to reclaim GPU memory from textures that were replaced
  306. * by a newer snapshot.
  307. *
  308. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  309. * @param {number} frameNumber The current frame number.
  310. * @private
  311. */
  312. function releaseRetiredTextures(primitive, frameNumber) {
  313. const retired = primitive._retiredTextures;
  314. if (!defined(retired) || retired.length === 0) {
  315. return;
  316. }
  317. const next = [];
  318. for (let i = 0; i < retired.length; i++) {
  319. const entry = retired[i];
  320. if (frameNumber - entry.frameNumber > 0) {
  321. entry.texture.destroy();
  322. } else {
  323. next.push(entry);
  324. }
  325. }
  326. primitive._retiredTextures = next;
  327. }
  328. function getSnapshotArrayBuffer(snapshot, key) {
  329. const value = snapshot?.[key];
  330. return defined(value) ? value.buffer : undefined;
  331. }
  332. function acquireAggregateScratchBuffer(
  333. primitive,
  334. key,
  335. componentDatatype,
  336. requiredLength,
  337. ) {
  338. let pool = primitive._aggregateScratchBuffers[key];
  339. if (!defined(pool)) {
  340. pool = [];
  341. primitive._aggregateScratchBuffers[key] = pool;
  342. }
  343. const activeBuffer = getSnapshotArrayBuffer(primitive._snapshot, key);
  344. for (let i = 0; i < pool.length; i++) {
  345. const candidate = pool[i];
  346. if (
  347. candidate.length >= requiredLength &&
  348. candidate.buffer !== activeBuffer
  349. ) {
  350. return candidate;
  351. }
  352. }
  353. const created = ComponentDatatype.createTypedArray(
  354. componentDatatype,
  355. requiredLength,
  356. );
  357. pool.push(created);
  358. return created;
  359. }
  360. function trimAggregateScratchBuffer(buffer, length) {
  361. if (buffer.length === length) {
  362. return buffer;
  363. }
  364. return buffer.subarray(0, length);
  365. }
  366. /**
  367. * Atomically promotes a fully-built snapshot to be the active splat data for
  368. * the primitive. This includes swapping attribute arrays, GPU textures, and
  369. * sorted indexes, as well as retiring any previously active textures so they
  370. * can be safely destroyed after the current frame finishes.
  371. *
  372. * The snapshot <b>must</b> be in the {@link SnapshotState.READY} state;
  373. * otherwise a {@link DeveloperError} is thrown.
  374. *
  375. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  376. * @param {GaussianSplatPrimitive.Snapshot} snapshot The snapshot to commit.
  377. * @param {FrameState} frameState The current frame state.
  378. * @throws {DeveloperError} If the snapshot is not READY.
  379. * @private
  380. */
  381. function commitSnapshot(primitive, snapshot, frameState) {
  382. if (!defined(snapshot.indexes) || snapshot.state !== SnapshotState.READY) {
  383. throw new DeveloperError("Committing snapshot before it is READY.");
  384. }
  385. const frameNumber = frameState.frameNumber;
  386. const currentSnapshot = primitive._snapshot;
  387. const splatTexture = defined(currentSnapshot)
  388. ? currentSnapshot.gaussianSplatTexture
  389. : primitive.gaussianSplatTexture;
  390. if (defined(splatTexture) && splatTexture !== snapshot.gaussianSplatTexture) {
  391. retireTexture(primitive, splatTexture, frameNumber);
  392. }
  393. const sphericalHarmonicsTexture = defined(currentSnapshot)
  394. ? currentSnapshot.sphericalHarmonicsTexture
  395. : primitive.sphericalHarmonicsTexture;
  396. if (
  397. defined(sphericalHarmonicsTexture) &&
  398. sphericalHarmonicsTexture !== snapshot.sphericalHarmonicsTexture
  399. ) {
  400. retireTexture(primitive, sphericalHarmonicsTexture, frameNumber);
  401. }
  402. primitive._snapshot = snapshot;
  403. primitive._positions = snapshot.positions;
  404. primitive._rotations = snapshot.rotations;
  405. primitive._scales = snapshot.scales;
  406. primitive._colors = snapshot.colors;
  407. primitive._shData = snapshot.shData;
  408. primitive._sphericalHarmonicsDegree = snapshot.sphericalHarmonicsDegree;
  409. primitive._numSplats = snapshot.numSplats;
  410. primitive._indexes = snapshot.indexes;
  411. primitive.gaussianSplatTexture = snapshot.gaussianSplatTexture;
  412. primitive.sphericalHarmonicsTexture = snapshot.sphericalHarmonicsTexture;
  413. primitive._lastTextureWidth = snapshot.lastTextureWidth;
  414. primitive._lastTextureHeight = snapshot.lastTextureHeight;
  415. // Commit row-addressing params alongside the texture; the shader must
  416. // always see the mask/shift that matches the active texture layout.
  417. primitive._splatRowMask = snapshot.splatRowMask;
  418. primitive._splatRowShift = snapshot.splatRowShift;
  419. // Above 1.0 when the previous snapshot hit the hard cap; used in
  420. // _wrappedUpdate to inflate traversal SSE and reduce tile load.
  421. primitive._splatBudgetSSEScale = snapshot.splatBudgetSSEScale ?? 1.0;
  422. primitive._hasGaussianSplatTexture = defined(snapshot.gaussianSplatTexture);
  423. primitive._needsGaussianSplatTexture = false;
  424. primitive._gaussianSplatTexturePending = false;
  425. primitive._vertexArray = undefined;
  426. primitive._vertexArrayLen = -1;
  427. primitive._drawCommand = undefined;
  428. primitive._sorterPromise = undefined;
  429. primitive._activeSort = undefined;
  430. primitive._sorterState = GaussianSplatSortingState.IDLE;
  431. primitive._dirty = false;
  432. }
  433. /**
  434. * Finalizes async splat texture generation for a snapshot. The resolved data
  435. * updates or recreates GPU textures, and the snapshot transitions to
  436. * {@link SnapshotState.TEXTURE_READY} when complete.
  437. *
  438. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  439. * @param {FrameState} frameState The current frame state.
  440. * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
  441. * @param {Promise<GaussianSplatPrimitive.AttributeTextureData>} promise Promise that resolves to packed splat texture data.
  442. * @returns {Promise<void>}
  443. * @private
  444. */
  445. async function processGeneratedSplatTextureData(
  446. primitive,
  447. frameState,
  448. snapshot,
  449. promise,
  450. ) {
  451. try {
  452. const splatTextureData = await promise;
  453. const maxTex = ContextLimits.maximumTextureSize;
  454. // Use maximumTextureSize as the texture width; splatsPerRow = maxTex / 2
  455. // (each splat occupies 2 side-by-side texels). The WASM buffer layout is
  456. // width-independent, so the raw data is reused as-is.
  457. const optimalWidth = maxTex;
  458. let optimalHeight = Math.ceil(snapshot.numSplats / (maxTex / 2));
  459. const splatRowShift = Math.log2(maxTex / 2);
  460. const splatRowMask = maxTex / 2 - 1;
  461. // Hard cap: >maxTex*(maxTex/2) splats cannot fit in any valid texture.
  462. if (optimalHeight > maxTex) {
  463. const originalCount = snapshot.numSplats;
  464. optimalHeight = maxTex;
  465. const splatsPerRow = optimalWidth / 2;
  466. snapshot.numSplats = maxTex * splatsPerRow;
  467. // Truncate CPU attribute arrays to match numSplats.
  468. snapshot.positions = snapshot.positions.subarray(
  469. 0,
  470. snapshot.numSplats * 3,
  471. );
  472. snapshot.rotations = snapshot.rotations.subarray(
  473. 0,
  474. snapshot.numSplats * 4,
  475. );
  476. snapshot.scales = snapshot.scales.subarray(0, snapshot.numSplats * 3);
  477. snapshot.colors = snapshot.colors.subarray(0, snapshot.numSplats * 4);
  478. // shData is allocated independently and must be truncated separately.
  479. if (defined(snapshot.shData)) {
  480. const shPerSplat = snapshot.shData.length / originalCount;
  481. snapshot.shData = snapshot.shData.subarray(
  482. 0,
  483. Math.floor(snapshot.numSplats * shPerSplat),
  484. );
  485. }
  486. // Scale up SSE next frame so traversal selects fewer tiles.
  487. snapshot.splatBudgetSSEScale = originalCount / snapshot.numSplats;
  488. console.warn(
  489. `[GaussianSplat][HARD CAP] ${originalCount} splats exceed the maximum texture capacity ` +
  490. `(${maxTex}\u00d7${splatsPerRow} = ${snapshot.numSplats} splats at width=${optimalWidth}). ` +
  491. `Rendering only the first ${snapshot.numSplats} splats to avoid a WebGL crash. ` +
  492. `Increasing maximumScreenSpaceError by ${snapshot.splatBudgetSSEScale.toFixed(2)}x next frame.`,
  493. );
  494. } else {
  495. // Within budget; clear any SSE inflation carried over from a previous cap.
  496. snapshot.splatBudgetSSEScale = 1.0;
  497. }
  498. // Trim or zero-pad the raw WASM buffer to match the chosen dimensions.
  499. const requiredLen = optimalWidth * optimalHeight * 4;
  500. let effectiveData;
  501. if (requiredLen <= splatTextureData.data.length) {
  502. effectiveData = splatTextureData.data.subarray(0, requiredLen);
  503. } else {
  504. effectiveData = new Uint32Array(requiredLen);
  505. effectiveData.set(splatTextureData.data);
  506. }
  507. const effectiveTextureData = {
  508. width: optimalWidth,
  509. height: optimalHeight,
  510. data: effectiveData,
  511. };
  512. snapshot.splatRowMask = splatRowMask;
  513. snapshot.splatRowShift = splatRowShift;
  514. if (primitive._pendingSnapshot !== snapshot) {
  515. snapshot.state = SnapshotState.BUILDING;
  516. return;
  517. }
  518. if (!defined(snapshot.gaussianSplatTexture)) {
  519. snapshot.gaussianSplatTexture = createGaussianSplatTexture(
  520. frameState.context,
  521. effectiveTextureData,
  522. );
  523. } else if (
  524. snapshot.lastTextureHeight !== effectiveTextureData.height ||
  525. snapshot.lastTextureWidth !== effectiveTextureData.width
  526. ) {
  527. const oldTex = snapshot.gaussianSplatTexture;
  528. snapshot.gaussianSplatTexture = createGaussianSplatTexture(
  529. frameState.context,
  530. effectiveTextureData,
  531. );
  532. oldTex.destroy();
  533. } else {
  534. snapshot.gaussianSplatTexture.copyFrom({
  535. source: {
  536. width: effectiveTextureData.width,
  537. height: effectiveTextureData.height,
  538. arrayBufferView: effectiveTextureData.data,
  539. },
  540. });
  541. }
  542. snapshot.lastTextureHeight = effectiveTextureData.height;
  543. snapshot.lastTextureWidth = effectiveTextureData.width;
  544. if (defined(snapshot.shData) && snapshot.sphericalHarmonicsDegree > 0) {
  545. const oldTex = snapshot.sphericalHarmonicsTexture;
  546. const width = ContextLimits.maximumTextureSize;
  547. const dims = snapshot.shCoefficientCount / 3;
  548. const splatsPerRow = Math.floor(width / dims);
  549. const floatsPerRow = splatsPerRow * (dims * 2);
  550. const shHeight = Math.ceil(snapshot.numSplats / splatsPerRow);
  551. // SH texture width is already maxTex and cannot be widened further.
  552. // When height would exceed the GPU limit, gracefully disable SH for this
  553. // snapshot and fall back to base color rendering rather than crashing.
  554. if (shHeight > width) {
  555. console.warn(
  556. `[GaussianSplat][SHTexture] ${snapshot.numSplats} splats require SH height ${shHeight} > maxTex ${width}. ` +
  557. `Disabling spherical harmonics for this snapshot (color-only fallback).`,
  558. );
  559. snapshot.sphericalHarmonicsDegree = 0;
  560. if (defined(oldTex)) {
  561. oldTex.destroy();
  562. }
  563. snapshot.sphericalHarmonicsTexture = undefined;
  564. } else {
  565. const texBuf = new Uint32Array(width * shHeight * 2);
  566. let dataIndex = 0;
  567. for (let i = 0; dataIndex < snapshot.shData.length; i += width * 2) {
  568. texBuf.set(
  569. snapshot.shData.subarray(dataIndex, dataIndex + floatsPerRow),
  570. i,
  571. );
  572. dataIndex += floatsPerRow;
  573. }
  574. snapshot.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
  575. frameState.context,
  576. {
  577. data: texBuf,
  578. width: width,
  579. height: shHeight,
  580. },
  581. );
  582. if (defined(oldTex)) {
  583. oldTex.destroy();
  584. }
  585. }
  586. }
  587. snapshot.state = SnapshotState.TEXTURE_READY;
  588. } catch (error) {
  589. console.error("Error generating Gaussian splat texture:", error);
  590. snapshot.state = SnapshotState.BUILDING;
  591. }
  592. }
  593. /**
  594. * Resolves an in-flight sort for a pending snapshot, validates that the result
  595. * still matches the active generation/request, and commits the snapshot when
  596. * valid.
  597. *
  598. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  599. * @param {FrameState} frameState The current frame state.
  600. * @param {object|undefined} pendingSort Pending sort metadata.
  601. * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
  602. * @returns {Promise<void>}
  603. * @private
  604. */
  605. async function resolvePendingSnapshotSort(
  606. primitive,
  607. frameState,
  608. pendingSort,
  609. sortPromise,
  610. ) {
  611. try {
  612. const sortedData = await sortPromise;
  613. if (
  614. !defined(pendingSort) ||
  615. pendingSort.snapshot !== primitive._pendingSnapshot
  616. ) {
  617. return;
  618. }
  619. const expectedCount = pendingSort.expectedCount;
  620. const currentCount = expectedCount;
  621. const sortedLen = sortedData?.length;
  622. if (expectedCount !== currentCount || sortedLen !== expectedCount) {
  623. primitive._pendingSortPromise = undefined;
  624. primitive._pendingSort = undefined;
  625. if (pendingSort.snapshot.state === SnapshotState.SORTING) {
  626. pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
  627. }
  628. return;
  629. }
  630. const pending = pendingSort.snapshot;
  631. pending.indexes = sortedData;
  632. pending.state = SnapshotState.READY;
  633. primitive._pendingSortPromise = undefined;
  634. primitive._pendingSort = undefined;
  635. commitSnapshot(primitive, pending, frameState);
  636. primitive._pendingSnapshot = undefined;
  637. GaussianSplatPrimitive.buildGSplatDrawCommand(primitive, frameState);
  638. } catch (err) {
  639. if (
  640. !defined(pendingSort) ||
  641. pendingSort.snapshot !== primitive._pendingSnapshot
  642. ) {
  643. return;
  644. }
  645. primitive._pendingSortPromise = undefined;
  646. primitive._pendingSort = undefined;
  647. if (pendingSort.snapshot.state === SnapshotState.SORTING) {
  648. pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
  649. }
  650. primitive._sorterState = GaussianSplatSortingState.ERROR;
  651. primitive._sorterError = err;
  652. }
  653. }
  654. /**
  655. * Resolves an in-flight steady-state sort for the current committed snapshot.
  656. * Results are ignored when superseded; otherwise, they advance sorting state
  657. * to {@link GaussianSplatSortingState.SORTED}.
  658. *
  659. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  660. * @param {object|undefined} activeSort Active sort metadata.
  661. * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
  662. * @returns {Promise<void>}
  663. * @private
  664. */
  665. async function resolveSteadySort(primitive, activeSort, sortPromise) {
  666. try {
  667. const sortedData = await sortPromise;
  668. const isActive = isActiveSort(primitive, activeSort);
  669. const expectedCount = activeSort?.expectedCount;
  670. const currentCount = expectedCount;
  671. const sortedLen = sortedData?.length;
  672. const isMismatch =
  673. expectedCount !== currentCount || sortedLen !== expectedCount;
  674. if (!isActive || isMismatch) {
  675. if (isActive) {
  676. primitive._sorterPromise = undefined;
  677. primitive._sorterState = GaussianSplatSortingState.IDLE;
  678. }
  679. return;
  680. }
  681. primitive._indexes = sortedData;
  682. primitive._sorterState = GaussianSplatSortingState.SORTED;
  683. } catch (err) {
  684. if (!isActiveSort(primitive, activeSort)) {
  685. return;
  686. }
  687. primitive._sorterState = GaussianSplatSortingState.ERROR;
  688. primitive._sorterError = err;
  689. }
  690. }
  691. /**
  692. * Creates a GPU texture that stores packed spherical harmonics coefficient
  693. * data for all splats. The texture uses a two-channel unsigned-integer format
  694. * ({@link PixelFormat.RG_INTEGER}) and nearest-neighbor sampling.
  695. *
  696. * @param {Context} context The WebGL context.
  697. * @param {GaussianSplatPrimitive.SphericalHarmonicsTextureData} shData Packed SH texture payload.
  698. * @returns {Texture} The created texture.
  699. * @private
  700. */
  701. function createSphericalHarmonicsTexture(context, shData) {
  702. const texture = new Texture({
  703. context: context,
  704. source: {
  705. width: shData.width,
  706. height: shData.height,
  707. arrayBufferView: shData.data,
  708. },
  709. preMultiplyAlpha: false,
  710. skipColorSpaceConversion: true,
  711. pixelFormat: PixelFormat.RG_INTEGER,
  712. pixelDatatype: PixelDatatype.UNSIGNED_INT,
  713. flipY: false,
  714. sampler: Sampler.NEAREST,
  715. });
  716. return texture;
  717. }
  718. /**
  719. * Creates a GPU texture that stores the packed Gaussian splat attributes
  720. * (positions, scales, rotations, colors). The texture uses an RGBA
  721. * unsigned-integer format ({@link PixelFormat.RGBA_INTEGER}) and
  722. * nearest-neighbor sampling.
  723. *
  724. * @param {Context} context The WebGL context.
  725. * @param {GaussianSplatPrimitive.AttributeTextureData} splatTextureData Packed splat texture payload.
  726. * @returns {Texture} The created texture.
  727. * @private
  728. */
  729. function createGaussianSplatTexture(context, splatTextureData) {
  730. return new Texture({
  731. context: context,
  732. source: {
  733. width: splatTextureData.width,
  734. height: splatTextureData.height,
  735. arrayBufferView: splatTextureData.data,
  736. },
  737. preMultiplyAlpha: false,
  738. skipColorSpaceConversion: true,
  739. pixelFormat: PixelFormat.RGBA_INTEGER,
  740. pixelDatatype: PixelDatatype.UNSIGNED_INT,
  741. flipY: false,
  742. sampler: Sampler.NEAREST,
  743. });
  744. }
  745. /** A primitive that renders Gaussian splats.
  746. * <p>
  747. * This primitive is used to render Gaussian splats in a 3D Tileset.
  748. * It is designed to work with the KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extensions.
  749. * </p>
  750. * @alias GaussianSplatPrimitive
  751. * @constructor
  752. * @param {object} options An object with the following properties:
  753. * @param {Cesium3DTileset} options.tileset The tileset that this primitive belongs to.
  754. * @param {boolean} [options.debugShowBoundingVolume=false] Whether to show the bounding volume of the primitive for debugging purposes.
  755. * @private
  756. */
  757. function GaussianSplatPrimitive(options) {
  758. options = options ?? Frozen.EMPTY_OBJECT;
  759. /**
  760. * The positions of the Gaussian splats in the primitive.
  761. * @type {undefined|Float32Array}
  762. * @private
  763. */
  764. this._positions = undefined;
  765. /**
  766. * The rotations of the Gaussian splats in the primitive.
  767. * @type {undefined|Float32Array}
  768. * @private
  769. */
  770. this._rotations = undefined;
  771. /**
  772. * The scales of the Gaussian splats in the primitive.
  773. * @type {undefined|Float32Array}
  774. * @private
  775. */
  776. this._scales = undefined;
  777. /**
  778. * The colors of the Gaussian splats in the primitive.
  779. * @type {undefined|Uint8Array}
  780. * @private
  781. */
  782. this._colors = undefined;
  783. /**
  784. * The indexes of the Gaussian splats in the primitive.
  785. * Used to index into the splat attribute texture in the vertex shader.
  786. * @type {undefined|Uint32Array}
  787. * @private
  788. */
  789. this._indexes = undefined;
  790. /**
  791. * The number of splats in the primitive.
  792. * This is the total number of splats across all selected tiles.
  793. * @type {number}
  794. * @private
  795. */
  796. this._numSplats = 0;
  797. /**
  798. * Indicates whether or not the primitive needs a Gaussian splat texture.
  799. * This is set to true when the primitive is first created or when the splat attributes change.
  800. * @type {boolean}
  801. * @private
  802. */
  803. this._needsGaussianSplatTexture = true;
  804. this._snapshot = undefined;
  805. this._pendingSnapshot = undefined;
  806. this._retiredTextures = [];
  807. this._aggregateScratchBuffers = {
  808. positions: [],
  809. scales: [],
  810. rotations: [],
  811. colors: [],
  812. };
  813. /**
  814. * Scratch buffer re-used across frames for aggregating packed spherical
  815. * harmonics data so that a fresh typed-array allocation is avoided on
  816. * every tile-selection change.
  817. * @type {undefined|Uint32Array}
  818. * @private
  819. */
  820. this._scratchAggregateShBuffer = undefined;
  821. this._selectedTilesStableFrames = 0;
  822. this._needsSnapshotRebuild = false;
  823. this._snapshotRebuildStallFrames = 0;
  824. /**
  825. * The previous view matrix used to determine if the primitive needs to be updated.
  826. * This is used to avoid unnecessary updates when the view matrix hasn't changed.
  827. * @type {Matrix4}
  828. * @private
  829. */
  830. this._prevViewMatrix = new Matrix4();
  831. /**
  832. * Indicates whether or not to show the bounding volume of the primitive for debugging purposes.
  833. * This is used to visualize the bounding volume of the primitive in the scene.
  834. * @type {boolean}
  835. * @private
  836. */
  837. this._debugShowBoundingVolume = options.debugShowBoundingVolume ?? false;
  838. /**
  839. * The texture used to store the Gaussian splat attributes.
  840. * This texture is created from the splat attributes (positions, scales, rotations, colors)
  841. * and is used in the vertex shader to render the splats.
  842. * @type {undefined|Texture}
  843. * @private
  844. * @see {@link GaussianSplatTextureGenerator}
  845. */
  846. this.gaussianSplatTexture = undefined;
  847. /**
  848. * The texture used to store the spherical harmonics coefficients for the Gaussian splats.
  849. * @type {undefined|Texture}
  850. * @private
  851. */
  852. this.sphericalHarmonicsTexture = undefined;
  853. /**
  854. * The last width of the Gaussian splat texture.
  855. * This is used to track changes in the texture size and update the primitive accordingly.
  856. * @type {number}
  857. * @private
  858. */
  859. this._lastTextureWidth = 0;
  860. /**
  861. * The last height of the Gaussian splat texture.
  862. * This is used to track changes in the texture size and update the primitive accordingly.
  863. * @type {number}
  864. * @private
  865. */
  866. this._lastTextureHeight = 0;
  867. /**
  868. * The vertex array used to render the Gaussian splats.
  869. * This vertex array contains the attributes needed to render the splats, such as positions and indexes.
  870. * @type {undefined|VertexArray}
  871. * @private
  872. */
  873. this._vertexArray = undefined;
  874. /**
  875. * The length of the vertex array, used to track changes in the number of splats.
  876. * This is used to determine if the vertex array needs to be rebuilt.
  877. * @type {number}
  878. * @private
  879. */
  880. this._vertexArrayLen = -1;
  881. this._splitDirection = SplitDirection.NONE;
  882. /**
  883. * The dirty flag forces the primitive to render this frame.
  884. * @type {boolean}
  885. * @private
  886. */
  887. this._dirty = false;
  888. this._tileset = options.tileset;
  889. this._baseTilesetUpdate = this._tileset.update;
  890. this._tileset.update = this._wrappedUpdate.bind(this);
  891. this._tileset.tileLoad.addEventListener(this.onTileLoad, this);
  892. this._tileset.tileVisible.addEventListener(this.onTileVisible, this);
  893. /**
  894. * Tracks current count of selected tiles.
  895. * This is used to determine if the primitive needs to be rebuilt.
  896. * @type {number}
  897. * @private
  898. */
  899. this.selectedTileLength = 0;
  900. this._selectedTileSet = new Set();
  901. /**
  902. * Indicates whether or not the primitive is ready for use.
  903. * @type {boolean}
  904. * @private
  905. */
  906. this._ready = false;
  907. /**
  908. * Indicates whether or not the primitive has a Gaussian splat texture.
  909. * @type {boolean}
  910. * @private
  911. */
  912. this._hasGaussianSplatTexture = false;
  913. /**
  914. * Indicates whether or not the primitive is currently generating a Gaussian splat texture.
  915. * @type {boolean}
  916. * @private
  917. */
  918. this._gaussianSplatTexturePending = false;
  919. /**
  920. * The draw command used to render the Gaussian splats.
  921. * @type {undefined|DrawCommand}
  922. * @private
  923. */
  924. this._drawCommand = undefined;
  925. this._drawCommandModelMatrix = new Matrix4();
  926. /**
  927. * The root transform of the tileset.
  928. * This is used to transform the splats into world space.
  929. * @type {undefined|Matrix4}
  930. * @private
  931. */
  932. this._rootTransform = undefined;
  933. /**
  934. * The axis correction matrix to transform the splats from Y-up to Z-up.
  935. * @type {Matrix4}
  936. * @private
  937. */
  938. this._axisCorrectionMatrix = ModelUtility.getAxisCorrectionMatrix(
  939. Axis.Y,
  940. Axis.X,
  941. new Matrix4(),
  942. );
  943. /**
  944. * Cached inverse rotation for SH evaluation, updated each snapshot rebuild.
  945. * Converts a world-space view direction to the original GLB Y-up model space
  946. * so that spherical harmonic coefficients are evaluated in the correct frame.
  947. * @type {Matrix3}
  948. * @private
  949. */
  950. this._shInverseRotation = new Matrix3();
  951. /**
  952. * Indicates whether or not the primitive has been destroyed.
  953. * @type {boolean}
  954. * @private
  955. */
  956. this._isDestroyed = false;
  957. /**
  958. * The state of the Gaussian splat sorting process.
  959. * This is used to track the progress of the sorting operation.
  960. * @type {GaussianSplatSortingState}
  961. * @private
  962. */
  963. this._sorterState = GaussianSplatSortingState.IDLE;
  964. /**
  965. * A promise that resolves when the Gaussian splat sorting operation is complete.
  966. * This is used to track the progress of the sorting operation.
  967. * @type {undefined|Promise}
  968. * @private
  969. */
  970. this._sorterPromise = undefined;
  971. this._splatDataGeneration = 0;
  972. this._sortRequestId = 0;
  973. this._activeSort = undefined;
  974. this._pendingSortPromise = undefined;
  975. this._pendingSort = undefined;
  976. this._lastSteadySortFrameNumber = -1;
  977. this._lastSteadySortCameraPosition = new Cartesian3();
  978. this._hasLastSteadySortCameraPosition = false;
  979. this._lastSteadySortCameraDirection = new Cartesian3();
  980. this._hasLastSteadySortCameraDirection = false;
  981. /**
  982. * An error that occurred during the Gaussian splat sorting operation.
  983. * Thrown when state is ERROR.
  984. * @type {undefined|Error}
  985. * @private
  986. */
  987. this._sorterError = undefined;
  988. // Splat texture row-addressing params; forwarded to the shader as uniforms.
  989. // The texture width is always maximumTextureSize (varies by GPU), so these
  990. // are computed per-snapshot and initialized here as safe placeholders.
  991. this._splatRowMask = 0; // overwritten on first snapshot commit
  992. this._splatRowShift = 0; // overwritten on first snapshot commit
  993. /**
  994. * Multiplier applied to maximumScreenSpaceError during tile traversal when
  995. * the previous snapshot exceeded the splat texture budget. A value above 1.0
  996. * biases traversal toward coarser LODs, lowering the total splat count.
  997. * Resets to 1.0 once the splat count is within budget.
  998. * @type {number}
  999. * @private
  1000. */
  1001. this._splatBudgetSSEScale = 1.0;
  1002. }
  1003. Object.defineProperties(GaussianSplatPrimitive.prototype, {
  1004. /**
  1005. * Indicates whether the primitive is ready for use.
  1006. * @memberof GaussianSplatPrimitive.prototype
  1007. * @type {boolean}
  1008. * @readonly
  1009. */
  1010. ready: {
  1011. get: function () {
  1012. return this._ready;
  1013. },
  1014. },
  1015. /**
  1016. * Indicates whether the primitive has completed loading and sorting.
  1017. * @memberof GaussianSplatPrimitive.prototype
  1018. * @type {boolean}
  1019. * @private
  1020. * @readonly
  1021. */
  1022. isStable: {
  1023. get: function () {
  1024. return (
  1025. !this._dirty &&
  1026. (!defined(this._pendingSnapshot) ||
  1027. this._pendingSnapshot.state === SnapshotState.READY)
  1028. );
  1029. },
  1030. },
  1031. /**
  1032. * The {@link SplitDirection} to apply to this point.
  1033. * @memberof GaussianSplatPrimitive.prototype
  1034. * @type {SplitDirection}
  1035. * @default {@link SplitDirection.NONE}
  1036. */
  1037. splitDirection: {
  1038. get: function () {
  1039. return this._splitDirection;
  1040. },
  1041. set: function (value) {
  1042. if (this._splitDirection !== value) {
  1043. this._splitDirection = value;
  1044. this._dirty = true;
  1045. }
  1046. },
  1047. },
  1048. });
  1049. /**
  1050. * Replaces the tileset's own update function so this primitive is updated
  1051. * immediately after the tileset traversal, within the same frame. When
  1052. * _splatBudgetSSEScale is above 1.0, maximumScreenSpaceError is inflated
  1053. * for the duration of the traversal to reduce the number of tiles selected.
  1054. * @param {FrameState} frameState
  1055. * @private
  1056. */
  1057. GaussianSplatPrimitive.prototype._wrappedUpdate = function (frameState) {
  1058. const tileset = this._tileset;
  1059. if (this._splatBudgetSSEScale !== 1.0) {
  1060. // Inflate SSE for this traversal only; the original value is restored
  1061. // immediately so the user-visible tileset property is never permanently changed.
  1062. const originalSSE = tileset.maximumScreenSpaceError;
  1063. tileset.maximumScreenSpaceError *= this._splatBudgetSSEScale;
  1064. this._baseTilesetUpdate.call(tileset, frameState);
  1065. tileset.maximumScreenSpaceError = originalSSE;
  1066. } else {
  1067. this._baseTilesetUpdate.call(tileset, frameState);
  1068. }
  1069. this.update(frameState);
  1070. };
  1071. /**
  1072. * Destroys the primitive and releases its resources in a deterministic manner.
  1073. * @private
  1074. */
  1075. GaussianSplatPrimitive.prototype.destroy = function () {
  1076. this._positions = undefined;
  1077. this._rotations = undefined;
  1078. this._scales = undefined;
  1079. this._colors = undefined;
  1080. this._indexes = undefined;
  1081. destroySnapshotTextures(this._pendingSnapshot);
  1082. destroySnapshotTextures(this._snapshot);
  1083. if (defined(this._retiredTextures)) {
  1084. for (let i = 0; i < this._retiredTextures.length; i++) {
  1085. this._retiredTextures[i].texture.destroy();
  1086. }
  1087. }
  1088. this._retiredTextures = [];
  1089. this._pendingSnapshot = undefined;
  1090. this._snapshot = undefined;
  1091. this._aggregateScratchBuffers = undefined;
  1092. this.gaussianSplatTexture = undefined;
  1093. this.sphericalHarmonicsTexture = undefined;
  1094. const drawCommand = this._drawCommand;
  1095. if (defined(drawCommand)) {
  1096. drawCommand.shaderProgram =
  1097. drawCommand.shaderProgram && drawCommand.shaderProgram.destroy();
  1098. }
  1099. if (defined(this._vertexArray)) {
  1100. this._vertexArray.destroy();
  1101. this._vertexArray = undefined;
  1102. }
  1103. this._tileset.update = this._baseTilesetUpdate.bind(this._tileset);
  1104. return destroyObject(this);
  1105. };
  1106. /**
  1107. * Returns true if this object was destroyed; otherwise, false.
  1108. * <br /><br />
  1109. * If this object was destroyed, it should not be used; calling any function other than
  1110. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
  1111. * @returns {boolean} Returns true if the primitive has been destroyed, otherwise false.
  1112. * @private
  1113. */
  1114. GaussianSplatPrimitive.prototype.isDestroyed = function () {
  1115. return this._isDestroyed;
  1116. };
  1117. /**
  1118. * Event callback for when a tile is loaded.
  1119. * This method is called when a tile is loaded and the primitive needs to be updated.
  1120. * It sets the dirty flag to true, indicating that the primitive needs to be rebuilt.
  1121. * @param {Cesium3DTile} tile
  1122. * @private
  1123. */
  1124. GaussianSplatPrimitive.prototype.onTileLoad = function (tile) {
  1125. this._dirty = true;
  1126. };
  1127. /**
  1128. * Callback for visible tiles.
  1129. * @param {Cesium3DTile} tile
  1130. * @private
  1131. */
  1132. GaussianSplatPrimitive.prototype.onTileVisible = function (tile) {};
  1133. /**
  1134. * Transforms the tile's splat primitive attributes into world space.
  1135. * <br /><br />
  1136. * This method applies the computed transform of the tile and the tileset's bounding sphere
  1137. * to the splat primitive's position, rotation, and scale attributes.
  1138. * It modifies the attributes in place, transforming them from local space to world space.
  1139. *
  1140. * @param {Cesium3DTile} tile
  1141. * @private
  1142. */
  1143. GaussianSplatPrimitive.transformTile = function (tile) {
  1144. const computedTransform = tile.computedTransform;
  1145. const gltfPrimitive = tile.content.gltfPrimitive;
  1146. const gaussianSplatPrimitive = tile.tileset.gaussianSplatPrimitive;
  1147. if (gaussianSplatPrimitive._rootTransform === undefined) {
  1148. gaussianSplatPrimitive._rootTransform = Transforms.eastNorthUpToFixedFrame(
  1149. tile.tileset.boundingSphere.center,
  1150. );
  1151. }
  1152. const rootTransform = gaussianSplatPrimitive._rootTransform;
  1153. const computedModelMatrix = Matrix4.multiplyTransformation(
  1154. computedTransform,
  1155. gaussianSplatPrimitive._axisCorrectionMatrix,
  1156. scratchMatrix4A,
  1157. );
  1158. Matrix4.multiplyTransformation(
  1159. computedModelMatrix,
  1160. tile.content.worldTransform,
  1161. computedModelMatrix,
  1162. );
  1163. // toLocal is inverse(rootTransform) only. tileset.modelMatrix is already
  1164. // factored into computedModelMatrix via tile.computedTransform, so its effect
  1165. // is baked directly into the splat values rather than split into the draw
  1166. // command's modelMatrix. This keeps czm_view * modelMatrix numerically small,
  1167. // avoiding float32 precision loss at ECEF-scale translations.
  1168. const toLocal = Matrix4.inverse(rootTransform, scratchMatrix4C);
  1169. const transform = Matrix4.multiplyTransformation(
  1170. toLocal,
  1171. computedModelMatrix,
  1172. scratchMatrix4A,
  1173. );
  1174. const cachedTransform = tile.content._lastSplatTransform;
  1175. if (
  1176. tile.content._transformed &&
  1177. defined(cachedTransform) &&
  1178. Matrix4.equalsEpsilon(transform, cachedTransform, TRANSFORM_CACHE_EPSILON)
  1179. ) {
  1180. return;
  1181. }
  1182. const positions = tile.content.positions;
  1183. const rotations = tile.content.rotations;
  1184. const scales = tile.content.scales;
  1185. // Extract the rotation quaternion from transform once, before the per-splat
  1186. // loop. The columns of transform's upper-left 3x3 have magnitude ≈ 1 (rigid
  1187. // body placement), so normalizing is numerically stable. We cannot decompose
  1188. // the per-splat combined matrix (transform × TRS_i) instead, because each
  1189. // splat's scale can be very small, causing catastrophic cancellation when
  1190. // dividing to recover a pure rotation matrix.
  1191. const col0Len = Math.sqrt(
  1192. transform[0] * transform[0] +
  1193. transform[1] * transform[1] +
  1194. transform[2] * transform[2],
  1195. );
  1196. const col1Len = Math.sqrt(
  1197. transform[4] * transform[4] +
  1198. transform[5] * transform[5] +
  1199. transform[6] * transform[6],
  1200. );
  1201. const col2Len = Math.sqrt(
  1202. transform[8] * transform[8] +
  1203. transform[9] * transform[9] +
  1204. transform[10] * transform[10],
  1205. );
  1206. scratchMatrix3[0] = transform[0] / col0Len;
  1207. scratchMatrix3[1] = transform[1] / col0Len;
  1208. scratchMatrix3[2] = transform[2] / col0Len;
  1209. scratchMatrix3[3] = transform[4] / col1Len;
  1210. scratchMatrix3[4] = transform[5] / col1Len;
  1211. scratchMatrix3[5] = transform[6] / col1Len;
  1212. scratchMatrix3[6] = transform[8] / col2Len;
  1213. scratchMatrix3[7] = transform[9] / col2Len;
  1214. scratchMatrix3[8] = transform[10] / col2Len;
  1215. const dot01 =
  1216. scratchMatrix3[0] * scratchMatrix3[3] +
  1217. scratchMatrix3[1] * scratchMatrix3[4] +
  1218. scratchMatrix3[2] * scratchMatrix3[5];
  1219. const dot02 =
  1220. scratchMatrix3[0] * scratchMatrix3[6] +
  1221. scratchMatrix3[1] * scratchMatrix3[7] +
  1222. scratchMatrix3[2] * scratchMatrix3[8];
  1223. const dot12 =
  1224. scratchMatrix3[3] * scratchMatrix3[6] +
  1225. scratchMatrix3[4] * scratchMatrix3[7] +
  1226. scratchMatrix3[5] * scratchMatrix3[8];
  1227. const hasUnitScale =
  1228. Math.abs(col0Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON &&
  1229. Math.abs(col1Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON &&
  1230. Math.abs(col2Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON;
  1231. const isOrthogonal =
  1232. Math.abs(dot01) <= RIGID_TRANSFORM_EPSILON &&
  1233. Math.abs(dot02) <= RIGID_TRANSFORM_EPSILON &&
  1234. Math.abs(dot12) <= RIGID_TRANSFORM_EPSILON;
  1235. const determinant =
  1236. scratchMatrix3[0] *
  1237. (scratchMatrix3[4] * scratchMatrix3[8] -
  1238. scratchMatrix3[5] * scratchMatrix3[7]) -
  1239. scratchMatrix3[3] *
  1240. (scratchMatrix3[1] * scratchMatrix3[8] -
  1241. scratchMatrix3[2] * scratchMatrix3[7]) +
  1242. scratchMatrix3[6] *
  1243. (scratchMatrix3[1] * scratchMatrix3[5] -
  1244. scratchMatrix3[2] * scratchMatrix3[4]);
  1245. const useFastPath =
  1246. hasUnitScale &&
  1247. isOrthogonal &&
  1248. Math.abs(determinant - 1.0) <= RIGID_TRANSFORM_EPSILON;
  1249. Quaternion.fromRotationMatrix(scratchMatrix3, scratchTransformQuat);
  1250. Quaternion.normalize(scratchTransformQuat, scratchTransformQuat);
  1251. const attributePositions = ModelUtility.getAttributeBySemantic(
  1252. gltfPrimitive,
  1253. VertexAttributeSemantic.POSITION,
  1254. ).typedArray;
  1255. const attributeRotations = ModelUtility.getAttributeBySemantic(
  1256. gltfPrimitive,
  1257. VertexAttributeSemantic.ROTATION,
  1258. ).typedArray;
  1259. const attributeScales = ModelUtility.getAttributeBySemantic(
  1260. gltfPrimitive,
  1261. VertexAttributeSemantic.SCALE,
  1262. ).typedArray;
  1263. const position = scratchTransformPosition;
  1264. const rotation = scratchTransformRotation;
  1265. const scale = scratchTransformScale;
  1266. for (let i = 0; i < attributePositions.length / 3; ++i) {
  1267. position.x = attributePositions[i * 3];
  1268. position.y = attributePositions[i * 3 + 1];
  1269. position.z = attributePositions[i * 3 + 2];
  1270. rotation.x = attributeRotations[i * 4];
  1271. rotation.y = attributeRotations[i * 4 + 1];
  1272. rotation.z = attributeRotations[i * 4 + 2];
  1273. rotation.w = attributeRotations[i * 4 + 3];
  1274. scale.x = attributeScales[i * 3];
  1275. scale.y = attributeScales[i * 3 + 1];
  1276. scale.z = attributeScales[i * 3 + 2];
  1277. if (useFastPath) {
  1278. Matrix4.multiplyByPoint(transform, position, position);
  1279. Quaternion.multiply(scratchTransformQuat, rotation, rotation);
  1280. Quaternion.normalize(rotation, rotation);
  1281. } else {
  1282. Matrix4.fromTranslationQuaternionRotationScale(
  1283. position,
  1284. rotation,
  1285. scale,
  1286. scratchMatrix4D,
  1287. );
  1288. Matrix4.multiplyTransformation(
  1289. transform,
  1290. scratchMatrix4D,
  1291. scratchMatrix4D,
  1292. );
  1293. Matrix4.getTranslation(scratchMatrix4D, position);
  1294. Matrix4.getScale(scratchMatrix4D, scale);
  1295. // rotation still holds the original splat quaternion from attributeRotations.
  1296. // Apply the transform's rotation by left-multiplying the transform quaternion.
  1297. Quaternion.multiply(scratchTransformQuat, rotation, rotation);
  1298. Quaternion.normalize(rotation, rotation);
  1299. }
  1300. positions[i * 3] = position.x;
  1301. positions[i * 3 + 1] = position.y;
  1302. positions[i * 3 + 2] = position.z;
  1303. rotations[i * 4] = rotation.x;
  1304. rotations[i * 4 + 1] = rotation.y;
  1305. rotations[i * 4 + 2] = rotation.z;
  1306. rotations[i * 4 + 3] = rotation.w;
  1307. scales[i * 3] = scale.x;
  1308. scales[i * 3 + 1] = scale.y;
  1309. scales[i * 3 + 2] = scale.z;
  1310. }
  1311. tile.content._lastSplatTransform = Matrix4.clone(
  1312. transform,
  1313. tile.content._lastSplatTransform,
  1314. );
  1315. tile.content._transformed = true;
  1316. };
  1317. /**
  1318. * Generates the Gaussian splat texture for the primitive.
  1319. * This method creates a texture from the splat attributes (positions, scales, rotations, colors)
  1320. * and updates the primitive's state accordingly.
  1321. *
  1322. * @see {@link GaussianSplatTextureGenerator}
  1323. *
  1324. * @param {GaussianSplatPrimitive} primitive The owning primitive.
  1325. * @param {FrameState} frameState The current frame state.
  1326. * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
  1327. * @private
  1328. */
  1329. GaussianSplatPrimitive.generateSplatTexture = function (
  1330. primitive,
  1331. frameState,
  1332. snapshot,
  1333. ) {
  1334. if (!defined(snapshot) || snapshot.state !== SnapshotState.BUILDING) {
  1335. return;
  1336. }
  1337. snapshot.state = SnapshotState.TEXTURE_PENDING;
  1338. const promise = GaussianSplatTextureGenerator.generateFromAttributes({
  1339. attributes: {
  1340. positions: new Float32Array(snapshot.positions),
  1341. scales: new Float32Array(snapshot.scales),
  1342. rotations: new Float32Array(snapshot.rotations),
  1343. colors: new Uint8Array(snapshot.colors),
  1344. },
  1345. count: snapshot.numSplats,
  1346. });
  1347. if (!defined(promise)) {
  1348. snapshot.state = SnapshotState.BUILDING;
  1349. return;
  1350. }
  1351. void processGeneratedSplatTextureData(
  1352. primitive,
  1353. frameState,
  1354. snapshot,
  1355. promise,
  1356. );
  1357. };
  1358. /**
  1359. * Builds the draw command for the Gaussian splat primitive.
  1360. * This method sets up the shader program, render state, and vertex array for rendering the Gaussian splats.
  1361. * It also configures the attributes and uniforms required for rendering.
  1362. *
  1363. * @param {GaussianSplatPrimitive} primitive
  1364. * @param {FrameState} frameState
  1365. *
  1366. * @private
  1367. */
  1368. GaussianSplatPrimitive.buildGSplatDrawCommand = function (
  1369. primitive,
  1370. frameState,
  1371. ) {
  1372. const tileset = primitive._tileset;
  1373. const renderResources = new GaussianSplatRenderResources(primitive);
  1374. const { shaderBuilder } = renderResources;
  1375. const renderStateOptions = renderResources.renderStateOptions;
  1376. renderStateOptions.cull.enabled = false;
  1377. renderStateOptions.depthMask = false;
  1378. renderStateOptions.depthTest.enabled = true;
  1379. renderStateOptions.blending = BlendingState.PRE_MULTIPLIED_ALPHA_BLEND;
  1380. renderResources.alphaOptions.pass = Pass.GAUSSIAN_SPLATS;
  1381. shaderBuilder.addAttribute("vec2", "a_screenQuadPosition");
  1382. shaderBuilder.addAttribute("float", "a_splatIndex");
  1383. shaderBuilder.addVarying("vec4", "v_splatColor");
  1384. shaderBuilder.addVarying("vec2", "v_vertPos");
  1385. shaderBuilder.addUniform(
  1386. "float",
  1387. "u_splitDirection",
  1388. ShaderDestination.VERTEX,
  1389. );
  1390. shaderBuilder.addVarying("float", "v_splitDirection");
  1391. shaderBuilder.addUniform(
  1392. "highp usampler2D",
  1393. "u_splatAttributeTexture",
  1394. ShaderDestination.VERTEX,
  1395. );
  1396. shaderBuilder.addUniform(
  1397. "float",
  1398. "u_sphericalHarmonicsDegree",
  1399. ShaderDestination.VERTEX,
  1400. );
  1401. shaderBuilder.addUniform("float", "u_splatScale", ShaderDestination.VERTEX);
  1402. shaderBuilder.addUniform(
  1403. "vec3",
  1404. "u_cameraPositionWC",
  1405. ShaderDestination.VERTEX,
  1406. );
  1407. shaderBuilder.addUniform(
  1408. "mat3",
  1409. "u_inverseModelRotation",
  1410. ShaderDestination.VERTEX,
  1411. );
  1412. const uniformMap = renderResources.uniformMap;
  1413. // Row-addressing uniforms: read from primitive each draw so they stay in sync
  1414. // with the texture width chosen for the current snapshot.
  1415. shaderBuilder.addUniform("int", "u_splatRowMask", ShaderDestination.VERTEX);
  1416. shaderBuilder.addUniform("int", "u_splatRowShift", ShaderDestination.VERTEX);
  1417. const textureCache = primitive.gaussianSplatTexture;
  1418. uniformMap.u_splatAttributeTexture = function () {
  1419. return textureCache;
  1420. };
  1421. uniformMap.u_splatRowMask = function () {
  1422. return primitive._splatRowMask;
  1423. };
  1424. uniformMap.u_splatRowShift = function () {
  1425. return primitive._splatRowShift;
  1426. };
  1427. if (primitive._sphericalHarmonicsDegree > 0) {
  1428. shaderBuilder.addDefine(
  1429. "HAS_SPHERICAL_HARMONICS",
  1430. "1",
  1431. ShaderDestination.VERTEX,
  1432. );
  1433. shaderBuilder.addUniform(
  1434. "highp usampler2D",
  1435. "u_sphericalHarmonicsTexture",
  1436. ShaderDestination.VERTEX,
  1437. );
  1438. uniformMap.u_sphericalHarmonicsTexture = function () {
  1439. return primitive.sphericalHarmonicsTexture;
  1440. };
  1441. }
  1442. uniformMap.u_sphericalHarmonicsDegree = function () {
  1443. return primitive._sphericalHarmonicsDegree;
  1444. };
  1445. uniformMap.u_cameraPositionWC = function () {
  1446. return Cartesian3.clone(frameState.camera.positionWC);
  1447. };
  1448. uniformMap.u_inverseModelRotation = function () {
  1449. // SH coefficients are encoded in the GLB Y-up training space. To evaluate
  1450. // them the world-space view direction must be rotated by
  1451. // inverse(computedTransform × axisCorrectionMatrix × worldTransform).
  1452. // This matrix is pre-computed each snapshot rebuild and stored on the
  1453. // primitive so the uniform closure just returns the cached value.
  1454. return primitive._shInverseRotation;
  1455. };
  1456. uniformMap.u_splitDirection = function () {
  1457. return primitive.splitDirection;
  1458. };
  1459. const instanceCount = defined(primitive._indexes)
  1460. ? primitive._indexes.length
  1461. : primitive._numSplats;
  1462. renderResources.instanceCount = instanceCount;
  1463. renderResources.count = 4;
  1464. renderResources.primitiveType = PrimitiveType.TRIANGLE_STRIP;
  1465. shaderBuilder.addVertexLines(GaussianSplatVS);
  1466. shaderBuilder.addFragmentLines(GaussianSplatFS);
  1467. const shaderProgram = shaderBuilder.buildShaderProgram(frameState.context);
  1468. let renderState = clone(
  1469. RenderState.fromCache(renderResources.renderStateOptions),
  1470. true,
  1471. );
  1472. renderState.cull.face = ModelUtility.getCullFace(
  1473. tileset.modelMatrix,
  1474. PrimitiveType.TRIANGLE_STRIP,
  1475. );
  1476. renderState = RenderState.fromCache(renderState);
  1477. const splatQuadAttrLocations = {
  1478. screenQuadPosition: 0,
  1479. splatIndex: 2,
  1480. };
  1481. const idxAttr = new ModelComponents.Attribute();
  1482. idxAttr.name = "_SPLAT_INDEXES";
  1483. idxAttr.typedArray = primitive._indexes;
  1484. idxAttr.componentDatatype = ComponentDatatype.UNSIGNED_INT;
  1485. idxAttr.type = AttributeType.SCALAR;
  1486. idxAttr.normalized = false;
  1487. idxAttr.count = renderResources.instanceCount;
  1488. idxAttr.constant = 0;
  1489. idxAttr.instanceDivisor = 1;
  1490. const needsRebuild =
  1491. !defined(primitive._vertexArray) ||
  1492. primitive._indexes.length > primitive._vertexArrayLen;
  1493. if (needsRebuild) {
  1494. const geometry = new Geometry({
  1495. attributes: {
  1496. screenQuadPosition: new GeometryAttribute({
  1497. componentDatatype: ComponentDatatype.FLOAT,
  1498. componentsPerAttribute: 2,
  1499. values: [-1, -1, 1, -1, 1, 1, -1, 1],
  1500. name: "_SCREEN_QUAD_POS",
  1501. variableName: "screenQuadPosition",
  1502. }),
  1503. splatIndex: { ...idxAttr, variableName: "splatIndex" },
  1504. },
  1505. primitiveType: PrimitiveType.TRIANGLE_STRIP,
  1506. });
  1507. primitive._vertexArray = VertexArray.fromGeometry({
  1508. context: frameState.context,
  1509. geometry: geometry,
  1510. attributeLocations: splatQuadAttrLocations,
  1511. bufferUsage: BufferUsage.DYNAMIC_DRAW,
  1512. interleave: false,
  1513. });
  1514. } else {
  1515. primitive._vertexArray
  1516. .getAttribute(1)
  1517. .vertexBuffer.copyFromArrayView(primitive._indexes);
  1518. }
  1519. primitive._vertexArrayLen = primitive._indexes.length;
  1520. // The draw command uses rootTransform as its modelMatrix. tileset.modelMatrix
  1521. // is baked into the splat positions by transformTile and must not appear here
  1522. // as well. This keeps czm_view * modelMatrix numerically small (ENU frame),
  1523. // avoiding float32 precision loss from ECEF-scale translations.
  1524. const modelMatrix = Matrix4.clone(
  1525. primitive._rootTransform,
  1526. primitive._drawCommandModelMatrix,
  1527. );
  1528. const vertexArrayCache = primitive._vertexArray;
  1529. const command = new DrawCommand({
  1530. boundingVolume: tileset.boundingSphere,
  1531. modelMatrix: modelMatrix,
  1532. uniformMap: uniformMap,
  1533. renderState: renderState,
  1534. vertexArray: vertexArrayCache,
  1535. shaderProgram: shaderProgram,
  1536. cull: renderStateOptions.cull.enabled,
  1537. pass: Pass.GAUSSIAN_SPLATS,
  1538. count: renderResources.count,
  1539. owner: primitive,
  1540. instanceCount: renderResources.instanceCount,
  1541. primitiveType: PrimitiveType.TRIANGLE_STRIP,
  1542. debugShowBoundingVolume: tileset.debugShowBoundingVolume,
  1543. castShadows: false,
  1544. receiveShadows: false,
  1545. });
  1546. primitive._drawCommand = command;
  1547. };
  1548. /**
  1549. * Updates the Gaussian splat primitive for the current frame.
  1550. * This method checks if the primitive needs to be updated based on the current frame state,
  1551. * and if so, it processes the selected tiles, aggregates their attributes,
  1552. * and generates the Gaussian splat texture if necessary.
  1553. * It also handles the sorting of splat indexes and builds the draw command for rendering.
  1554. *
  1555. * @param {FrameState} frameState
  1556. * @private
  1557. */
  1558. GaussianSplatPrimitive.prototype.update = function (frameState) {
  1559. const tileset = this._tileset;
  1560. releaseRetiredTextures(this, frameState.frameNumber);
  1561. if (!tileset.show) {
  1562. return;
  1563. }
  1564. if (this._drawCommand) {
  1565. frameState.commandList.push(this._drawCommand);
  1566. }
  1567. if (tileset._modelMatrixChanged) {
  1568. this._dirty = true;
  1569. return;
  1570. }
  1571. const hasRootTransform = defined(this._rootTransform);
  1572. if (frameState.passes.pick === true) {
  1573. return;
  1574. }
  1575. if (this.splitDirection !== tileset.splitDirection) {
  1576. this.splitDirection = tileset.splitDirection;
  1577. }
  1578. const camera = frameState.camera;
  1579. if (!defined(camera)) {
  1580. return;
  1581. }
  1582. if (this._sorterState === GaussianSplatSortingState.IDLE) {
  1583. const selectedTilesChanged =
  1584. tileset._selectedTiles.length !== 0 &&
  1585. haveSelectedTilesChanged(this, tileset._selectedTiles);
  1586. if (tileset._selectedTiles.length === 0) {
  1587. this._selectedTilesStableFrames = 0;
  1588. this._needsSnapshotRebuild = false;
  1589. this._snapshotRebuildStallFrames = 0;
  1590. } else if (selectedTilesChanged) {
  1591. this._selectedTilesStableFrames = 0;
  1592. } else {
  1593. this._selectedTilesStableFrames++;
  1594. }
  1595. if (selectedTilesChanged || this._dirty) {
  1596. this._needsSnapshotRebuild = true;
  1597. }
  1598. const isStable = this._selectedTilesStableFrames >= DEFAULT_STABLE_FRAMES;
  1599. const isBootstrap =
  1600. !defined(this._snapshot) &&
  1601. !defined(this._pendingSnapshot) &&
  1602. !defined(this._drawCommand);
  1603. // This prevents an indefinite wait if selected tiles never settle completely.
  1604. // In practice, this is the upper bound on "wait-for-stability" before forcing
  1605. // a rebuild to avoid visible starvation.
  1606. if (this._needsSnapshotRebuild && tileset._selectedTiles.length !== 0) {
  1607. this._snapshotRebuildStallFrames++;
  1608. } else {
  1609. this._snapshotRebuildStallFrames = 0;
  1610. }
  1611. const allowRebuild =
  1612. isStable ||
  1613. isBootstrap ||
  1614. this._snapshotRebuildStallFrames >= DEFAULT_MAX_SNAPSHOT_STALL_FRAMES;
  1615. const hasPendingWork =
  1616. this._dirty ||
  1617. this._needsSnapshotRebuild ||
  1618. selectedTilesChanged ||
  1619. defined(this._pendingSnapshot) ||
  1620. defined(this._pendingSortPromise) ||
  1621. !defined(this._drawCommand);
  1622. if (
  1623. !hasPendingWork &&
  1624. Matrix4.equals(camera.viewMatrix, this._prevViewMatrix)
  1625. ) {
  1626. return;
  1627. }
  1628. if (
  1629. tileset._selectedTiles.length !== 0 &&
  1630. this._needsSnapshotRebuild &&
  1631. allowRebuild
  1632. ) {
  1633. this._splatDataGeneration++;
  1634. this._activeSort = undefined;
  1635. this._sorterPromise = undefined;
  1636. this._sorterState = GaussianSplatSortingState.IDLE;
  1637. this._pendingSortPromise = undefined;
  1638. this._pendingSort = undefined;
  1639. if (defined(this._pendingSnapshot)) {
  1640. destroySnapshotTextures(this._pendingSnapshot);
  1641. }
  1642. const tiles = tileset._selectedTiles;
  1643. // Rebuild the ENU origin from the current tileset world center so that
  1644. // baked splat positions remain in a numerically small (meter-scale) local
  1645. // frame, regardless of the current tileset.modelMatrix value.
  1646. this._rootTransform = Transforms.eastNorthUpToFixedFrame(
  1647. tileset.boundingSphere.center,
  1648. );
  1649. // Compute the SH inverse rotation from the first tile's coordinate frame.
  1650. // SH coefficients are encoded in the GLB Y-up training space. To evaluate
  1651. // them correctly the view direction must be transformed by
  1652. // inverse(computedTransform × axisCorrectionMatrix × worldTransform).
  1653. // All tiles in a typical GS tileset share the same root coordinate frame,
  1654. // so using the first tile is sufficient.
  1655. {
  1656. const ft = tiles[0];
  1657. const shFwd = Matrix4.multiplyTransformation(
  1658. ft.computedTransform,
  1659. this._axisCorrectionMatrix,
  1660. scratchMatrix4C,
  1661. );
  1662. Matrix4.multiplyTransformation(
  1663. shFwd,
  1664. ft.content.worldTransform ?? Matrix4.IDENTITY,
  1665. shFwd,
  1666. );
  1667. Matrix4.getRotation(
  1668. Matrix4.inverse(shFwd, shFwd),
  1669. this._shInverseRotation,
  1670. );
  1671. }
  1672. for (const tile of tiles) {
  1673. GaussianSplatPrimitive.transformTile(tile);
  1674. }
  1675. const totalElements = tiles.reduce(
  1676. (total, tile) => total + tile.content.pointsLength,
  1677. 0,
  1678. );
  1679. const aggregateAttributeValues = (
  1680. key,
  1681. componentDatatype,
  1682. getAttributeCallback,
  1683. numberOfComponents,
  1684. ) => {
  1685. let aggregate;
  1686. let offset = 0;
  1687. let requiredLength = 0;
  1688. for (const tile of tiles) {
  1689. const attribute = getAttributeCallback(tile.content);
  1690. const componentsPerAttribute = defined(numberOfComponents)
  1691. ? numberOfComponents
  1692. : AttributeType.getNumberOfComponents(attribute.type);
  1693. const buffer = defined(attribute.typedArray)
  1694. ? attribute.typedArray
  1695. : attribute;
  1696. requiredLength += buffer.length;
  1697. if (!defined(aggregate)) {
  1698. aggregate = acquireAggregateScratchBuffer(
  1699. this,
  1700. key,
  1701. componentDatatype,
  1702. totalElements * componentsPerAttribute,
  1703. );
  1704. }
  1705. }
  1706. if (!defined(aggregate)) {
  1707. return ComponentDatatype.createTypedArray(componentDatatype, 0);
  1708. }
  1709. for (const tile of tiles) {
  1710. const content = tile.content;
  1711. const attribute = getAttributeCallback(content);
  1712. const buffer = defined(attribute.typedArray)
  1713. ? attribute.typedArray
  1714. : attribute;
  1715. aggregate.set(buffer, offset);
  1716. offset += buffer.length;
  1717. }
  1718. return trimAggregateScratchBuffer(aggregate, requiredLength);
  1719. };
  1720. const aggregateShData = () => {
  1721. // Determine the SH degree from the first tile with SH data so we can
  1722. // pre-allocate the aggregate buffer once, outside the tile loop.
  1723. let coefs = 0;
  1724. for (const tile of tiles) {
  1725. if (tile.content.sphericalHarmonicsDegree > 0) {
  1726. switch (tile.content.sphericalHarmonicsDegree) {
  1727. case 1:
  1728. coefs = 9;
  1729. break;
  1730. case 2:
  1731. coefs = 24;
  1732. break;
  1733. case 3:
  1734. coefs = 45;
  1735. break;
  1736. }
  1737. break;
  1738. }
  1739. }
  1740. if (coefs === 0) {
  1741. return undefined;
  1742. }
  1743. const requiredLength = totalElements * (coefs * (2 / 3));
  1744. // Re-use the class-level scratch buffer when it is already large
  1745. // enough, avoiding a fresh allocation (and eventual GC) every frame.
  1746. if (
  1747. !defined(this._scratchAggregateShBuffer) ||
  1748. this._scratchAggregateShBuffer.length < requiredLength
  1749. ) {
  1750. this._scratchAggregateShBuffer = new Uint32Array(requiredLength);
  1751. }
  1752. const aggregate = this._scratchAggregateShBuffer;
  1753. let offset = 0;
  1754. for (const tile of tiles) {
  1755. const tileShData = tile.content.packedSphericalHarmonicsData;
  1756. if (tile.content.sphericalHarmonicsDegree > 0) {
  1757. aggregate.set(tileShData, offset);
  1758. offset += tileShData.length;
  1759. }
  1760. }
  1761. // Return a correctly-sized view so downstream consumers see the
  1762. // exact element count they expect.
  1763. if (offset < aggregate.length) {
  1764. return aggregate.subarray(0, offset);
  1765. }
  1766. return aggregate;
  1767. };
  1768. const positions = aggregateAttributeValues(
  1769. "positions",
  1770. ComponentDatatype.FLOAT,
  1771. (content) => content.positions,
  1772. 3,
  1773. );
  1774. const scales = aggregateAttributeValues(
  1775. "scales",
  1776. ComponentDatatype.FLOAT,
  1777. (content) => content.scales,
  1778. 3,
  1779. );
  1780. const rotations = aggregateAttributeValues(
  1781. "rotations",
  1782. ComponentDatatype.FLOAT,
  1783. (content) => content.rotations,
  1784. 4,
  1785. );
  1786. const colors = aggregateAttributeValues(
  1787. "colors",
  1788. ComponentDatatype.UNSIGNED_BYTE,
  1789. (content) =>
  1790. ModelUtility.getAttributeBySemantic(
  1791. content.gltfPrimitive,
  1792. VertexAttributeSemantic.COLOR,
  1793. ),
  1794. );
  1795. const sphericalHarmonicsDegree =
  1796. tiles[0].content.sphericalHarmonicsDegree;
  1797. const shCoefficientCount =
  1798. sphericalHarmonicsDegree > 0
  1799. ? tiles[0].content.sphericalHarmonicsCoefficientCount
  1800. : 0;
  1801. const shData = aggregateShData();
  1802. this._pendingSnapshot = {
  1803. generation: this._splatDataGeneration,
  1804. positions: positions,
  1805. rotations: rotations,
  1806. scales: scales,
  1807. colors: colors,
  1808. shData: shData,
  1809. sphericalHarmonicsDegree: sphericalHarmonicsDegree,
  1810. shCoefficientCount: shCoefficientCount,
  1811. numSplats: totalElements,
  1812. indexes: undefined,
  1813. gaussianSplatTexture: undefined,
  1814. sphericalHarmonicsTexture: undefined,
  1815. lastTextureWidth: 0,
  1816. lastTextureHeight: 0,
  1817. splatRowMask: 0, // set by processGeneratedSplatTextureData
  1818. splatRowShift: 0, // set by processGeneratedSplatTextureData
  1819. state: SnapshotState.BUILDING,
  1820. };
  1821. this.selectedTileLength = tileset._selectedTiles.length;
  1822. this._selectedTileSet = new Set(tileset._selectedTiles);
  1823. this._dirty = false;
  1824. this._needsSnapshotRebuild = false;
  1825. this._snapshotRebuildStallFrames = 0;
  1826. }
  1827. if (defined(this._pendingSnapshot)) {
  1828. const pending = this._pendingSnapshot;
  1829. if (pending.state === SnapshotState.BUILDING) {
  1830. GaussianSplatPrimitive.generateSplatTexture(this, frameState, pending);
  1831. return;
  1832. }
  1833. if (pending.state === SnapshotState.TEXTURE_PENDING) {
  1834. return;
  1835. }
  1836. if (
  1837. pending.state === SnapshotState.TEXTURE_READY &&
  1838. !defined(pending.gaussianSplatTexture)
  1839. ) {
  1840. return;
  1841. }
  1842. if (!hasRootTransform) {
  1843. return;
  1844. }
  1845. Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
  1846. Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
  1847. if (
  1848. pending.state === SnapshotState.TEXTURE_READY &&
  1849. !defined(this._pendingSortPromise)
  1850. ) {
  1851. const requestId = ++this._sortRequestId;
  1852. const dataGeneration = this._splatDataGeneration;
  1853. this._pendingSort = {
  1854. requestId: requestId,
  1855. dataGeneration: dataGeneration,
  1856. expectedCount: pending.numSplats,
  1857. snapshot: pending,
  1858. };
  1859. const sortPromise = GaussianSplatSorter.radixSortIndexes({
  1860. primitive: {
  1861. positions: new Float32Array(pending.positions),
  1862. modelView: Float32Array.from(scratchMatrix4A),
  1863. count: pending.numSplats,
  1864. },
  1865. sortType: "Index",
  1866. });
  1867. if (!defined(sortPromise)) {
  1868. this._pendingSortPromise = undefined;
  1869. this._pendingSort = undefined;
  1870. pending.state = SnapshotState.TEXTURE_READY;
  1871. return;
  1872. }
  1873. this._pendingSortPromise = sortPromise;
  1874. pending.state = SnapshotState.SORTING;
  1875. const pendingSort = this._pendingSort;
  1876. void resolvePendingSnapshotSort(
  1877. this,
  1878. frameState,
  1879. pendingSort,
  1880. sortPromise,
  1881. );
  1882. return;
  1883. }
  1884. if (!defined(this._pendingSortPromise)) {
  1885. if (pending.state === SnapshotState.SORTING) {
  1886. pending.state = SnapshotState.TEXTURE_READY;
  1887. }
  1888. return;
  1889. }
  1890. return;
  1891. }
  1892. if (this._numSplats === 0) {
  1893. return;
  1894. }
  1895. if (!hasRootTransform) {
  1896. return;
  1897. }
  1898. Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
  1899. Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
  1900. if (!defined(this._sorterPromise)) {
  1901. if (!shouldStartSteadySort(this, frameState)) {
  1902. return;
  1903. }
  1904. const requestId = ++this._sortRequestId;
  1905. const dataGeneration = this._splatDataGeneration;
  1906. const expectedCount = this._numSplats;
  1907. this._activeSort = {
  1908. requestId: requestId,
  1909. dataGeneration: dataGeneration,
  1910. expectedCount: expectedCount,
  1911. };
  1912. const rawPromise = GaussianSplatSorter.radixSortIndexes({
  1913. primitive: {
  1914. positions: new Float32Array(this._positions),
  1915. modelView: Float32Array.from(scratchMatrix4A),
  1916. count: this._numSplats,
  1917. },
  1918. sortType: "Index",
  1919. });
  1920. this._sorterPromise = rawPromise;
  1921. if (defined(rawPromise)) {
  1922. markSteadySortStart(this, frameState);
  1923. const activeSort = this._activeSort;
  1924. this._sorterState = GaussianSplatSortingState.SORTING;
  1925. void resolveSteadySort(this, activeSort, rawPromise);
  1926. return;
  1927. }
  1928. }
  1929. if (!defined(this._sorterPromise)) {
  1930. this._sorterState = GaussianSplatSortingState.WAITING;
  1931. return;
  1932. }
  1933. this._sorterState = GaussianSplatSortingState.SORTING;
  1934. return;
  1935. } else if (this._sorterState === GaussianSplatSortingState.WAITING) {
  1936. if (!defined(this._sorterPromise)) {
  1937. const requestId = ++this._sortRequestId;
  1938. const dataGeneration = this._splatDataGeneration;
  1939. const expectedCount = this._numSplats;
  1940. this._activeSort = {
  1941. requestId: requestId,
  1942. dataGeneration: dataGeneration,
  1943. expectedCount: expectedCount,
  1944. };
  1945. const rawPromise = GaussianSplatSorter.radixSortIndexes({
  1946. primitive: {
  1947. positions: new Float32Array(this._positions),
  1948. modelView: Float32Array.from(scratchMatrix4A),
  1949. count: this._numSplats,
  1950. },
  1951. sortType: "Index",
  1952. });
  1953. this._sorterPromise = rawPromise;
  1954. if (defined(rawPromise)) {
  1955. markSteadySortStart(this, frameState);
  1956. const activeSort = this._activeSort;
  1957. this._sorterState = GaussianSplatSortingState.SORTING;
  1958. void resolveSteadySort(this, activeSort, rawPromise);
  1959. return;
  1960. }
  1961. }
  1962. if (!defined(this._sorterPromise)) {
  1963. this._sorterState = GaussianSplatSortingState.WAITING;
  1964. return;
  1965. }
  1966. this._sorterState = GaussianSplatSortingState.SORTING;
  1967. return;
  1968. } else if (this._sorterState === GaussianSplatSortingState.SORTING) {
  1969. return; //still sorting, wait for next frame
  1970. } else if (this._sorterState === GaussianSplatSortingState.SORTED) {
  1971. //update the draw command if sorted
  1972. GaussianSplatPrimitive.buildGSplatDrawCommand(this, frameState);
  1973. this._sorterState = GaussianSplatSortingState.IDLE; //reset state for next frame
  1974. this._dirty = false;
  1975. this._sorterPromise = undefined; //reset promise for next frame
  1976. this._activeSort = undefined;
  1977. } else if (this._sorterState === GaussianSplatSortingState.ERROR) {
  1978. throw this._sorterError;
  1979. }
  1980. this._dirty = false;
  1981. };
  1982. export default GaussianSplatPrimitive;