| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147 |
- import Frozen from "../Core/Frozen.js";
- import Matrix4 from "../Core/Matrix4.js";
- import Matrix3 from "../Core/Matrix3.js";
- import ModelUtility from "./Model/ModelUtility.js";
- import GaussianSplatSorter from "./GaussianSplatSorter.js";
- import GaussianSplatTextureGenerator from "./GaussianSplatTextureGenerator.js";
- import ComponentDatatype from "../Core/ComponentDatatype.js";
- import PixelDatatype from "../Renderer/PixelDatatype.js";
- import PixelFormat from "../Core/PixelFormat.js";
- import Sampler from "../Renderer/Sampler.js";
- import Texture from "../Renderer/Texture.js";
- import GaussianSplatRenderResources from "./GaussianSplatRenderResources.js";
- import BlendingState from "./BlendingState.js";
- import Pass from "../Renderer/Pass.js";
- import ShaderDestination from "../Renderer/ShaderDestination.js";
- import GaussianSplatVS from "../Shaders/PrimitiveGaussianSplatVS.js";
- import GaussianSplatFS from "../Shaders/PrimitiveGaussianSplatFS.js";
- import PrimitiveType from "../Core/PrimitiveType.js";
- import DrawCommand from "../Renderer/DrawCommand.js";
- import Geometry from "../Core/Geometry.js";
- import GeometryAttribute from "../Core/GeometryAttribute.js";
- import VertexArray from "../Renderer/VertexArray.js";
- import BufferUsage from "../Renderer/BufferUsage.js";
- import RenderState from "../Renderer/RenderState.js";
- import clone from "../Core/clone.js";
- import defined from "../Core/defined.js";
- import DeveloperError from "../Core/DeveloperError.js";
- import VertexAttributeSemantic from "./VertexAttributeSemantic.js";
- import AttributeType from "./AttributeType.js";
- import ModelComponents from "./ModelComponents.js";
- import Axis from "./Axis.js";
- import Cartesian3 from "../Core/Cartesian3.js";
- import Quaternion from "../Core/Quaternion.js";
- import SplitDirection from "./SplitDirection.js";
- import destroyObject from "../Core/destroyObject.js";
- import ContextLimits from "../Renderer/ContextLimits.js";
- import Transforms from "../Core/Transforms.js";
-
- const scratchMatrix4A = new Matrix4();
- const scratchMatrix4C = new Matrix4();
- const scratchMatrix4D = new Matrix4();
- const scratchMatrix3 = new Matrix3();
- const scratchTransformQuat = new Quaternion();
- const scratchTransformPosition = new Cartesian3();
- const scratchTransformRotation = new Quaternion();
- const scratchTransformScale = new Cartesian3();
- const TRANSFORM_CACHE_EPSILON = 1e-12;
- const RIGID_TRANSFORM_EPSILON = 1e-5;
- const UNIT_SCALE_FAST_PATH_EPSILON = 1e-7;
-
- /**
- * Runtime state machine for steady-state re-sorting of an already committed snapshot.
- *
- * The transition points are in {@link GaussianSplatPrimitive#update}:
- * - IDLE -> WAITING/SORTING when a new steady sort request is scheduled
- * - WAITING -> SORTING when a sorter promise becomes available
- * - SORTING -> SORTED when the active promise resolves with a valid result
- * - SORTED -> IDLE after rebuilding the draw command with fresh indexes
- * - Any state -> ERROR when the active sort promise rejects
- *
- * @private
- */
- const GaussianSplatSortingState = {
- // No steady sort request is in flight. update() may decide to start one.
- IDLE: 0,
- // A steady sort was requested, but sorter capacity was unavailable this frame.
- WAITING: 1,
- // A steady sort promise is in flight and results are pending.
- SORTING: 2,
- // A valid sorted index buffer is available and waiting to be committed to draw command.
- SORTED: 3,
- // The active sort request failed; update() throws the captured error.
- ERROR: 4,
- };
-
- /**
- * Snapshot lifecycle for rebuilding aggregated splat data.
- *
- * Transition order:
- * BUILDING -> TEXTURE_PENDING -> TEXTURE_READY -> SORTING -> READY
- *
- * The transition points are split across two functions:
- * - {@link GaussianSplatPrimitive#update} drives BUILDING/SORTING/READY
- * - {@link GaussianSplatPrimitive.generateSplatTexture} drives TEXTURE_PENDING/TEXTURE_READY
- *
- * A snapshot is committed only when it reaches READY, so all GPU resources
- * and sorted indexes swap atomically as one unit.
- *
- * @private
- */
- const SnapshotState = {
- // CPU aggregation is complete and this snapshot is ready to start async texture generation.
- BUILDING: "BUILDING",
- // Async texture generation/upload is in flight for this snapshot.
- TEXTURE_PENDING: "TEXTURE_PENDING",
- // Attribute textures are ready; snapshot can now request index sorting.
- TEXTURE_READY: "TEXTURE_READY",
- // Sort request is in flight for this snapshot generation.
- SORTING: "SORTING",
- // Sorted indexes were validated for this generation and may be committed.
- READY: "READY",
- };
-
- /**
- * Aggregated Gaussian splat snapshot data that is built asynchronously and
- * atomically committed once all required resources are ready.
- *
- * @typedef {object} GaussianSplatPrimitive.Snapshot
- * @property {number} generation Monotonic data generation token.
- * @property {Float32Array} positions Packed splat positions (xyz).
- * @property {Float32Array} rotations Packed splat rotations (xyzw).
- * @property {Float32Array} scales Packed splat scales (xyz).
- * @property {Uint8Array} colors Packed splat colors (rgba).
- * @property {Uint32Array|undefined} shData Packed spherical harmonics data.
- * @property {number} sphericalHarmonicsDegree Spherical harmonics degree.
- * @property {number} shCoefficientCount Coefficients per splat.
- * @property {number} numSplats Total splat count in this snapshot.
- * @property {Uint32Array|undefined} indexes Sorted index buffer when READY.
- * @property {Texture|undefined} gaussianSplatTexture Packed splat attribute texture.
- * @property {Texture|undefined} sphericalHarmonicsTexture Packed SH texture.
- * @property {number} lastTextureWidth Last splat texture width.
- * @property {number} lastTextureHeight Last splat texture height.
- * @property {string} state Current snapshot lifecycle state from {@link SnapshotState}.
- * @private
- */
-
- /**
- * Packed spherical harmonics texture payload.
- *
- * @typedef {object} GaussianSplatPrimitive.SphericalHarmonicsTextureData
- * @property {number} width Texture width in texels.
- * @property {number} height Texture height in texels.
- * @property {ArrayBufferView} data Packed unsigned integer texture data.
- * @private
- */
-
- /**
- * Packed Gaussian splat attribute texture payload.
- *
- * @typedef {object} GaussianSplatPrimitive.AttributeTextureData
- * @property {number} width Texture width in texels.
- * @property {number} height Texture height in texels.
- * @property {ArrayBufferView} data Packed unsigned integer texture data.
- * @private
- */
-
- // Two stable frames avoids rebuilding during brief selected-tile jitter.
- const DEFAULT_STABLE_FRAMES = 2;
- // If selection keeps changing, force a rebuild after ~0.5s at 60fps to guarantee progress.
- // Lower values react faster but can thrash on noisy LOD transitions.
- // Higher values reduce rebuild churn but keep stale snapshots visible longer.
- const DEFAULT_MAX_SNAPSHOT_STALL_FRAMES = 30;
- // Minimum delay between steady re-sort requests once the camera is moving.
- const DEFAULT_SORT_MIN_FRAME_INTERVAL = 3;
- // ~0.5 degree camera direction change threshold before triggering steady re-sort.
- const DEFAULT_SORT_MIN_ANGLE_RADIANS = 0.008726646259971648;
- // Minimum camera movement in world units before triggering steady re-sort.
- const DEFAULT_SORT_MIN_POSITION_DELTA = 1.0;
-
- /**
- * Determines whether the camera has moved or rotated enough since the last
- * steady sort to justify scheduling a new one.
- *
- * Returns {@code true} when any of the following hold:
- * - No previous steady sort has been recorded yet.
- * - The camera position has moved by at least {@link DEFAULT_SORT_MIN_POSITION_DELTA} world units.
- * - The camera direction has changed by at least {@link DEFAULT_SORT_MIN_ANGLE_RADIANS} radians.
- *
- * A minimum frame interval ({@link DEFAULT_SORT_MIN_FRAME_INTERVAL}) is
- * enforced to prevent re-sorting every single frame.
- *
- * @param {GaussianSplatPrimitive} primitive The splat primitive to check.
- * @param {FrameState} frameState The current frame state.
- * @returns {boolean} Whether a new steady sort should begin.
- * @private
- */
- function shouldStartSteadySort(primitive, frameState) {
- const framesSinceLastSort =
- primitive._lastSteadySortFrameNumber >= 0
- ? frameState.frameNumber - primitive._lastSteadySortFrameNumber
- : Number.POSITIVE_INFINITY;
- if (
- primitive._lastSteadySortFrameNumber >= 0 &&
- framesSinceLastSort < DEFAULT_SORT_MIN_FRAME_INTERVAL
- ) {
- return false;
- }
-
- const camera = frameState.camera;
- if (!defined(camera)) {
- return false;
- }
- if (
- !primitive._hasLastSteadySortCameraPosition ||
- !primitive._hasLastSteadySortCameraDirection
- ) {
- return true;
- }
-
- const positionDelta = Cartesian3.distance(
- camera.positionWC,
- primitive._lastSteadySortCameraPosition,
- );
- if (positionDelta >= DEFAULT_SORT_MIN_POSITION_DELTA) {
- return true;
- }
-
- const angleDelta = Cartesian3.angleBetween(
- camera.directionWC,
- primitive._lastSteadySortCameraDirection,
- );
- return angleDelta >= DEFAULT_SORT_MIN_ANGLE_RADIANS;
- }
-
- /**
- * Records the frame number and camera pose at the start of a steady sort so
- * that {@link shouldStartSteadySort} can later compute deltas.
- *
- * @param {GaussianSplatPrimitive} primitive The splat primitive to update.
- * @param {FrameState} frameState The current frame state.
- * @private
- */
- function markSteadySortStart(primitive, frameState) {
- primitive._lastSteadySortFrameNumber = frameState.frameNumber;
- const camera = frameState.camera;
- if (!defined(camera)) {
- return;
- }
- Cartesian3.clone(camera.positionWC, primitive._lastSteadySortCameraPosition);
- primitive._hasLastSteadySortCameraPosition = true;
- Cartesian3.clone(
- camera.directionWC,
- primitive._lastSteadySortCameraDirection,
- );
- primitive._hasLastSteadySortCameraDirection = true;
- }
-
- /**
- * Checks whether the set of currently selected tiles differs from the set
- * recorded on the primitive. This is used to detect LOD transitions that
- * require a snapshot rebuild.
- *
- * @param {GaussianSplatPrimitive} primitive The splat primitive.
- * @param {Cesium3DTile[]} selectedTiles The tiles selected this frame.
- * @returns {boolean} {@code true} if the tile set has changed.
- * @private
- */
- function haveSelectedTilesChanged(primitive, selectedTiles) {
- const prevSet = primitive._selectedTileSet;
- if (!defined(prevSet) || prevSet.size !== selectedTiles.length) {
- return true;
- }
-
- for (let i = 0; i < selectedTiles.length; i++) {
- if (!prevSet.has(selectedTiles[i])) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Returns whether the given sort result still matches the primitive's current
- * sort request and data generation, i.e. it has not been superseded.
- *
- * @param {GaussianSplatPrimitive} primitive The splat primitive.
- * @param {object} activeSort The sort result to validate.
- * @returns {boolean} {@code true} if the sort result is still current.
- * @private
- */
- function isActiveSort(primitive, activeSort) {
- return (
- defined(activeSort) &&
- activeSort.requestId === primitive._sortRequestId &&
- activeSort.dataGeneration === primitive._splatDataGeneration
- );
- }
-
- /**
- * Destroys the GPU textures owned by a snapshot, if any, and clears the
- * references so they are not used after destruction.
- *
- * @param {GaussianSplatPrimitive.Snapshot|undefined} snapshot The snapshot whose textures should be destroyed.
- * @private
- */
- function destroySnapshotTextures(snapshot) {
- if (!defined(snapshot)) {
- return;
- }
- if (defined(snapshot.gaussianSplatTexture)) {
- snapshot.gaussianSplatTexture.destroy();
- snapshot.gaussianSplatTexture = undefined;
- }
- if (defined(snapshot.sphericalHarmonicsTexture)) {
- snapshot.sphericalHarmonicsTexture.destroy();
- snapshot.sphericalHarmonicsTexture = undefined;
- }
- }
-
- /**
- * Schedules a GPU texture for deferred destruction. The texture is kept alive
- * for one additional frame so that any in-flight draw commands that reference
- * it can finish before the underlying GPU resource is released.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {Texture|undefined} texture The texture to retire.
- * @param {number} frameNumber The frame number at which the texture was retired.
- * @private
- */
- function retireTexture(primitive, texture, frameNumber) {
- if (!defined(texture)) {
- return;
- }
- const retired = primitive._retiredTextures;
- retired.push({
- texture: texture,
- frameNumber: frameNumber,
- });
- }
-
- /**
- * Destroys any retired textures whose grace period (one frame) has elapsed.
- * Called once per frame to reclaim GPU memory from textures that were replaced
- * by a newer snapshot.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {number} frameNumber The current frame number.
- * @private
- */
- function releaseRetiredTextures(primitive, frameNumber) {
- const retired = primitive._retiredTextures;
- if (!defined(retired) || retired.length === 0) {
- return;
- }
- const next = [];
- for (let i = 0; i < retired.length; i++) {
- const entry = retired[i];
- if (frameNumber - entry.frameNumber > 0) {
- entry.texture.destroy();
- } else {
- next.push(entry);
- }
- }
- primitive._retiredTextures = next;
- }
-
- function getSnapshotArrayBuffer(snapshot, key) {
- const value = snapshot?.[key];
- return defined(value) ? value.buffer : undefined;
- }
-
- function acquireAggregateScratchBuffer(
- primitive,
- key,
- componentDatatype,
- requiredLength,
- ) {
- let pool = primitive._aggregateScratchBuffers[key];
- if (!defined(pool)) {
- pool = [];
- primitive._aggregateScratchBuffers[key] = pool;
- }
-
- const activeBuffer = getSnapshotArrayBuffer(primitive._snapshot, key);
- for (let i = 0; i < pool.length; i++) {
- const candidate = pool[i];
- if (
- candidate.length >= requiredLength &&
- candidate.buffer !== activeBuffer
- ) {
- return candidate;
- }
- }
-
- const created = ComponentDatatype.createTypedArray(
- componentDatatype,
- requiredLength,
- );
- pool.push(created);
- return created;
- }
-
- function trimAggregateScratchBuffer(buffer, length) {
- if (buffer.length === length) {
- return buffer;
- }
- return buffer.subarray(0, length);
- }
-
- /**
- * Atomically promotes a fully-built snapshot to be the active splat data for
- * the primitive. This includes swapping attribute arrays, GPU textures, and
- * sorted indexes, as well as retiring any previously active textures so they
- * can be safely destroyed after the current frame finishes.
- *
- * The snapshot <b>must</b> be in the {@link SnapshotState.READY} state;
- * otherwise a {@link DeveloperError} is thrown.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {GaussianSplatPrimitive.Snapshot} snapshot The snapshot to commit.
- * @param {FrameState} frameState The current frame state.
- * @throws {DeveloperError} If the snapshot is not READY.
- * @private
- */
- function commitSnapshot(primitive, snapshot, frameState) {
- if (!defined(snapshot.indexes) || snapshot.state !== SnapshotState.READY) {
- throw new DeveloperError("Committing snapshot before it is READY.");
- }
-
- const frameNumber = frameState.frameNumber;
- const currentSnapshot = primitive._snapshot;
- const splatTexture = defined(currentSnapshot)
- ? currentSnapshot.gaussianSplatTexture
- : primitive.gaussianSplatTexture;
- if (defined(splatTexture) && splatTexture !== snapshot.gaussianSplatTexture) {
- retireTexture(primitive, splatTexture, frameNumber);
- }
-
- const sphericalHarmonicsTexture = defined(currentSnapshot)
- ? currentSnapshot.sphericalHarmonicsTexture
- : primitive.sphericalHarmonicsTexture;
- if (
- defined(sphericalHarmonicsTexture) &&
- sphericalHarmonicsTexture !== snapshot.sphericalHarmonicsTexture
- ) {
- retireTexture(primitive, sphericalHarmonicsTexture, frameNumber);
- }
-
- primitive._snapshot = snapshot;
- primitive._positions = snapshot.positions;
- primitive._rotations = snapshot.rotations;
- primitive._scales = snapshot.scales;
- primitive._colors = snapshot.colors;
- primitive._shData = snapshot.shData;
- primitive._sphericalHarmonicsDegree = snapshot.sphericalHarmonicsDegree;
- primitive._numSplats = snapshot.numSplats;
- primitive._indexes = snapshot.indexes;
- primitive.gaussianSplatTexture = snapshot.gaussianSplatTexture;
- primitive.sphericalHarmonicsTexture = snapshot.sphericalHarmonicsTexture;
- primitive._lastTextureWidth = snapshot.lastTextureWidth;
- primitive._lastTextureHeight = snapshot.lastTextureHeight;
- // Commit row-addressing params alongside the texture; the shader must
- // always see the mask/shift that matches the active texture layout.
- primitive._splatRowMask = snapshot.splatRowMask;
- primitive._splatRowShift = snapshot.splatRowShift;
- // Above 1.0 when the previous snapshot hit the hard cap; used in
- // _wrappedUpdate to inflate traversal SSE and reduce tile load.
- primitive._splatBudgetSSEScale = snapshot.splatBudgetSSEScale ?? 1.0;
- primitive._hasGaussianSplatTexture = defined(snapshot.gaussianSplatTexture);
- primitive._needsGaussianSplatTexture = false;
- primitive._gaussianSplatTexturePending = false;
-
- primitive._vertexArray = undefined;
- primitive._vertexArrayLen = -1;
- primitive._drawCommand = undefined;
- primitive._sorterPromise = undefined;
- primitive._activeSort = undefined;
- primitive._sorterState = GaussianSplatSortingState.IDLE;
- primitive._dirty = false;
- }
-
- /**
- * Finalizes async splat texture generation for a snapshot. The resolved data
- * updates or recreates GPU textures, and the snapshot transitions to
- * {@link SnapshotState.TEXTURE_READY} when complete.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {FrameState} frameState The current frame state.
- * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
- * @param {Promise<GaussianSplatPrimitive.AttributeTextureData>} promise Promise that resolves to packed splat texture data.
- * @returns {Promise<void>}
- * @private
- */
- async function processGeneratedSplatTextureData(
- primitive,
- frameState,
- snapshot,
- promise,
- ) {
- try {
- const splatTextureData = await promise;
- const maxTex = ContextLimits.maximumTextureSize;
-
- // Use maximumTextureSize as the texture width; splatsPerRow = maxTex / 2
- // (each splat occupies 2 side-by-side texels). The WASM buffer layout is
- // width-independent, so the raw data is reused as-is.
- const optimalWidth = maxTex;
- let optimalHeight = Math.ceil(snapshot.numSplats / (maxTex / 2));
- const splatRowShift = Math.log2(maxTex / 2);
- const splatRowMask = maxTex / 2 - 1;
-
- // Hard cap: >maxTex*(maxTex/2) splats cannot fit in any valid texture.
- if (optimalHeight > maxTex) {
- const originalCount = snapshot.numSplats;
- optimalHeight = maxTex;
- const splatsPerRow = optimalWidth / 2;
- snapshot.numSplats = maxTex * splatsPerRow;
- // Truncate CPU attribute arrays to match numSplats.
- snapshot.positions = snapshot.positions.subarray(
- 0,
- snapshot.numSplats * 3,
- );
- snapshot.rotations = snapshot.rotations.subarray(
- 0,
- snapshot.numSplats * 4,
- );
- snapshot.scales = snapshot.scales.subarray(0, snapshot.numSplats * 3);
- snapshot.colors = snapshot.colors.subarray(0, snapshot.numSplats * 4);
- // shData is allocated independently and must be truncated separately.
- if (defined(snapshot.shData)) {
- const shPerSplat = snapshot.shData.length / originalCount;
- snapshot.shData = snapshot.shData.subarray(
- 0,
- Math.floor(snapshot.numSplats * shPerSplat),
- );
- }
- // Scale up SSE next frame so traversal selects fewer tiles.
- snapshot.splatBudgetSSEScale = originalCount / snapshot.numSplats;
- console.warn(
- `[GaussianSplat][HARD CAP] ${originalCount} splats exceed the maximum texture capacity ` +
- `(${maxTex}\u00d7${splatsPerRow} = ${snapshot.numSplats} splats at width=${optimalWidth}). ` +
- `Rendering only the first ${snapshot.numSplats} splats to avoid a WebGL crash. ` +
- `Increasing maximumScreenSpaceError by ${snapshot.splatBudgetSSEScale.toFixed(2)}x next frame.`,
- );
- } else {
- // Within budget; clear any SSE inflation carried over from a previous cap.
- snapshot.splatBudgetSSEScale = 1.0;
- }
-
- // Trim or zero-pad the raw WASM buffer to match the chosen dimensions.
- const requiredLen = optimalWidth * optimalHeight * 4;
- let effectiveData;
- if (requiredLen <= splatTextureData.data.length) {
- effectiveData = splatTextureData.data.subarray(0, requiredLen);
- } else {
- effectiveData = new Uint32Array(requiredLen);
- effectiveData.set(splatTextureData.data);
- }
- const effectiveTextureData = {
- width: optimalWidth,
- height: optimalHeight,
- data: effectiveData,
- };
-
- snapshot.splatRowMask = splatRowMask;
- snapshot.splatRowShift = splatRowShift;
-
- if (primitive._pendingSnapshot !== snapshot) {
- snapshot.state = SnapshotState.BUILDING;
- return;
- }
- if (!defined(snapshot.gaussianSplatTexture)) {
- snapshot.gaussianSplatTexture = createGaussianSplatTexture(
- frameState.context,
- effectiveTextureData,
- );
- } else if (
- snapshot.lastTextureHeight !== effectiveTextureData.height ||
- snapshot.lastTextureWidth !== effectiveTextureData.width
- ) {
- const oldTex = snapshot.gaussianSplatTexture;
- snapshot.gaussianSplatTexture = createGaussianSplatTexture(
- frameState.context,
- effectiveTextureData,
- );
- oldTex.destroy();
- } else {
- snapshot.gaussianSplatTexture.copyFrom({
- source: {
- width: effectiveTextureData.width,
- height: effectiveTextureData.height,
- arrayBufferView: effectiveTextureData.data,
- },
- });
- }
- snapshot.lastTextureHeight = effectiveTextureData.height;
- snapshot.lastTextureWidth = effectiveTextureData.width;
-
- if (defined(snapshot.shData) && snapshot.sphericalHarmonicsDegree > 0) {
- const oldTex = snapshot.sphericalHarmonicsTexture;
- const width = ContextLimits.maximumTextureSize;
- const dims = snapshot.shCoefficientCount / 3;
- const splatsPerRow = Math.floor(width / dims);
- const floatsPerRow = splatsPerRow * (dims * 2);
-
- const shHeight = Math.ceil(snapshot.numSplats / splatsPerRow);
-
- // SH texture width is already maxTex and cannot be widened further.
- // When height would exceed the GPU limit, gracefully disable SH for this
- // snapshot and fall back to base color rendering rather than crashing.
- if (shHeight > width) {
- console.warn(
- `[GaussianSplat][SHTexture] ${snapshot.numSplats} splats require SH height ${shHeight} > maxTex ${width}. ` +
- `Disabling spherical harmonics for this snapshot (color-only fallback).`,
- );
- snapshot.sphericalHarmonicsDegree = 0;
- if (defined(oldTex)) {
- oldTex.destroy();
- }
- snapshot.sphericalHarmonicsTexture = undefined;
- } else {
- const texBuf = new Uint32Array(width * shHeight * 2);
-
- let dataIndex = 0;
- for (let i = 0; dataIndex < snapshot.shData.length; i += width * 2) {
- texBuf.set(
- snapshot.shData.subarray(dataIndex, dataIndex + floatsPerRow),
- i,
- );
- dataIndex += floatsPerRow;
- }
- snapshot.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
- frameState.context,
- {
- data: texBuf,
- width: width,
- height: shHeight,
- },
- );
- if (defined(oldTex)) {
- oldTex.destroy();
- }
- }
- }
-
- snapshot.state = SnapshotState.TEXTURE_READY;
- } catch (error) {
- console.error("Error generating Gaussian splat texture:", error);
- snapshot.state = SnapshotState.BUILDING;
- }
- }
-
- /**
- * Resolves an in-flight sort for a pending snapshot, validates that the result
- * still matches the active generation/request, and commits the snapshot when
- * valid.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {FrameState} frameState The current frame state.
- * @param {object|undefined} pendingSort Pending sort metadata.
- * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
- * @returns {Promise<void>}
- * @private
- */
- async function resolvePendingSnapshotSort(
- primitive,
- frameState,
- pendingSort,
- sortPromise,
- ) {
- try {
- const sortedData = await sortPromise;
- if (
- !defined(pendingSort) ||
- pendingSort.snapshot !== primitive._pendingSnapshot
- ) {
- return;
- }
- const expectedCount = pendingSort.expectedCount;
- const currentCount = expectedCount;
- const sortedLen = sortedData?.length;
- if (expectedCount !== currentCount || sortedLen !== expectedCount) {
- primitive._pendingSortPromise = undefined;
- primitive._pendingSort = undefined;
- if (pendingSort.snapshot.state === SnapshotState.SORTING) {
- pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
- }
- return;
- }
-
- const pending = pendingSort.snapshot;
- pending.indexes = sortedData;
- pending.state = SnapshotState.READY;
- primitive._pendingSortPromise = undefined;
- primitive._pendingSort = undefined;
- commitSnapshot(primitive, pending, frameState);
- primitive._pendingSnapshot = undefined;
- GaussianSplatPrimitive.buildGSplatDrawCommand(primitive, frameState);
- } catch (err) {
- if (
- !defined(pendingSort) ||
- pendingSort.snapshot !== primitive._pendingSnapshot
- ) {
- return;
- }
- primitive._pendingSortPromise = undefined;
- primitive._pendingSort = undefined;
- if (pendingSort.snapshot.state === SnapshotState.SORTING) {
- pendingSort.snapshot.state = SnapshotState.TEXTURE_READY;
- }
- primitive._sorterState = GaussianSplatSortingState.ERROR;
- primitive._sorterError = err;
- }
- }
-
- /**
- * Resolves an in-flight steady-state sort for the current committed snapshot.
- * Results are ignored when superseded; otherwise, they advance sorting state
- * to {@link GaussianSplatSortingState.SORTED}.
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {object|undefined} activeSort Active sort metadata.
- * @param {Promise<Uint32Array>} sortPromise Promise that resolves to sorted indexes.
- * @returns {Promise<void>}
- * @private
- */
- async function resolveSteadySort(primitive, activeSort, sortPromise) {
- try {
- const sortedData = await sortPromise;
- const isActive = isActiveSort(primitive, activeSort);
- const expectedCount = activeSort?.expectedCount;
- const currentCount = expectedCount;
- const sortedLen = sortedData?.length;
- const isMismatch =
- expectedCount !== currentCount || sortedLen !== expectedCount;
- if (!isActive || isMismatch) {
- if (isActive) {
- primitive._sorterPromise = undefined;
- primitive._sorterState = GaussianSplatSortingState.IDLE;
- }
- return;
- }
- primitive._indexes = sortedData;
- primitive._sorterState = GaussianSplatSortingState.SORTED;
- } catch (err) {
- if (!isActiveSort(primitive, activeSort)) {
- return;
- }
- primitive._sorterState = GaussianSplatSortingState.ERROR;
- primitive._sorterError = err;
- }
- }
-
- /**
- * Creates a GPU texture that stores packed spherical harmonics coefficient
- * data for all splats. The texture uses a two-channel unsigned-integer format
- * ({@link PixelFormat.RG_INTEGER}) and nearest-neighbor sampling.
- *
- * @param {Context} context The WebGL context.
- * @param {GaussianSplatPrimitive.SphericalHarmonicsTextureData} shData Packed SH texture payload.
- * @returns {Texture} The created texture.
- * @private
- */
- function createSphericalHarmonicsTexture(context, shData) {
- const texture = new Texture({
- context: context,
- source: {
- width: shData.width,
- height: shData.height,
- arrayBufferView: shData.data,
- },
- preMultiplyAlpha: false,
- skipColorSpaceConversion: true,
- pixelFormat: PixelFormat.RG_INTEGER,
- pixelDatatype: PixelDatatype.UNSIGNED_INT,
- flipY: false,
- sampler: Sampler.NEAREST,
- });
-
- return texture;
- }
-
- /**
- * Creates a GPU texture that stores the packed Gaussian splat attributes
- * (positions, scales, rotations, colors). The texture uses an RGBA
- * unsigned-integer format ({@link PixelFormat.RGBA_INTEGER}) and
- * nearest-neighbor sampling.
- *
- * @param {Context} context The WebGL context.
- * @param {GaussianSplatPrimitive.AttributeTextureData} splatTextureData Packed splat texture payload.
- * @returns {Texture} The created texture.
- * @private
- */
- function createGaussianSplatTexture(context, splatTextureData) {
- return new Texture({
- context: context,
- source: {
- width: splatTextureData.width,
- height: splatTextureData.height,
- arrayBufferView: splatTextureData.data,
- },
- preMultiplyAlpha: false,
- skipColorSpaceConversion: true,
- pixelFormat: PixelFormat.RGBA_INTEGER,
- pixelDatatype: PixelDatatype.UNSIGNED_INT,
- flipY: false,
- sampler: Sampler.NEAREST,
- });
- }
-
- /** A primitive that renders Gaussian splats.
- * <p>
- * This primitive is used to render Gaussian splats in a 3D Tileset.
- * It is designed to work with the KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extensions.
- * </p>
- * @alias GaussianSplatPrimitive
- * @constructor
- * @param {object} options An object with the following properties:
- * @param {Cesium3DTileset} options.tileset The tileset that this primitive belongs to.
- * @param {boolean} [options.debugShowBoundingVolume=false] Whether to show the bounding volume of the primitive for debugging purposes.
- * @private
- */
-
- function GaussianSplatPrimitive(options) {
- options = options ?? Frozen.EMPTY_OBJECT;
-
- /**
- * The positions of the Gaussian splats in the primitive.
- * @type {undefined|Float32Array}
- * @private
- */
- this._positions = undefined;
- /**
- * The rotations of the Gaussian splats in the primitive.
- * @type {undefined|Float32Array}
- * @private
- */
- this._rotations = undefined;
- /**
- * The scales of the Gaussian splats in the primitive.
- * @type {undefined|Float32Array}
- * @private
- */
- this._scales = undefined;
- /**
- * The colors of the Gaussian splats in the primitive.
- * @type {undefined|Uint8Array}
- * @private
- */
- this._colors = undefined;
- /**
- * The indexes of the Gaussian splats in the primitive.
- * Used to index into the splat attribute texture in the vertex shader.
- * @type {undefined|Uint32Array}
- * @private
- */
- this._indexes = undefined;
- /**
- * The number of splats in the primitive.
- * This is the total number of splats across all selected tiles.
- * @type {number}
- * @private
- */
- this._numSplats = 0;
- /**
- * Indicates whether or not the primitive needs a Gaussian splat texture.
- * This is set to true when the primitive is first created or when the splat attributes change.
- * @type {boolean}
- * @private
- */
- this._needsGaussianSplatTexture = true;
- this._snapshot = undefined;
- this._pendingSnapshot = undefined;
- this._retiredTextures = [];
- this._aggregateScratchBuffers = {
- positions: [],
- scales: [],
- rotations: [],
- colors: [],
- };
-
- /**
- * Scratch buffer re-used across frames for aggregating packed spherical
- * harmonics data so that a fresh typed-array allocation is avoided on
- * every tile-selection change.
- * @type {undefined|Uint32Array}
- * @private
- */
- this._scratchAggregateShBuffer = undefined;
- this._selectedTilesStableFrames = 0;
- this._needsSnapshotRebuild = false;
- this._snapshotRebuildStallFrames = 0;
-
- /**
- * The previous view matrix used to determine if the primitive needs to be updated.
- * This is used to avoid unnecessary updates when the view matrix hasn't changed.
- * @type {Matrix4}
- * @private
- */
- this._prevViewMatrix = new Matrix4();
-
- /**
- * Indicates whether or not to show the bounding volume of the primitive for debugging purposes.
- * This is used to visualize the bounding volume of the primitive in the scene.
- * @type {boolean}
- * @private
- */
- this._debugShowBoundingVolume = options.debugShowBoundingVolume ?? false;
-
- /**
- * The texture used to store the Gaussian splat attributes.
- * This texture is created from the splat attributes (positions, scales, rotations, colors)
- * and is used in the vertex shader to render the splats.
- * @type {undefined|Texture}
- * @private
- * @see {@link GaussianSplatTextureGenerator}
- */
- this.gaussianSplatTexture = undefined;
-
- /**
- * The texture used to store the spherical harmonics coefficients for the Gaussian splats.
- * @type {undefined|Texture}
- * @private
- */
- this.sphericalHarmonicsTexture = undefined;
-
- /**
- * The last width of the Gaussian splat texture.
- * This is used to track changes in the texture size and update the primitive accordingly.
- * @type {number}
- * @private
- */
- this._lastTextureWidth = 0;
- /**
- * The last height of the Gaussian splat texture.
- * This is used to track changes in the texture size and update the primitive accordingly.
- * @type {number}
- * @private
- */
- this._lastTextureHeight = 0;
- /**
- * The vertex array used to render the Gaussian splats.
- * This vertex array contains the attributes needed to render the splats, such as positions and indexes.
- * @type {undefined|VertexArray}
- * @private
- */
- this._vertexArray = undefined;
- /**
- * The length of the vertex array, used to track changes in the number of splats.
- * This is used to determine if the vertex array needs to be rebuilt.
- * @type {number}
- * @private
- */
- this._vertexArrayLen = -1;
- this._splitDirection = SplitDirection.NONE;
-
- /**
- * The dirty flag forces the primitive to render this frame.
- * @type {boolean}
- * @private
- */
- this._dirty = false;
-
- this._tileset = options.tileset;
-
- this._baseTilesetUpdate = this._tileset.update;
- this._tileset.update = this._wrappedUpdate.bind(this);
-
- this._tileset.tileLoad.addEventListener(this.onTileLoad, this);
- this._tileset.tileVisible.addEventListener(this.onTileVisible, this);
-
- /**
- * Tracks current count of selected tiles.
- * This is used to determine if the primitive needs to be rebuilt.
- * @type {number}
- * @private
- */
- this.selectedTileLength = 0;
- this._selectedTileSet = new Set();
-
- /**
- * Indicates whether or not the primitive is ready for use.
- * @type {boolean}
- * @private
- */
- this._ready = false;
-
- /**
- * Indicates whether or not the primitive has a Gaussian splat texture.
- * @type {boolean}
- * @private
- */
- this._hasGaussianSplatTexture = false;
-
- /**
- * Indicates whether or not the primitive is currently generating a Gaussian splat texture.
- * @type {boolean}
- * @private
- */
- this._gaussianSplatTexturePending = false;
-
- /**
- * The draw command used to render the Gaussian splats.
- * @type {undefined|DrawCommand}
- * @private
- */
- this._drawCommand = undefined;
- this._drawCommandModelMatrix = new Matrix4();
- /**
- * The root transform of the tileset.
- * This is used to transform the splats into world space.
- * @type {undefined|Matrix4}
- * @private
- */
- this._rootTransform = undefined;
-
- /**
- * The axis correction matrix to transform the splats from Y-up to Z-up.
- * @type {Matrix4}
- * @private
- */
- this._axisCorrectionMatrix = ModelUtility.getAxisCorrectionMatrix(
- Axis.Y,
- Axis.X,
- new Matrix4(),
- );
-
- /**
- * Cached inverse rotation for SH evaluation, updated each snapshot rebuild.
- * Converts a world-space view direction to the original GLB Y-up model space
- * so that spherical harmonic coefficients are evaluated in the correct frame.
- * @type {Matrix3}
- * @private
- */
- this._shInverseRotation = new Matrix3();
-
- /**
- * Indicates whether or not the primitive has been destroyed.
- * @type {boolean}
- * @private
- */
- this._isDestroyed = false;
-
- /**
- * The state of the Gaussian splat sorting process.
- * This is used to track the progress of the sorting operation.
- * @type {GaussianSplatSortingState}
- * @private
- */
- this._sorterState = GaussianSplatSortingState.IDLE;
- /**
- * A promise that resolves when the Gaussian splat sorting operation is complete.
- * This is used to track the progress of the sorting operation.
- * @type {undefined|Promise}
- * @private
- */
- this._sorterPromise = undefined;
- this._splatDataGeneration = 0;
- this._sortRequestId = 0;
- this._activeSort = undefined;
- this._pendingSortPromise = undefined;
- this._pendingSort = undefined;
- this._lastSteadySortFrameNumber = -1;
- this._lastSteadySortCameraPosition = new Cartesian3();
- this._hasLastSteadySortCameraPosition = false;
- this._lastSteadySortCameraDirection = new Cartesian3();
- this._hasLastSteadySortCameraDirection = false;
-
- /**
- * An error that occurred during the Gaussian splat sorting operation.
- * Thrown when state is ERROR.
- * @type {undefined|Error}
- * @private
- */
- this._sorterError = undefined;
-
- // Splat texture row-addressing params; forwarded to the shader as uniforms.
- // The texture width is always maximumTextureSize (varies by GPU), so these
- // are computed per-snapshot and initialized here as safe placeholders.
- this._splatRowMask = 0; // overwritten on first snapshot commit
- this._splatRowShift = 0; // overwritten on first snapshot commit
-
- /**
- * Multiplier applied to maximumScreenSpaceError during tile traversal when
- * the previous snapshot exceeded the splat texture budget. A value above 1.0
- * biases traversal toward coarser LODs, lowering the total splat count.
- * Resets to 1.0 once the splat count is within budget.
- * @type {number}
- * @private
- */
- this._splatBudgetSSEScale = 1.0;
- }
-
- Object.defineProperties(GaussianSplatPrimitive.prototype, {
- /**
- * Indicates whether the primitive is ready for use.
- * @memberof GaussianSplatPrimitive.prototype
- * @type {boolean}
- * @readonly
- */
- ready: {
- get: function () {
- return this._ready;
- },
- },
-
- /**
- * Indicates whether the primitive has completed loading and sorting.
- * @memberof GaussianSplatPrimitive.prototype
- * @type {boolean}
- * @private
- * @readonly
- */
- isStable: {
- get: function () {
- return (
- !this._dirty &&
- (!defined(this._pendingSnapshot) ||
- this._pendingSnapshot.state === SnapshotState.READY)
- );
- },
- },
-
- /**
- * The {@link SplitDirection} to apply to this point.
- * @memberof GaussianSplatPrimitive.prototype
- * @type {SplitDirection}
- * @default {@link SplitDirection.NONE}
- */
- splitDirection: {
- get: function () {
- return this._splitDirection;
- },
- set: function (value) {
- if (this._splitDirection !== value) {
- this._splitDirection = value;
- this._dirty = true;
- }
- },
- },
- });
-
- /**
- * Replaces the tileset's own update function so this primitive is updated
- * immediately after the tileset traversal, within the same frame. When
- * _splatBudgetSSEScale is above 1.0, maximumScreenSpaceError is inflated
- * for the duration of the traversal to reduce the number of tiles selected.
- * @param {FrameState} frameState
- * @private
- */
- GaussianSplatPrimitive.prototype._wrappedUpdate = function (frameState) {
- const tileset = this._tileset;
- if (this._splatBudgetSSEScale !== 1.0) {
- // Inflate SSE for this traversal only; the original value is restored
- // immediately so the user-visible tileset property is never permanently changed.
- const originalSSE = tileset.maximumScreenSpaceError;
- tileset.maximumScreenSpaceError *= this._splatBudgetSSEScale;
- this._baseTilesetUpdate.call(tileset, frameState);
- tileset.maximumScreenSpaceError = originalSSE;
- } else {
- this._baseTilesetUpdate.call(tileset, frameState);
- }
- this.update(frameState);
- };
-
- /**
- * Destroys the primitive and releases its resources in a deterministic manner.
- * @private
- */
- GaussianSplatPrimitive.prototype.destroy = function () {
- this._positions = undefined;
- this._rotations = undefined;
- this._scales = undefined;
- this._colors = undefined;
- this._indexes = undefined;
- destroySnapshotTextures(this._pendingSnapshot);
- destroySnapshotTextures(this._snapshot);
- if (defined(this._retiredTextures)) {
- for (let i = 0; i < this._retiredTextures.length; i++) {
- this._retiredTextures[i].texture.destroy();
- }
- }
- this._retiredTextures = [];
- this._pendingSnapshot = undefined;
- this._snapshot = undefined;
- this._aggregateScratchBuffers = undefined;
- this.gaussianSplatTexture = undefined;
- this.sphericalHarmonicsTexture = undefined;
-
- const drawCommand = this._drawCommand;
- if (defined(drawCommand)) {
- drawCommand.shaderProgram =
- drawCommand.shaderProgram && drawCommand.shaderProgram.destroy();
- }
-
- if (defined(this._vertexArray)) {
- this._vertexArray.destroy();
- this._vertexArray = undefined;
- }
-
- this._tileset.update = this._baseTilesetUpdate.bind(this._tileset);
-
- return destroyObject(this);
- };
-
- /**
- * Returns true if this object was destroyed; otherwise, false.
- * <br /><br />
- * If this object was destroyed, it should not be used; calling any function other than
- * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
- * @returns {boolean} Returns true if the primitive has been destroyed, otherwise false.
- * @private
- */
- GaussianSplatPrimitive.prototype.isDestroyed = function () {
- return this._isDestroyed;
- };
-
- /**
- * Event callback for when a tile is loaded.
- * This method is called when a tile is loaded and the primitive needs to be updated.
- * It sets the dirty flag to true, indicating that the primitive needs to be rebuilt.
- * @param {Cesium3DTile} tile
- * @private
- */
- GaussianSplatPrimitive.prototype.onTileLoad = function (tile) {
- this._dirty = true;
- };
-
- /**
- * Callback for visible tiles.
- * @param {Cesium3DTile} tile
- * @private
- */
- GaussianSplatPrimitive.prototype.onTileVisible = function (tile) {};
-
- /**
- * Transforms the tile's splat primitive attributes into world space.
- * <br /><br />
- * This method applies the computed transform of the tile and the tileset's bounding sphere
- * to the splat primitive's position, rotation, and scale attributes.
- * It modifies the attributes in place, transforming them from local space to world space.
- *
- * @param {Cesium3DTile} tile
- * @private
- */
- GaussianSplatPrimitive.transformTile = function (tile) {
- const computedTransform = tile.computedTransform;
- const gltfPrimitive = tile.content.gltfPrimitive;
- const gaussianSplatPrimitive = tile.tileset.gaussianSplatPrimitive;
-
- if (gaussianSplatPrimitive._rootTransform === undefined) {
- gaussianSplatPrimitive._rootTransform = Transforms.eastNorthUpToFixedFrame(
- tile.tileset.boundingSphere.center,
- );
- }
- const rootTransform = gaussianSplatPrimitive._rootTransform;
-
- const computedModelMatrix = Matrix4.multiplyTransformation(
- computedTransform,
- gaussianSplatPrimitive._axisCorrectionMatrix,
- scratchMatrix4A,
- );
-
- Matrix4.multiplyTransformation(
- computedModelMatrix,
- tile.content.worldTransform,
- computedModelMatrix,
- );
-
- // toLocal is inverse(rootTransform) only. tileset.modelMatrix is already
- // factored into computedModelMatrix via tile.computedTransform, so its effect
- // is baked directly into the splat values rather than split into the draw
- // command's modelMatrix. This keeps czm_view * modelMatrix numerically small,
- // avoiding float32 precision loss at ECEF-scale translations.
- const toLocal = Matrix4.inverse(rootTransform, scratchMatrix4C);
- const transform = Matrix4.multiplyTransformation(
- toLocal,
- computedModelMatrix,
- scratchMatrix4A,
- );
- const cachedTransform = tile.content._lastSplatTransform;
- if (
- tile.content._transformed &&
- defined(cachedTransform) &&
- Matrix4.equalsEpsilon(transform, cachedTransform, TRANSFORM_CACHE_EPSILON)
- ) {
- return;
- }
- const positions = tile.content.positions;
- const rotations = tile.content.rotations;
- const scales = tile.content.scales;
-
- // Extract the rotation quaternion from transform once, before the per-splat
- // loop. The columns of transform's upper-left 3x3 have magnitude ≈ 1 (rigid
- // body placement), so normalizing is numerically stable. We cannot decompose
- // the per-splat combined matrix (transform × TRS_i) instead, because each
- // splat's scale can be very small, causing catastrophic cancellation when
- // dividing to recover a pure rotation matrix.
- const col0Len = Math.sqrt(
- transform[0] * transform[0] +
- transform[1] * transform[1] +
- transform[2] * transform[2],
- );
- const col1Len = Math.sqrt(
- transform[4] * transform[4] +
- transform[5] * transform[5] +
- transform[6] * transform[6],
- );
- const col2Len = Math.sqrt(
- transform[8] * transform[8] +
- transform[9] * transform[9] +
- transform[10] * transform[10],
- );
- scratchMatrix3[0] = transform[0] / col0Len;
- scratchMatrix3[1] = transform[1] / col0Len;
- scratchMatrix3[2] = transform[2] / col0Len;
- scratchMatrix3[3] = transform[4] / col1Len;
- scratchMatrix3[4] = transform[5] / col1Len;
- scratchMatrix3[5] = transform[6] / col1Len;
- scratchMatrix3[6] = transform[8] / col2Len;
- scratchMatrix3[7] = transform[9] / col2Len;
- scratchMatrix3[8] = transform[10] / col2Len;
- const dot01 =
- scratchMatrix3[0] * scratchMatrix3[3] +
- scratchMatrix3[1] * scratchMatrix3[4] +
- scratchMatrix3[2] * scratchMatrix3[5];
- const dot02 =
- scratchMatrix3[0] * scratchMatrix3[6] +
- scratchMatrix3[1] * scratchMatrix3[7] +
- scratchMatrix3[2] * scratchMatrix3[8];
- const dot12 =
- scratchMatrix3[3] * scratchMatrix3[6] +
- scratchMatrix3[4] * scratchMatrix3[7] +
- scratchMatrix3[5] * scratchMatrix3[8];
- const hasUnitScale =
- Math.abs(col0Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON &&
- Math.abs(col1Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON &&
- Math.abs(col2Len - 1.0) <= UNIT_SCALE_FAST_PATH_EPSILON;
- const isOrthogonal =
- Math.abs(dot01) <= RIGID_TRANSFORM_EPSILON &&
- Math.abs(dot02) <= RIGID_TRANSFORM_EPSILON &&
- Math.abs(dot12) <= RIGID_TRANSFORM_EPSILON;
- const determinant =
- scratchMatrix3[0] *
- (scratchMatrix3[4] * scratchMatrix3[8] -
- scratchMatrix3[5] * scratchMatrix3[7]) -
- scratchMatrix3[3] *
- (scratchMatrix3[1] * scratchMatrix3[8] -
- scratchMatrix3[2] * scratchMatrix3[7]) +
- scratchMatrix3[6] *
- (scratchMatrix3[1] * scratchMatrix3[5] -
- scratchMatrix3[2] * scratchMatrix3[4]);
- const useFastPath =
- hasUnitScale &&
- isOrthogonal &&
- Math.abs(determinant - 1.0) <= RIGID_TRANSFORM_EPSILON;
- Quaternion.fromRotationMatrix(scratchMatrix3, scratchTransformQuat);
- Quaternion.normalize(scratchTransformQuat, scratchTransformQuat);
- const attributePositions = ModelUtility.getAttributeBySemantic(
- gltfPrimitive,
- VertexAttributeSemantic.POSITION,
- ).typedArray;
-
- const attributeRotations = ModelUtility.getAttributeBySemantic(
- gltfPrimitive,
- VertexAttributeSemantic.ROTATION,
- ).typedArray;
-
- const attributeScales = ModelUtility.getAttributeBySemantic(
- gltfPrimitive,
- VertexAttributeSemantic.SCALE,
- ).typedArray;
-
- const position = scratchTransformPosition;
- const rotation = scratchTransformRotation;
- const scale = scratchTransformScale;
- for (let i = 0; i < attributePositions.length / 3; ++i) {
- position.x = attributePositions[i * 3];
- position.y = attributePositions[i * 3 + 1];
- position.z = attributePositions[i * 3 + 2];
-
- rotation.x = attributeRotations[i * 4];
- rotation.y = attributeRotations[i * 4 + 1];
- rotation.z = attributeRotations[i * 4 + 2];
- rotation.w = attributeRotations[i * 4 + 3];
-
- scale.x = attributeScales[i * 3];
- scale.y = attributeScales[i * 3 + 1];
- scale.z = attributeScales[i * 3 + 2];
-
- if (useFastPath) {
- Matrix4.multiplyByPoint(transform, position, position);
- Quaternion.multiply(scratchTransformQuat, rotation, rotation);
- Quaternion.normalize(rotation, rotation);
- } else {
- Matrix4.fromTranslationQuaternionRotationScale(
- position,
- rotation,
- scale,
- scratchMatrix4D,
- );
-
- Matrix4.multiplyTransformation(
- transform,
- scratchMatrix4D,
- scratchMatrix4D,
- );
-
- Matrix4.getTranslation(scratchMatrix4D, position);
- Matrix4.getScale(scratchMatrix4D, scale);
- // rotation still holds the original splat quaternion from attributeRotations.
- // Apply the transform's rotation by left-multiplying the transform quaternion.
- Quaternion.multiply(scratchTransformQuat, rotation, rotation);
- Quaternion.normalize(rotation, rotation);
- }
-
- positions[i * 3] = position.x;
- positions[i * 3 + 1] = position.y;
- positions[i * 3 + 2] = position.z;
-
- rotations[i * 4] = rotation.x;
- rotations[i * 4 + 1] = rotation.y;
- rotations[i * 4 + 2] = rotation.z;
- rotations[i * 4 + 3] = rotation.w;
-
- scales[i * 3] = scale.x;
- scales[i * 3 + 1] = scale.y;
- scales[i * 3 + 2] = scale.z;
- }
- tile.content._lastSplatTransform = Matrix4.clone(
- transform,
- tile.content._lastSplatTransform,
- );
- tile.content._transformed = true;
- };
-
- /**
- * Generates the Gaussian splat texture for the primitive.
- * This method creates a texture from the splat attributes (positions, scales, rotations, colors)
- * and updates the primitive's state accordingly.
- *
- * @see {@link GaussianSplatTextureGenerator}
- *
- * @param {GaussianSplatPrimitive} primitive The owning primitive.
- * @param {FrameState} frameState The current frame state.
- * @param {GaussianSplatPrimitive.Snapshot} snapshot Snapshot being populated.
- * @private
- */
- GaussianSplatPrimitive.generateSplatTexture = function (
- primitive,
- frameState,
- snapshot,
- ) {
- if (!defined(snapshot) || snapshot.state !== SnapshotState.BUILDING) {
- return;
- }
- snapshot.state = SnapshotState.TEXTURE_PENDING;
- const promise = GaussianSplatTextureGenerator.generateFromAttributes({
- attributes: {
- positions: new Float32Array(snapshot.positions),
- scales: new Float32Array(snapshot.scales),
- rotations: new Float32Array(snapshot.rotations),
- colors: new Uint8Array(snapshot.colors),
- },
- count: snapshot.numSplats,
- });
- if (!defined(promise)) {
- snapshot.state = SnapshotState.BUILDING;
- return;
- }
- void processGeneratedSplatTextureData(
- primitive,
- frameState,
- snapshot,
- promise,
- );
- };
-
- /**
- * Builds the draw command for the Gaussian splat primitive.
- * This method sets up the shader program, render state, and vertex array for rendering the Gaussian splats.
- * It also configures the attributes and uniforms required for rendering.
- *
- * @param {GaussianSplatPrimitive} primitive
- * @param {FrameState} frameState
- *
- * @private
- */
- GaussianSplatPrimitive.buildGSplatDrawCommand = function (
- primitive,
- frameState,
- ) {
- const tileset = primitive._tileset;
- const renderResources = new GaussianSplatRenderResources(primitive);
- const { shaderBuilder } = renderResources;
- const renderStateOptions = renderResources.renderStateOptions;
- renderStateOptions.cull.enabled = false;
- renderStateOptions.depthMask = false;
- renderStateOptions.depthTest.enabled = true;
- renderStateOptions.blending = BlendingState.PRE_MULTIPLIED_ALPHA_BLEND;
- renderResources.alphaOptions.pass = Pass.GAUSSIAN_SPLATS;
-
- shaderBuilder.addAttribute("vec2", "a_screenQuadPosition");
- shaderBuilder.addAttribute("float", "a_splatIndex");
- shaderBuilder.addVarying("vec4", "v_splatColor");
- shaderBuilder.addVarying("vec2", "v_vertPos");
- shaderBuilder.addUniform(
- "float",
- "u_splitDirection",
- ShaderDestination.VERTEX,
- );
- shaderBuilder.addVarying("float", "v_splitDirection");
- shaderBuilder.addUniform(
- "highp usampler2D",
- "u_splatAttributeTexture",
- ShaderDestination.VERTEX,
- );
-
- shaderBuilder.addUniform(
- "float",
- "u_sphericalHarmonicsDegree",
- ShaderDestination.VERTEX,
- );
-
- shaderBuilder.addUniform("float", "u_splatScale", ShaderDestination.VERTEX);
-
- shaderBuilder.addUniform(
- "vec3",
- "u_cameraPositionWC",
- ShaderDestination.VERTEX,
- );
-
- shaderBuilder.addUniform(
- "mat3",
- "u_inverseModelRotation",
- ShaderDestination.VERTEX,
- );
-
- const uniformMap = renderResources.uniformMap;
-
- // Row-addressing uniforms: read from primitive each draw so they stay in sync
- // with the texture width chosen for the current snapshot.
- shaderBuilder.addUniform("int", "u_splatRowMask", ShaderDestination.VERTEX);
- shaderBuilder.addUniform("int", "u_splatRowShift", ShaderDestination.VERTEX);
-
- const textureCache = primitive.gaussianSplatTexture;
- uniformMap.u_splatAttributeTexture = function () {
- return textureCache;
- };
- uniformMap.u_splatRowMask = function () {
- return primitive._splatRowMask;
- };
- uniformMap.u_splatRowShift = function () {
- return primitive._splatRowShift;
- };
-
- if (primitive._sphericalHarmonicsDegree > 0) {
- shaderBuilder.addDefine(
- "HAS_SPHERICAL_HARMONICS",
- "1",
- ShaderDestination.VERTEX,
- );
- shaderBuilder.addUniform(
- "highp usampler2D",
- "u_sphericalHarmonicsTexture",
- ShaderDestination.VERTEX,
- );
- uniformMap.u_sphericalHarmonicsTexture = function () {
- return primitive.sphericalHarmonicsTexture;
- };
- }
- uniformMap.u_sphericalHarmonicsDegree = function () {
- return primitive._sphericalHarmonicsDegree;
- };
-
- uniformMap.u_cameraPositionWC = function () {
- return Cartesian3.clone(frameState.camera.positionWC);
- };
-
- uniformMap.u_inverseModelRotation = function () {
- // SH coefficients are encoded in the GLB Y-up training space. To evaluate
- // them the world-space view direction must be rotated by
- // inverse(computedTransform × axisCorrectionMatrix × worldTransform).
- // This matrix is pre-computed each snapshot rebuild and stored on the
- // primitive so the uniform closure just returns the cached value.
- return primitive._shInverseRotation;
- };
-
- uniformMap.u_splitDirection = function () {
- return primitive.splitDirection;
- };
-
- const instanceCount = defined(primitive._indexes)
- ? primitive._indexes.length
- : primitive._numSplats;
- renderResources.instanceCount = instanceCount;
- renderResources.count = 4;
- renderResources.primitiveType = PrimitiveType.TRIANGLE_STRIP;
- shaderBuilder.addVertexLines(GaussianSplatVS);
- shaderBuilder.addFragmentLines(GaussianSplatFS);
- const shaderProgram = shaderBuilder.buildShaderProgram(frameState.context);
- let renderState = clone(
- RenderState.fromCache(renderResources.renderStateOptions),
- true,
- );
-
- renderState.cull.face = ModelUtility.getCullFace(
- tileset.modelMatrix,
- PrimitiveType.TRIANGLE_STRIP,
- );
-
- renderState = RenderState.fromCache(renderState);
- const splatQuadAttrLocations = {
- screenQuadPosition: 0,
- splatIndex: 2,
- };
-
- const idxAttr = new ModelComponents.Attribute();
- idxAttr.name = "_SPLAT_INDEXES";
- idxAttr.typedArray = primitive._indexes;
- idxAttr.componentDatatype = ComponentDatatype.UNSIGNED_INT;
- idxAttr.type = AttributeType.SCALAR;
- idxAttr.normalized = false;
- idxAttr.count = renderResources.instanceCount;
- idxAttr.constant = 0;
- idxAttr.instanceDivisor = 1;
-
- const needsRebuild =
- !defined(primitive._vertexArray) ||
- primitive._indexes.length > primitive._vertexArrayLen;
- if (needsRebuild) {
- const geometry = new Geometry({
- attributes: {
- screenQuadPosition: new GeometryAttribute({
- componentDatatype: ComponentDatatype.FLOAT,
- componentsPerAttribute: 2,
- values: [-1, -1, 1, -1, 1, 1, -1, 1],
- name: "_SCREEN_QUAD_POS",
- variableName: "screenQuadPosition",
- }),
- splatIndex: { ...idxAttr, variableName: "splatIndex" },
- },
- primitiveType: PrimitiveType.TRIANGLE_STRIP,
- });
-
- primitive._vertexArray = VertexArray.fromGeometry({
- context: frameState.context,
- geometry: geometry,
- attributeLocations: splatQuadAttrLocations,
- bufferUsage: BufferUsage.DYNAMIC_DRAW,
- interleave: false,
- });
- } else {
- primitive._vertexArray
- .getAttribute(1)
- .vertexBuffer.copyFromArrayView(primitive._indexes);
- }
-
- primitive._vertexArrayLen = primitive._indexes.length;
-
- // The draw command uses rootTransform as its modelMatrix. tileset.modelMatrix
- // is baked into the splat positions by transformTile and must not appear here
- // as well. This keeps czm_view * modelMatrix numerically small (ENU frame),
- // avoiding float32 precision loss from ECEF-scale translations.
- const modelMatrix = Matrix4.clone(
- primitive._rootTransform,
- primitive._drawCommandModelMatrix,
- );
-
- const vertexArrayCache = primitive._vertexArray;
- const command = new DrawCommand({
- boundingVolume: tileset.boundingSphere,
- modelMatrix: modelMatrix,
- uniformMap: uniformMap,
- renderState: renderState,
- vertexArray: vertexArrayCache,
- shaderProgram: shaderProgram,
- cull: renderStateOptions.cull.enabled,
- pass: Pass.GAUSSIAN_SPLATS,
- count: renderResources.count,
- owner: primitive,
- instanceCount: renderResources.instanceCount,
- primitiveType: PrimitiveType.TRIANGLE_STRIP,
- debugShowBoundingVolume: tileset.debugShowBoundingVolume,
- castShadows: false,
- receiveShadows: false,
- });
-
- primitive._drawCommand = command;
- };
-
- /**
- * Updates the Gaussian splat primitive for the current frame.
- * This method checks if the primitive needs to be updated based on the current frame state,
- * and if so, it processes the selected tiles, aggregates their attributes,
- * and generates the Gaussian splat texture if necessary.
- * It also handles the sorting of splat indexes and builds the draw command for rendering.
- *
- * @param {FrameState} frameState
- * @private
- */
- GaussianSplatPrimitive.prototype.update = function (frameState) {
- const tileset = this._tileset;
-
- releaseRetiredTextures(this, frameState.frameNumber);
-
- if (!tileset.show) {
- return;
- }
-
- if (this._drawCommand) {
- frameState.commandList.push(this._drawCommand);
- }
-
- if (tileset._modelMatrixChanged) {
- this._dirty = true;
- return;
- }
- const hasRootTransform = defined(this._rootTransform);
-
- if (frameState.passes.pick === true) {
- return;
- }
-
- if (this.splitDirection !== tileset.splitDirection) {
- this.splitDirection = tileset.splitDirection;
- }
- const camera = frameState.camera;
- if (!defined(camera)) {
- return;
- }
-
- if (this._sorterState === GaussianSplatSortingState.IDLE) {
- const selectedTilesChanged =
- tileset._selectedTiles.length !== 0 &&
- haveSelectedTilesChanged(this, tileset._selectedTiles);
- if (tileset._selectedTiles.length === 0) {
- this._selectedTilesStableFrames = 0;
- this._needsSnapshotRebuild = false;
- this._snapshotRebuildStallFrames = 0;
- } else if (selectedTilesChanged) {
- this._selectedTilesStableFrames = 0;
- } else {
- this._selectedTilesStableFrames++;
- }
- if (selectedTilesChanged || this._dirty) {
- this._needsSnapshotRebuild = true;
- }
- const isStable = this._selectedTilesStableFrames >= DEFAULT_STABLE_FRAMES;
- const isBootstrap =
- !defined(this._snapshot) &&
- !defined(this._pendingSnapshot) &&
- !defined(this._drawCommand);
- // This prevents an indefinite wait if selected tiles never settle completely.
- // In practice, this is the upper bound on "wait-for-stability" before forcing
- // a rebuild to avoid visible starvation.
- if (this._needsSnapshotRebuild && tileset._selectedTiles.length !== 0) {
- this._snapshotRebuildStallFrames++;
- } else {
- this._snapshotRebuildStallFrames = 0;
- }
- const allowRebuild =
- isStable ||
- isBootstrap ||
- this._snapshotRebuildStallFrames >= DEFAULT_MAX_SNAPSHOT_STALL_FRAMES;
- const hasPendingWork =
- this._dirty ||
- this._needsSnapshotRebuild ||
- selectedTilesChanged ||
- defined(this._pendingSnapshot) ||
- defined(this._pendingSortPromise) ||
- !defined(this._drawCommand);
- if (
- !hasPendingWork &&
- Matrix4.equals(camera.viewMatrix, this._prevViewMatrix)
- ) {
- return;
- }
-
- if (
- tileset._selectedTiles.length !== 0 &&
- this._needsSnapshotRebuild &&
- allowRebuild
- ) {
- this._splatDataGeneration++;
- this._activeSort = undefined;
- this._sorterPromise = undefined;
- this._sorterState = GaussianSplatSortingState.IDLE;
- this._pendingSortPromise = undefined;
- this._pendingSort = undefined;
- if (defined(this._pendingSnapshot)) {
- destroySnapshotTextures(this._pendingSnapshot);
- }
-
- const tiles = tileset._selectedTiles;
-
- // Rebuild the ENU origin from the current tileset world center so that
- // baked splat positions remain in a numerically small (meter-scale) local
- // frame, regardless of the current tileset.modelMatrix value.
- this._rootTransform = Transforms.eastNorthUpToFixedFrame(
- tileset.boundingSphere.center,
- );
-
- // Compute the SH inverse rotation from the first tile's coordinate frame.
- // SH coefficients are encoded in the GLB Y-up training space. To evaluate
- // them correctly the view direction must be transformed by
- // inverse(computedTransform × axisCorrectionMatrix × worldTransform).
- // All tiles in a typical GS tileset share the same root coordinate frame,
- // so using the first tile is sufficient.
- {
- const ft = tiles[0];
- const shFwd = Matrix4.multiplyTransformation(
- ft.computedTransform,
- this._axisCorrectionMatrix,
- scratchMatrix4C,
- );
- Matrix4.multiplyTransformation(
- shFwd,
- ft.content.worldTransform ?? Matrix4.IDENTITY,
- shFwd,
- );
- Matrix4.getRotation(
- Matrix4.inverse(shFwd, shFwd),
- this._shInverseRotation,
- );
- }
-
- for (const tile of tiles) {
- GaussianSplatPrimitive.transformTile(tile);
- }
-
- const totalElements = tiles.reduce(
- (total, tile) => total + tile.content.pointsLength,
- 0,
- );
- const aggregateAttributeValues = (
- key,
- componentDatatype,
- getAttributeCallback,
- numberOfComponents,
- ) => {
- let aggregate;
- let offset = 0;
- let requiredLength = 0;
- for (const tile of tiles) {
- const attribute = getAttributeCallback(tile.content);
- const componentsPerAttribute = defined(numberOfComponents)
- ? numberOfComponents
- : AttributeType.getNumberOfComponents(attribute.type);
- const buffer = defined(attribute.typedArray)
- ? attribute.typedArray
- : attribute;
- requiredLength += buffer.length;
- if (!defined(aggregate)) {
- aggregate = acquireAggregateScratchBuffer(
- this,
- key,
- componentDatatype,
- totalElements * componentsPerAttribute,
- );
- }
- }
-
- if (!defined(aggregate)) {
- return ComponentDatatype.createTypedArray(componentDatatype, 0);
- }
-
- for (const tile of tiles) {
- const content = tile.content;
- const attribute = getAttributeCallback(content);
- const buffer = defined(attribute.typedArray)
- ? attribute.typedArray
- : attribute;
- aggregate.set(buffer, offset);
- offset += buffer.length;
- }
- return trimAggregateScratchBuffer(aggregate, requiredLength);
- };
-
- const aggregateShData = () => {
- // Determine the SH degree from the first tile with SH data so we can
- // pre-allocate the aggregate buffer once, outside the tile loop.
- let coefs = 0;
- for (const tile of tiles) {
- if (tile.content.sphericalHarmonicsDegree > 0) {
- switch (tile.content.sphericalHarmonicsDegree) {
- case 1:
- coefs = 9;
- break;
- case 2:
- coefs = 24;
- break;
- case 3:
- coefs = 45;
- break;
- }
- break;
- }
- }
-
- if (coefs === 0) {
- return undefined;
- }
-
- const requiredLength = totalElements * (coefs * (2 / 3));
-
- // Re-use the class-level scratch buffer when it is already large
- // enough, avoiding a fresh allocation (and eventual GC) every frame.
- if (
- !defined(this._scratchAggregateShBuffer) ||
- this._scratchAggregateShBuffer.length < requiredLength
- ) {
- this._scratchAggregateShBuffer = new Uint32Array(requiredLength);
- }
- const aggregate = this._scratchAggregateShBuffer;
-
- let offset = 0;
- for (const tile of tiles) {
- const tileShData = tile.content.packedSphericalHarmonicsData;
- if (tile.content.sphericalHarmonicsDegree > 0) {
- aggregate.set(tileShData, offset);
- offset += tileShData.length;
- }
- }
-
- // Return a correctly-sized view so downstream consumers see the
- // exact element count they expect.
- if (offset < aggregate.length) {
- return aggregate.subarray(0, offset);
- }
- return aggregate;
- };
-
- const positions = aggregateAttributeValues(
- "positions",
- ComponentDatatype.FLOAT,
- (content) => content.positions,
- 3,
- );
-
- const scales = aggregateAttributeValues(
- "scales",
- ComponentDatatype.FLOAT,
- (content) => content.scales,
- 3,
- );
-
- const rotations = aggregateAttributeValues(
- "rotations",
- ComponentDatatype.FLOAT,
- (content) => content.rotations,
- 4,
- );
-
- const colors = aggregateAttributeValues(
- "colors",
- ComponentDatatype.UNSIGNED_BYTE,
- (content) =>
- ModelUtility.getAttributeBySemantic(
- content.gltfPrimitive,
- VertexAttributeSemantic.COLOR,
- ),
- );
-
- const sphericalHarmonicsDegree =
- tiles[0].content.sphericalHarmonicsDegree;
- const shCoefficientCount =
- sphericalHarmonicsDegree > 0
- ? tiles[0].content.sphericalHarmonicsCoefficientCount
- : 0;
- const shData = aggregateShData();
-
- this._pendingSnapshot = {
- generation: this._splatDataGeneration,
- positions: positions,
- rotations: rotations,
- scales: scales,
- colors: colors,
- shData: shData,
- sphericalHarmonicsDegree: sphericalHarmonicsDegree,
- shCoefficientCount: shCoefficientCount,
- numSplats: totalElements,
- indexes: undefined,
- gaussianSplatTexture: undefined,
- sphericalHarmonicsTexture: undefined,
- lastTextureWidth: 0,
- lastTextureHeight: 0,
- splatRowMask: 0, // set by processGeneratedSplatTextureData
- splatRowShift: 0, // set by processGeneratedSplatTextureData
- state: SnapshotState.BUILDING,
- };
-
- this.selectedTileLength = tileset._selectedTiles.length;
- this._selectedTileSet = new Set(tileset._selectedTiles);
- this._dirty = false;
- this._needsSnapshotRebuild = false;
- this._snapshotRebuildStallFrames = 0;
- }
-
- if (defined(this._pendingSnapshot)) {
- const pending = this._pendingSnapshot;
- if (pending.state === SnapshotState.BUILDING) {
- GaussianSplatPrimitive.generateSplatTexture(this, frameState, pending);
- return;
- }
- if (pending.state === SnapshotState.TEXTURE_PENDING) {
- return;
- }
- if (
- pending.state === SnapshotState.TEXTURE_READY &&
- !defined(pending.gaussianSplatTexture)
- ) {
- return;
- }
-
- if (!hasRootTransform) {
- return;
- }
-
- Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
- Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
-
- if (
- pending.state === SnapshotState.TEXTURE_READY &&
- !defined(this._pendingSortPromise)
- ) {
- const requestId = ++this._sortRequestId;
- const dataGeneration = this._splatDataGeneration;
- this._pendingSort = {
- requestId: requestId,
- dataGeneration: dataGeneration,
- expectedCount: pending.numSplats,
- snapshot: pending,
- };
- const sortPromise = GaussianSplatSorter.radixSortIndexes({
- primitive: {
- positions: new Float32Array(pending.positions),
- modelView: Float32Array.from(scratchMatrix4A),
- count: pending.numSplats,
- },
- sortType: "Index",
- });
- if (!defined(sortPromise)) {
- this._pendingSortPromise = undefined;
- this._pendingSort = undefined;
- pending.state = SnapshotState.TEXTURE_READY;
- return;
- }
- this._pendingSortPromise = sortPromise;
- pending.state = SnapshotState.SORTING;
- const pendingSort = this._pendingSort;
- void resolvePendingSnapshotSort(
- this,
- frameState,
- pendingSort,
- sortPromise,
- );
- return;
- }
-
- if (!defined(this._pendingSortPromise)) {
- if (pending.state === SnapshotState.SORTING) {
- pending.state = SnapshotState.TEXTURE_READY;
- }
- return;
- }
- return;
- }
-
- if (this._numSplats === 0) {
- return;
- }
-
- if (!hasRootTransform) {
- return;
- }
-
- Matrix4.clone(camera.viewMatrix, this._prevViewMatrix);
- Matrix4.multiply(camera.viewMatrix, this._rootTransform, scratchMatrix4A);
-
- if (!defined(this._sorterPromise)) {
- if (!shouldStartSteadySort(this, frameState)) {
- return;
- }
- const requestId = ++this._sortRequestId;
- const dataGeneration = this._splatDataGeneration;
- const expectedCount = this._numSplats;
- this._activeSort = {
- requestId: requestId,
- dataGeneration: dataGeneration,
- expectedCount: expectedCount,
- };
- const rawPromise = GaussianSplatSorter.radixSortIndexes({
- primitive: {
- positions: new Float32Array(this._positions),
- modelView: Float32Array.from(scratchMatrix4A),
- count: this._numSplats,
- },
- sortType: "Index",
- });
- this._sorterPromise = rawPromise;
- if (defined(rawPromise)) {
- markSteadySortStart(this, frameState);
- const activeSort = this._activeSort;
- this._sorterState = GaussianSplatSortingState.SORTING;
- void resolveSteadySort(this, activeSort, rawPromise);
- return;
- }
- }
-
- if (!defined(this._sorterPromise)) {
- this._sorterState = GaussianSplatSortingState.WAITING;
- return;
- }
- this._sorterState = GaussianSplatSortingState.SORTING;
- return;
- } else if (this._sorterState === GaussianSplatSortingState.WAITING) {
- if (!defined(this._sorterPromise)) {
- const requestId = ++this._sortRequestId;
- const dataGeneration = this._splatDataGeneration;
- const expectedCount = this._numSplats;
- this._activeSort = {
- requestId: requestId,
- dataGeneration: dataGeneration,
- expectedCount: expectedCount,
- };
- const rawPromise = GaussianSplatSorter.radixSortIndexes({
- primitive: {
- positions: new Float32Array(this._positions),
- modelView: Float32Array.from(scratchMatrix4A),
- count: this._numSplats,
- },
- sortType: "Index",
- });
- this._sorterPromise = rawPromise;
- if (defined(rawPromise)) {
- markSteadySortStart(this, frameState);
- const activeSort = this._activeSort;
- this._sorterState = GaussianSplatSortingState.SORTING;
- void resolveSteadySort(this, activeSort, rawPromise);
- return;
- }
- }
- if (!defined(this._sorterPromise)) {
- this._sorterState = GaussianSplatSortingState.WAITING;
- return;
- }
- this._sorterState = GaussianSplatSortingState.SORTING;
- return;
- } else if (this._sorterState === GaussianSplatSortingState.SORTING) {
- return; //still sorting, wait for next frame
- } else if (this._sorterState === GaussianSplatSortingState.SORTED) {
- //update the draw command if sorted
- GaussianSplatPrimitive.buildGSplatDrawCommand(this, frameState);
- this._sorterState = GaussianSplatSortingState.IDLE; //reset state for next frame
- this._dirty = false;
- this._sorterPromise = undefined; //reset promise for next frame
- this._activeSort = undefined;
- } else if (this._sorterState === GaussianSplatSortingState.ERROR) {
- throw this._sorterError;
- }
-
- this._dirty = false;
- };
-
- export default GaussianSplatPrimitive;
|