2148 lines
71 KiB
JavaScript
2148 lines
71 KiB
JavaScript
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;
|