@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
827 lines (759 loc) • 24 kB
JavaScript
import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
import GltfLoader from "./GltfLoader.js";
import RuntimeError from "../Core/RuntimeError.js";
import Axis from "./Axis.js";
import GaussianSplatPrimitive from "./GaussianSplatPrimitive.js";
import destroyObject from "../Core/destroyObject.js";
import ModelUtility from "./Model/ModelUtility.js";
import VertexAttributeSemantic from "./VertexAttributeSemantic.js";
import deprecationWarning from "../Core/deprecationWarning.js";
/**
* Represents the contents of a glTF or glb using the {@link https://github.com/CesiumGS/glTF/tree/draft-splat-spz/extensions/2.0/Khronos/KHR_gaussian_splatting | KHR_gaussian_splatting} and {@link https://github.com/CesiumGS/glTF/tree/draft-splat-spz/extensions/2.0/Khronos/KHR_gaussian_splatting_compression_spz_2 | KHR_gaussian_splatting_compression_spz_2} extensions.
* <p>
* Implements the {@link Cesium3DTileContent} interface.
* </p>
*
* @alias GaussianSplat3DTileContent
* @constructor
*/
function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
this._tileset = tileset;
this._tile = tile;
this._resource = resource;
this._loader = loader;
if (!defined(this._tileset.gaussianSplatPrimitive)) {
this._tileset.gaussianSplatPrimitive = new GaussianSplatPrimitive({
tileset: this._tileset,
});
}
/**
* Local copy of the position attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._positions = undefined;
/**
* Local copy of the rotation attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._rotations = undefined;
/**
* Local copy of the scale attribute buffer that has been transformed into root tile space. Originals are kept in the gltf loader.
* Used for rendering
* @type {undefined|Float32Array}
* @private
*/
this._scales = undefined;
/**
* glTF primitive data that contains the Gaussian splat data needed for rendering.
* @type {undefined|Primitive}
* @private
*/
this.gltfPrimitive = undefined;
/**
* Transform matrix to turn model coordinates into world coordinates.
* @type {undefined|Matrix4}
* @private
*/
this.worldTransform = undefined;
/**
* Gets or sets if any feature's property changed. Used to
* optimized applying a style when a feature's property changed.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {boolean}
*
* @private
*/
this.featurePropertiesDirty = false;
this._metadata = undefined;
this._group = undefined;
this._ready = false;
/**
* Indicates whether or not the local coordinates of the tile have been transformed
* @type {boolean}
* @private
*/
this._transformed = false;
/**
* The degree of the spherical harmonics used for the Gaussian splats.
* @type {number}
* @private
*/
this._sphericalHarmonicsDegree = 0;
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
* @private
*/
this._sphericalHarmonicsCoefficientCount = 0;
/**
* Spherical Harmonic data that has been packed for use in a texture or shader.
* @type {undefined|Uint32Array}
* @private
*/
this._packedSphericalHarmonicsData = undefined;
}
/**
* Performs checks to ensure that the provided tileset has the Gaussian Splatting extensions.
*
* @param {Cesium3DTileset} tileset The tileset to check for the extensions.
* @returns {boolean} Returns <code>true</code> if the necessary extensions are included in the tileset.
* @static
*/
GaussianSplat3DTileContent.tilesetRequiresGaussianSplattingExt = function (
tileset,
) {
let hasGaussianSplatExtension = false;
if (tileset.isGltfExtensionRequired instanceof Function) {
hasGaussianSplatExtension =
tileset.isGltfExtensionRequired("KHR_gaussian_splatting") &&
tileset.isGltfExtensionRequired(
"KHR_gaussian_splatting_compression_spz_2",
);
if (
tileset.isGltfExtensionRequired("KHR_spz_gaussian_splats_compression")
) {
deprecationWarning(
"KHR_spz_gaussian_splats_compression",
"Support for the original KHR_spz_gaussian_splats_compression extension has been removed in favor " +
"of the up to date KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extensions" +
"\n\nPlease retile your tileset with the KHR_gaussian_splatting and " +
"KHR_gaussian_splatting_compression_spz_2 extensions.",
);
}
}
return hasGaussianSplatExtension;
};
Object.defineProperties(GaussianSplat3DTileContent.prototype, {
/**
* Gets the number of features in the tile. Currently this is always zero.
*
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {number}
* @readonly
*/
featuresLength: {
get: function () {
return 0;
},
},
/**
* Equal to the number of Gaussian splats in the tile. Each splat is represented by a median point and a set of attributes, so we can
* treat this as the number of points in the tile.
*
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {number}
* @readonly
*/
pointsLength: {
get: function () {
return this.gltfPrimitive.attributes[0].count;
},
},
/**
* Gets the number of triangles in the tile. Currently this is always zero because Gaussian splats are not represented as triangles in the tile content.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {number}
* @readonly
*/
trianglesLength: {
get: function () {
return 0;
},
},
/**
* The number of bytes used by the geometry attributes of this content.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {number}
* @readonly
*/
geometryByteLength: {
get: function () {
return this.gltfPrimitive.attributes.reduce((totalLength, attribute) => {
return totalLength + attribute.byteLength;
}, 0);
},
},
/**
* The number of bytes used by the textures of this content.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {number}
* @readonly
*/
texturesByteLength: {
get: function () {
const texture = this._tileset.gaussianSplatPrimitive.gaussianSplatTexture;
const selectedTileLength =
this._tileset.gaussianSplatPrimitive.selectedTileLength;
if (!defined(texture) || selectedTileLength === 0) {
return 0;
}
return texture.sizeInBytes / selectedTileLength;
},
},
/**
* Gets the amount of memory used by the batch table textures and any binary
* metadata properties not accounted for in geometryByteLength or
* texturesByteLength
* <p>
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {number}
* @readonly
*/
batchTableByteLength: {
get: function () {
return 0;
},
},
/**
* Gets the array of {@link Cesium3DTileContent} objects for contents that contain other contents, such as composite tiles. The inner contents may in turn have inner contents, such as a composite tile that contains a composite tile.
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/Composite|Composite specification}
*
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {Array}
* @readonly
*/
innerContents: {
get: function () {
return undefined;
},
},
/**
* Returns true when the tile's content is ready to render; otherwise false
*
* @memberof GaussianSplat3DTileContent.prototype
*
* @type {boolean}
* @readonly
*/
ready: {
get: function () {
return this._ready;
},
},
/**
* Returns true when the tile's content is transformed to world coordinates; otherwise false
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {boolean}
* @readonly
*/
transformed: {
get: function () {
return this._transformed;
},
},
/**
* The tileset that this content belongs to.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {Cesium3DTileset}
* @readonly
*/
tileset: {
get: function () {
return this._tileset;
},
},
/**
* The tile that this content belongs to.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {Cesium3DTile}
* @readonly
*/
tile: {
get: function () {
return this._tile;
},
},
/**
* The resource that this content was loaded from.
* <p>
* @memberof GaussianSplat3DTileContent.prototype
* @type {Resource}
* @readonly
*/
url: {
get: function () {
return this._resource.getUrlComponent(true);
},
},
/**
* Gets the batch table for this content.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DTileBatchTable}
* @readonly
*
* @private
*/
batchTable: {
get: function () {
return undefined;
},
},
/**
* Gets the metadata for this content, whether it is available explicitly or via
* implicit tiling. If there is no metadata, this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {ImplicitMetadataView|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
metadata: {
get: function () {
return this._metadata;
},
set: function (value) {
this._metadata = value;
},
},
/**
* Gets the group for this content if the content has metadata (3D Tiles 1.1) or
* if it uses the <code>3DTILES_metadata</code> extension. If neither are present,
* this property should be undefined.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @type {Cesium3DTileContentGroup|undefined}
*
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
group: {
get: function () {
return this._group;
},
set: function (value) {
this._group = value;
},
},
/**
* Get the transformed positions of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
positions: {
get: function () {
return this._positions;
},
},
/**
* Get the transformed rotations of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
rotations: {
get: function () {
return this._rotations;
},
},
/**
* Get the transformed scales of this tile's Gaussian splats.
* @type {undefined|Float32Array}
* @private
*/
scales: {
get: function () {
return this._scales;
},
},
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
sphericalHarmonicsCoefficientCount: {
get: function () {
return this._sphericalHarmonicsCoefficientCount;
},
},
/**
* The degree of the spherical harmonics used for the Gaussian splats.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
sphericalHarmonicsDegree: {
get: function () {
return this._sphericalHarmonicsDegree;
},
},
/**
* The packed spherical harmonic data for the Gaussian splats for use a shader or texture.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
packedSphericalHarmonicsData: {
get: function () {
return this._packedSphericalHarmonicsData;
},
},
});
function getShAttributePrefix(attribute) {
const prefix = attribute.startsWith("KHR_gaussian_splatting:")
? "KHR_gaussian_splatting:"
: "_";
return `${prefix}SH_DEGREE_`;
}
/**
* Determine Spherical Harmonics degree and coefficient count from attributes
* @param {Attribute[]} attributes - The list of glTF attributes.
* @returns {object} An object containing the degree (l) and coefficient (n).
* @private
*/
function degreeAndCoefFromAttributes(attributes) {
const shAttributes = attributes.filter((attr) =>
attr.name.includes("SH_DEGREE_"),
);
switch (shAttributes.length) {
default:
case 0:
return { l: 0, n: 0 };
case 3:
return { l: 1, n: 9 };
case 8:
return { l: 2, n: 24 };
case 15:
return { l: 3, n: 45 };
}
}
/**
* Converts a 32-bit floating point number to a 16-bit floating point number.
* @param {number} float32 input
* @returns {number} Half precision float
* @private
*/
const buffer = new ArrayBuffer(4);
const floatView = new Float32Array(buffer);
const intView = new Uint32Array(buffer);
function float32ToFloat16(float32) {
floatView[0] = float32;
const bits = intView[0];
const sign = (bits >> 31) & 0x1;
const exponent = (bits >> 23) & 0xff;
const mantissa = bits & 0x7fffff;
let half;
if (exponent === 0xff) {
half = (sign << 15) | (0x1f << 10) | (mantissa ? 0x200 : 0);
} else if (exponent === 0) {
half = sign << 15;
} else {
const newExponent = exponent - 127 + 15;
if (newExponent >= 31) {
half = (sign << 15) | (0x1f << 10);
} else if (newExponent <= 0) {
half = sign << 15;
} else {
half = (sign << 15) | (newExponent << 10) | (mantissa >>> 13);
}
}
return half;
}
/**
* Extracts the spherical harmonic degree and coefficient from the attribute name.
* @param {string} attribute - The attribute name.
* @returns {object} An object containing the degree (l) and coefficient (n).
* @private
*/
function extractSHDegreeAndCoef(attribute) {
const prefix = getShAttributePrefix(attribute);
const separator = "_COEF_";
const lStart = prefix.length;
const coefIndex = attribute.indexOf(separator, lStart);
const l = parseInt(attribute.slice(lStart, coefIndex), 10);
const n = parseInt(attribute.slice(coefIndex + separator.length), 10);
return { l, n };
}
/**
* Packs spherical harmonic data into half-precision floats.
* @param {GaussianSplat3DTileContent} tileContent - The tile content containing the spherical harmonic data.
* @returns {Uint32Array} - The Float16 packed spherical harmonic data.
* @private
*/
function packSphericalHarmonicsData(tileContent) {
const degree = tileContent.sphericalHarmonicsDegree;
const coefs = tileContent.sphericalHarmonicsCoefficientCount;
const totalLength = tileContent.pointsLength * (coefs * (2 / 3)); //3 packs into 2
const packedData = new Uint32Array(totalLength);
const shAttributes = tileContent.gltfPrimitive.attributes.filter((attr) =>
attr.name.includes("SH_DEGREE_"),
);
let stride = 0;
const base = [0, 9, 24];
switch (degree) {
case 1:
stride = 9;
break;
case 2:
stride = 24;
break;
case 3:
stride = 45;
break;
}
shAttributes.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
const packedStride = stride * (2 / 3);
for (let i = 0; i < shAttributes.length; i++) {
const { l, n } = extractSHDegreeAndCoef(shAttributes[i].name);
for (let j = 0; j < tileContent.pointsLength; j++) {
//interleave the data
const packedBase = (base[l - 1] * 2) / 3;
const idx = j * packedStride + packedBase + n * 2;
const src = j * 3;
packedData[idx] =
float32ToFloat16(shAttributes[i].typedArray[src]) |
(float32ToFloat16(shAttributes[i].typedArray[src + 1]) << 16);
packedData[idx + 1] = float32ToFloat16(
shAttributes[i].typedArray[src + 2],
);
}
}
return packedData;
}
/**
* Creates a new instance of {@link GaussianSplat3DTileContent} from a glTF or glb resource.
*
* @param {Cesium3DTileset} tileset - The tileset that this content belongs to.
* @param {Cesium3DTile} tile - The tile that this content belongs to.
* @param {Resource|string} resource - The resource or URL of the glTF or glb file.
* @param {object|Uint8Array} gltf - The glTF JSON object or a Uint8Array containing the glb binary data.
* @returns {GaussianSplat3DTileContent} A new GaussianSplat3DTileContent instance.
* @throws {RuntimeError} If the glTF or glb fails to load.
* @private
*/
GaussianSplat3DTileContent.fromGltf = async function (
tileset,
tile,
resource,
gltf,
) {
const basePath = resource;
const baseResource = Resource.createIfNeeded(basePath);
const loaderOptions = {
releaseGltfJson: false,
upAxis: Axis.Y,
forwardAxis: Axis.Z,
};
if (defined(gltf.asset)) {
loaderOptions.gltfJson = gltf;
loaderOptions.baseResource = baseResource;
loaderOptions.gltfResource = baseResource;
} else if (gltf instanceof Uint8Array) {
loaderOptions.typedArray = gltf;
loaderOptions.baseResource = baseResource;
loaderOptions.gltfResource = baseResource;
} else {
loaderOptions.gltfResource = Resource.createIfNeeded(gltf);
}
const loader = new GltfLoader(loaderOptions);
try {
await loader.load();
} catch (error) {
loader.destroy();
throw new RuntimeError(`Failed to load glTF: ${error.message}`);
}
return new GaussianSplat3DTileContent(loader, tileset, tile, resource);
};
/**
* Updates the content of the tile and prepares it for rendering.
* @param {Cesium3DTileset}Data attribution
* @param {FrameState} frameState - The current frame state.
* @private
*/
GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) {
const loader = this._loader;
if (this._ready) {
if (!this._transformed && primitive.root.content.ready) {
GaussianSplatPrimitive.transformTile(this._tile);
this._transformed = true;
}
return;
}
frameState.afterRender.push(() => true);
if (!defined(loader)) {
this._ready = true;
return;
}
if (this._resourcesLoaded) {
this.gltfPrimitive = loader.components.scene.nodes[0].primitives[0];
this.worldTransform = loader.components.scene.nodes[0].matrix;
this._ready = true;
this._positions = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray,
);
this._rotations = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray,
);
this._scales = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.gltfPrimitive,
VertexAttributeSemantic.SCALE,
).typedArray,
);
const { l, n } = degreeAndCoefFromAttributes(this.gltfPrimitive.attributes);
this._sphericalHarmonicsDegree = l;
this._sphericalHarmonicsCoefficientCount = n;
this._packedSphericalHarmonicsData = packSphericalHarmonicsData(this);
return;
}
this._resourcesLoaded = loader.process(frameState);
};
/**
* Returns whether the feature has this property.
*
* @param {number} batchId The batchId for the feature.
* @param {string} name The case-sensitive name of the property.
* @returns {boolean} <code>true</code> if the feature has this property; otherwise, <code>false</code>.
*/
GaussianSplat3DTileContent.prototype.hasProperty = function (batchId, name) {
return false;
};
/**
* Returns the {@link Cesium3DTileFeature} object for the feature with the
* given <code>batchId</code>. This object is used to get and modify the
* feature's properties.
* <p>
* Features in a tile are ordered by <code>batchId</code>, an index used to retrieve their metadata from the batch table.
* </p>
*
* @see {@link https://github.com/CesiumGS/3d-tiles/tree/main/specification/TileFormats/BatchTable}.
*
* @param {number} batchId The batchId for the feature.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
*
* @exception {DeveloperError} batchId must be between zero and {@link Cesium3DTileContent#featuresLength} - 1.
*/
GaussianSplat3DTileContent.prototype.getFeature = function (batchId) {
return undefined;
};
/**
* Called when {@link Cesium3DTileset#debugColorizeTiles} changes.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {boolean} enabled Whether to enable or disable debug settings.
* @returns {Cesium3DTileFeature} The corresponding {@link Cesium3DTileFeature} object.
* @private
*/
GaussianSplat3DTileContent.prototype.applyDebugSettings = function (
enabled,
color,
) {};
/**
* Apply a style to the content
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @param {Cesium3DTileStyle} style The style.
*
* @private
*/
GaussianSplat3DTileContent.prototype.applyStyle = function (style) {};
/**
* Find an intersection between a ray and the tile content surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {FrameState} frameState The frame state.
* @param {Cartesian3|undefined} [result] The intersection or <code>undefined</code> if none was found.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @private
*/
GaussianSplat3DTileContent.prototype.pick = function (ray, frameState, result) {
return undefined;
};
/**
* 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.
* <p>
* This is used to implement the <code>Cesium3DTileContent</code> interface, but is
* not part of the public Cesium API.
* </p>
*
* @returns {boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
*
* @see Cesium3DTileContent#destroy
*
* @private
*/
GaussianSplat3DTileContent.prototype.isDestroyed = function () {
return this.isDestroyed;
};
/**
* Frees the resources used by this object.
* @private
*/
GaussianSplat3DTileContent.prototype.destroy = function () {
this.splatPrimitive = undefined;
if (
defined(this._tileset.gaussianSplatPrimitive) &&
!this._tileset.gaussianSplatPrimitive.isDestroyed()
) {
this._tileset.gaussianSplatPrimitive.destroy();
}
this._tileset.gaussianSplatPrimitive = undefined;
this._tile = undefined;
this._tileset = undefined;
this._resource = undefined;
this._ready = false;
this._group = undefined;
this._metadata = undefined;
this._resourcesLoaded = false;
if (defined(this._loader)) {
this._loader.destroy();
this._loader = undefined;
}
return destroyObject(this);
};
export default GaussianSplat3DTileContent;