@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,063 lines (958 loc) • 32.7 kB
JavaScript
import Frozen from "../Core/Frozen.js";
import Matrix4 from "../Core/Matrix4.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 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";
const scratchMatrix4A = new Matrix4();
const scratchMatrix4B = new Matrix4();
const scratchMatrix4C = new Matrix4();
const scratchMatrix4D = new Matrix4();
const GaussianSplatSortingState = {
IDLE: 0,
WAITING: 1,
SORTING: 2,
SORTED: 3,
ERROR: 4,
};
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;
}
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;
/**
* 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;
/**
* 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;
/**
* 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(),
);
/**
* 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;
/**
* An error that occurred during the Gaussian splat sorting operation.
* Thrown when state is ERROR.
* @type {undefined|Error}
* @private
*/
this._sorterError = undefined;
}
Object.defineProperties(GaussianSplatPrimitive.prototype, {
/**
* Indicates whether the primitive is ready for use.
* @memberof GaussianSplatPrimitive.prototype
* @type {boolean}
* @readonly
*/
ready: {
get: function () {
return this._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;
}
},
},
});
/**
* Since we aren't visible at the scene level, we need to wrap the tileset update
* so we not only get called but ensure we update immediately after the tileset.
* @param {FrameState} frameState
* @private
*
*/
GaussianSplatPrimitive.prototype._wrappedUpdate = function (frameState) {
this._baseTilesetUpdate.call(this._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;
if (defined(this.gaussianSplatTexture)) {
this.gaussianSplatTexture.destroy();
this.gaussianSplatTexture = 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;
const computedModelMatrix = Matrix4.multiplyTransformation(
computedTransform,
gaussianSplatPrimitive._axisCorrectionMatrix,
scratchMatrix4A,
);
Matrix4.multiplyTransformation(
computedModelMatrix,
tile.content.worldTransform,
computedModelMatrix,
);
const toGlobal = Matrix4.multiply(
tile.tileset.modelMatrix,
Matrix4.fromArray(tile.tileset.root.transform),
scratchMatrix4B,
);
const toLocal = Matrix4.inverse(toGlobal, scratchMatrix4C);
const transform = Matrix4.multiplyTransformation(
toLocal,
computedModelMatrix,
scratchMatrix4A,
);
const positions = tile.content._originalPositions;
const rotations = tile.content._originalRotations;
const scales = tile.content._originalScales;
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 = new Cartesian3();
const rotation = new Quaternion();
const scale = new Cartesian3();
for (let i = 0; i < positions.length / 3; ++i) {
position.x = positions[i * 3];
position.y = positions[i * 3 + 1];
position.z = positions[i * 3 + 2];
rotation.x = rotations[i * 4];
rotation.y = rotations[i * 4 + 1];
rotation.z = rotations[i * 4 + 2];
rotation.w = rotations[i * 4 + 3];
scale.x = scales[i * 3];
scale.y = scales[i * 3 + 1];
scale.z = scales[i * 3 + 2];
Matrix4.fromTranslationQuaternionRotationScale(
position,
rotation,
scale,
scratchMatrix4C,
);
Matrix4.multiplyTransformation(transform, scratchMatrix4C, scratchMatrix4C);
Matrix4.getTranslation(scratchMatrix4C, position);
Matrix4.getRotation(scratchMatrix4C, rotation);
Matrix4.getScale(scratchMatrix4C, scale);
attributePositions[i * 3] = position.x;
attributePositions[i * 3 + 1] = position.y;
attributePositions[i * 3 + 2] = position.z;
attributeRotations[i * 4] = rotation.x;
attributeRotations[i * 4 + 1] = rotation.y;
attributeRotations[i * 4 + 2] = rotation.z;
attributeRotations[i * 4 + 3] = rotation.w;
attributeScales[i * 3] = scale.x;
attributeScales[i * 3 + 1] = scale.y;
attributeScales[i * 3 + 2] = scale.z;
}
};
/**
* 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
* @param {FrameState} frameState
* @private
*/
GaussianSplatPrimitive.generateSplatTexture = function (primitive, frameState) {
primitive._gaussianSplatTexturePending = true;
const promise = GaussianSplatTextureGenerator.generateFromAttributes({
attributes: {
positions: new Float32Array(primitive._positions),
scales: new Float32Array(primitive._scales),
rotations: new Float32Array(primitive._rotations),
colors: new Uint8Array(primitive._colors),
},
count: primitive._numSplats,
});
if (!defined(promise)) {
primitive._gaussianSplatTexturePending = false;
return;
}
promise
.then((splatTextureData) => {
if (!primitive._gaussianSplatTexture) {
// First frame, so create the texture.
primitive.gaussianSplatTexture = createGaussianSplatTexture(
frameState.context,
splatTextureData,
);
} else if (
primitive._lastTextureHeight !== splatTextureData.height ||
primitive._lastTextureWidth !== splatTextureData.width
) {
const oldTex = primitive.gaussianSplatTexture;
primitive._gaussianSplatTexture = createGaussianSplatTexture(
frameState.context,
splatTextureData,
);
oldTex.destroy();
} else {
primitive.gaussianSplatTexture.copyFrom({
source: {
width: splatTextureData.width,
height: splatTextureData.height,
arrayBufferView: splatTextureData.data,
},
});
}
primitive._lastTextureHeight = splatTextureData.height;
primitive._lastTextureWidth = splatTextureData.width;
primitive._hasGaussianSplatTexture = true;
primitive._needsGaussianSplatTexture = false;
primitive._gaussianSplatTexturePending = false;
if (
!defined(primitive._indexes) ||
primitive._indexes.length < primitive._numSplats
) {
primitive._indexes = new Uint32Array(primitive._numSplats);
}
for (let i = 0; i < primitive._numSplats; ++i) {
primitive._indexes[i] = i;
}
})
.catch((error) => {
console.error("Error generating Gaussian splat texture:", error);
primitive._gaussianSplatTexturePending = false;
});
};
/**
* 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;
uniformMap.u_splatAttributeTexture = function () {
return primitive.gaussianSplatTexture;
};
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 () {
const tileset = primitive._tileset;
const modelMatrix = Matrix4.multiply(
tileset.modelMatrix,
Matrix4.fromArray(tileset.root.transform),
scratchMatrix4A,
);
const inverseModelRotation = Matrix4.getRotation(
Matrix4.inverse(modelMatrix, scratchMatrix4C),
scratchMatrix4D,
);
return inverseModelRotation;
};
uniformMap.u_splitDirection = function () {
return primitive.splitDirection;
};
renderResources.instanceCount = primitive._numSplats;
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;
if (
!defined(primitive._vertexArray) ||
primitive._indexes.length > primitive._vertexArrayLen
) {
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;
const modelMatrix = Matrix4.multiply(
tileset.modelMatrix,
Matrix4.fromArray(tileset.root.transform),
scratchMatrix4B,
);
const command = new DrawCommand({
boundingVolume: tileset.boundingSphere,
modelMatrix: modelMatrix,
uniformMap: uniformMap,
renderState: renderState,
vertexArray: primitive._vertexArray,
shaderProgram: shaderProgram,
cull: renderStateOptions.cull.enabled,
pass: Pass.GAUSSIAN_SPLATS,
count: renderResources.count,
owner: this,
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;
if (!defined(this._rootTransform)) {
this._rootTransform = tileset.root.computedTransform;
}
if (!tileset.show || tileset._selectedTiles.length === 0) {
return;
}
if (this._drawCommand) {
frameState.commandList.push(this._drawCommand);
}
if (tileset._modelMatrixChanged) {
this._dirty = true;
return;
}
if (frameState.passes.pick === true) {
return;
}
if (this.splitDirection !== tileset.splitDirection) {
this.splitDirection = tileset.splitDirection;
}
if (this._sorterState === GaussianSplatSortingState.IDLE) {
if (
!this._dirty &&
Matrix4.equals(frameState.camera.viewMatrix, this._prevViewMatrix)
) {
// No need to update if the view matrix hasn't changed and the primitive isn't dirty.
return;
}
if (
tileset._selectedTiles.length !== 0 &&
tileset._selectedTiles.length !== this.selectedTileLength
) {
this._numSplats = 0;
this._positions = undefined;
this._rotations = undefined;
this._scales = undefined;
this._colors = undefined;
this._indexes = undefined;
this._shData = undefined;
this._needsGaussianSplatTexture = true;
this._gaussianSplatTexturePending = false;
const tiles = tileset._selectedTiles;
const totalElements = tiles.reduce(
(total, tile) => total + tile.content.pointsLength,
0,
);
const aggregateAttributeValues = (
componentDatatype,
getAttributeCallback,
) => {
let aggregate;
let offset = 0;
for (const tile of tiles) {
const primitive = tile.content.gltfPrimitive;
const attribute = getAttributeCallback(primitive);
if (!defined(aggregate)) {
aggregate = ComponentDatatype.createTypedArray(
componentDatatype,
totalElements *
AttributeType.getNumberOfComponents(attribute.type),
);
}
aggregate.set(attribute.typedArray, offset);
offset += attribute.typedArray.length;
}
return aggregate;
};
const aggregateShData = () => {
let offset = 0;
for (const tile of tiles) {
const shData = tile.content.packedSphericalHarmonicsData;
if (tile.content.sphericalHarmonicsDegree > 0) {
if (!defined(this._shData)) {
let coefs;
switch (tile.content.sphericalHarmonicsDegree) {
case 1:
coefs = 9;
break;
case 2:
coefs = 24;
break;
case 3:
coefs = 45;
}
this._shData = new Uint32Array(totalElements * (coefs * (2 / 3)));
}
this._shData.set(shData, offset);
offset += shData.length;
}
}
};
this._positions = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.POSITION,
),
);
this._scales = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.SCALE,
),
);
this._rotations = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
),
);
this._colors = aggregateAttributeValues(
ComponentDatatype.UNSIGNED_BYTE,
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
gltfPrimitive,
VertexAttributeSemantic.COLOR,
),
);
aggregateShData();
this._sphericalHarmonicsDegree =
tiles[0].content.sphericalHarmonicsDegree;
this._numSplats = totalElements;
this.selectedTileLength = tileset._selectedTiles.length;
}
if (this._numSplats === 0) {
return;
}
if (this._needsGaussianSplatTexture) {
if (!this._gaussianSplatTexturePending) {
GaussianSplatPrimitive.generateSplatTexture(this, frameState);
if (defined(this._shData)) {
const oldTex = this.sphericalHarmonicsTexture;
const width = ContextLimits.maximumTextureSize;
const dims =
tileset._selectedTiles[0].content
.sphericalHarmonicsCoefficientCount / 3;
const splatsPerRow = Math.floor(width / dims);
const floatsPerRow = splatsPerRow * (dims * 2);
const texBuf = new Uint32Array(
width * Math.ceil(this._numSplats / splatsPerRow) * 2,
);
let dataIndex = 0;
for (let i = 0; dataIndex < this._shData.length; i += width * 2) {
texBuf.set(
this._shData.subarray(dataIndex, dataIndex + floatsPerRow),
i,
);
dataIndex += floatsPerRow;
}
this.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
frameState.context,
{
data: texBuf,
width: width,
height: Math.ceil(this._numSplats / splatsPerRow),
},
);
if (defined(oldTex)) {
oldTex.destroy();
}
}
}
return;
}
Matrix4.clone(frameState.camera.viewMatrix, this._prevViewMatrix);
Matrix4.multiply(
frameState.camera.viewMatrix,
this._rootTransform,
scratchMatrix4A,
);
if (!defined(this._sorterPromise)) {
this._sorterPromise = GaussianSplatSorter.radixSortIndexes({
primitive: {
positions: new Float32Array(this._positions),
modelView: Float32Array.from(scratchMatrix4A),
count: this._numSplats,
},
sortType: "Index",
});
}
if (!defined(this._sorterPromise)) {
this._sorterState = GaussianSplatSortingState.WAITING;
return;
}
this._sorterPromise.catch((err) => {
this._sorterState = GaussianSplatSortingState.ERROR;
this._sorterError = err;
});
this._sorterPromise.then((sortedData) => {
this._indexes = sortedData;
this._sorterState = GaussianSplatSortingState.SORTED;
});
} else if (this._sorterState === GaussianSplatSortingState.WAITING) {
if (!defined(this._sorterPromise)) {
this._sorterPromise = GaussianSplatSorter.radixSortIndexes({
primitive: {
positions: new Float32Array(this._positions),
modelView: Float32Array.from(scratchMatrix4A),
count: this._numSplats,
},
sortType: "Index",
});
}
if (!defined(this._sorterPromise)) {
this._sorterState = GaussianSplatSortingState.WAITING;
return;
}
this._sorterPromise.catch((err) => {
this._sorterState = GaussianSplatSortingState.ERROR;
this._sorterError = err;
});
this._sorterPromise.then((sortedData) => {
this._indexes = sortedData;
this._sorterState = GaussianSplatSortingState.SORTED;
});
this._sorterState = GaussianSplatSortingState.SORTING; //set state to sorting
} 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
} else if (this._sorterState === GaussianSplatSortingState.ERROR) {
throw this._sorterError;
}
this._dirty = false;
};
export default GaussianSplatPrimitive;