From 7c6a2a835a7b4bc9be7f346c87505b9abd6495b5 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 1 Aug 2025 02:57:55 -0500 Subject: [PATCH 01/22] clean up --- .../Scene/GaussianSplat3DTileContent.js | 160 ++++++++++++++++++ .../Source/Scene/GaussianSplatPrimitive.js | 141 +++++++++++++++ .../Source/Scene/GltfVertexBufferLoader.js | 40 +++++ .../Shaders/PrimitiveGaussianSplatFS.glsl | 10 +- .../Shaders/PrimitiveGaussianSplatVS.glsl | 105 +++++++++++- 5 files changed, 446 insertions(+), 10 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 210df712a627..e3491e8feb4a 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -78,6 +78,20 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) { * @private */ this._transformed = false; + + /** + * The degree of the spherical harmonics used for the Gaussian splats. + * @type {number} + * @private + */ + this.shDegree = 0; + + /** + * The number of spherical harmonic coefficients used for the Gaussian splats. + * @type {number} + * @private + */ + this.shCoefficientCount = 0; } Object.defineProperties(GaussianSplat3DTileContent.prototype, { @@ -311,6 +325,144 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { }, }); +/** + * Determine Spherical Harmonics degree and coefficient count from attributes + * @param {Array} attributes - The list of attributes. + * @returns {Object} An object containing the degree (l) and coefficient (n). + */ +function degreeAndCoefFromAttributes(attributes) { + const prefix = "_SH_DEGREE_"; + const shAttributes = attributes.filter((attr) => + attr.name.startsWith(prefix), + ); + + 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 {Float} 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 = "_SH_DEGREE_"; + 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 {*} data - The input data to pack. + * @param {*} shDegree - The spherical harmonic degree. + * @returns {Uint32Array} - The packed data. + */ +function packSphericalHarmonicData(tileContent) { + const degree = tileContent.shDegree; + const coefs = tileContent.shCoefficientCount; + const totalLength = tileContent.pointsLength * (coefs * (2 / 3)); //3 packs into 2 + const packedData = new Uint32Array(totalLength); + + const shAttributes = tileContent.splatPrimitive.attributes.filter((attr) => + attr.name.startsWith("_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. * @@ -412,6 +564,14 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) { ).typedArray, ); + const { l, n } = degreeAndCoefFromAttributes( + this.splatPrimitive.attributes, + ); + this.shDegree = l; + this.shCoefficientCount = n; + + this._packedShData = packSphericalHarmonicData(this); + return; } diff --git a/packages/engine/Source/Scene/GaussianSplatPrimitive.js b/packages/engine/Source/Scene/GaussianSplatPrimitive.js index 555bd2bae6e1..c79704f78024 100644 --- a/packages/engine/Source/Scene/GaussianSplatPrimitive.js +++ b/packages/engine/Source/Scene/GaussianSplatPrimitive.js @@ -31,10 +31,12 @@ 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, @@ -44,6 +46,25 @@ const GaussianSplatSortingState = { ERROR: 4, }; +function createGaussianSplatSHTexture(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, @@ -148,6 +169,14 @@ function GaussianSplatPrimitive(options) { * @see {@link GaussianSplatTextureGenerator} */ this.gaussianSplatTexture = undefined; + + /** + * The texture used to store the spherical harmonics coefficients for the Gaussian splats. + * @type {undefined|Texture} + * @private + */ + this.gaussianSplatSHTexture = undefined; + /** * The last width of the Gaussian splat texture. * This is used to track changes in the texture size and update the primitive accordingly. @@ -601,12 +630,65 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function ( ShaderDestination.VERTEX, ); + shaderBuilder.addUniform("float", "u_shDegree", 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._shDegree > 0) { + shaderBuilder.addDefine( + "HAS_SPHERICAL_HARMONICS", + "1", + ShaderDestination.VERTEX, + ); + shaderBuilder.addUniform( + "highp usampler2D", + "u_gaussianSplatSHTexture", + ShaderDestination.VERTEX, + ); + uniformMap.u_gaussianSplatSHTexture = function () { + return primitive.gaussianSplatSHTexture; + }; + } + uniformMap.u_shDegree = function () { + return primitive._shDegree; + }; + + 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; }; @@ -756,6 +838,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { this._scales = undefined; this._colors = undefined; this._indexes = undefined; + this._shData = undefined; this._needsGaussianSplatTexture = true; this._gaussianSplatTexturePending = false; @@ -786,6 +869,31 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { return aggregate; }; + const aggregateShData = () => { + let offset = 0; + for (const tile of tiles) { + const shData = tile.content._packedShData; + if (tile.content.shDegree > 0) { + if (!defined(this._shData)) { + let coefs; + switch (tile.content.shDegree) { + 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, (splatPrimitive) => @@ -822,6 +930,9 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { ), ); + aggregateShData(); + this._shDegree = tiles[0].content.shDegree; + this._numSplats = totalElements; this.selectedTileLength = tileset._selectedTiles.length; } @@ -833,6 +944,36 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { if (this._needsGaussianSplatTexture) { if (!this._gaussianSplatTexturePending) { GaussianSplatPrimitive.generateSplatTexture(this, frameState); + if (defined(this._shData)) { + const oldTex = this.gaussianSplatSHTexture; + const width = ContextLimits.maximumTextureSize; + const dims = tileset._selectedTiles[0].content.shCoefficientCount / 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.gaussianSplatSHTexture = createGaussianSplatSHTexture( + frameState.context, + { + data: texBuf, + width: width, + height: Math.ceil(this._numSplats / splatsPerRow), + }, + ); + if (defined(oldTex)) { + oldTex.destroy(); + } + } } return; } diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js index 2ab17332ac7f..078a24755544 100644 --- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js +++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js @@ -306,6 +306,19 @@ async function loadFromSpz(vertexBufferLoader) { } } +function extractSHDegreeAndCoef(attribute) { + const prefix = "_SH_DEGREE_"; + 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 }; +} + function processSpz(vertexBufferLoader) { vertexBufferLoader._state = ResourceLoaderState.PROCESSING; const spzLoader = vertexBufferLoader._spzLoader; @@ -344,6 +357,33 @@ function processSpz(vertexBufferLoader) { 255.0, ); } + } else if (vertexBufferLoader._attributeSemantic.startsWith("_SH_DEGREE_")) { + const { l, n } = extractSHDegreeAndCoef( + vertexBufferLoader._attributeSemantic, + ); + const shDegree = gcloudData.shDegree; + let stride = 0; + const base = [0, 9, 24]; + switch (shDegree) { + case 1: + stride = 9; + break; + case 2: + stride = 24; + break; + case 3: + stride = 45; + break; + } + const count = gcloudData.numPoints; + const sh = gcloudData.sh; + vertexBufferLoader._typedArray = new Float32Array(count * 3); + for (let i = 0; i < count; i++) { + const idx = i * stride + base[l - 1] + n * 3; + vertexBufferLoader._typedArray[i * 3] = sh[idx]; + vertexBufferLoader._typedArray[i * 3 + 1] = sh[idx + 1]; + vertexBufferLoader._typedArray[i * 3 + 2] = sh[idx + 2]; + } } } diff --git a/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glsl b/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glsl index 8700569edc06..32710da348cc 100644 --- a/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glsl +++ b/packages/engine/Source/Shaders/PrimitiveGaussianSplatFS.glsl @@ -8,11 +8,11 @@ void main() { if (v_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard; if (v_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard; - mediump float A = dot(v_vertPos, v_vertPos); - if(A > 1.0) { + float A = -dot(v_vertPos, v_vertPos); + if (A < -4.) { discard; } - mediump float scale = 4.0; - mediump float B = exp(-A * scale) * (v_splatColor.a); - out_FragColor = vec4(v_splatColor.rgb * B, B); + + float B = exp(A * 4.) * v_splatColor.a ; + out_FragColor = vec4(v_splatColor.rgb * B , B); } diff --git a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl index 6a7341b5f198..48a0a91d9608 100644 --- a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl +++ b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl @@ -7,18 +7,108 @@ // // Discards splats outside the view frustum or with negligible screen size. // +#if defined(HAS_SPHERICAL_HARMONICS) +const uint coefficientCount[3] = uint[3](3u,8u,15u); +const float SH_C1 = 0.48860251; +const float SH_C2[5] = float[5]( + 1.092548430, + -1.09254843, + 0.315391565, + -1.09254843, + 0.546274215 +); + +const float SH_C3[7] = float[7]( + -0.59004358, + 2.890611442, + -0.45704579, + 0.373176332, + -0.45704579, + 1.445305721, + -0.59004358 +); + +uvec2 loadSHCoeff(uint splatID, int index) { + ivec2 shTexSize = textureSize(u_gaussianSplatSHTexture, 0); + uint dims = coefficientCount[uint(u_shDegree)-1u]; + uint splatsPerRow = uint(shTexSize.x) / dims; + uint shIndex = (splatID%splatsPerRow) * dims + uint(index); + ivec2 shPosCoord = ivec2(shIndex, splatID / splatsPerRow); + return texelFetch(u_gaussianSplatSHTexture, shPosCoord, 0).rg; +} + +vec3 halfToVec3(uvec2 packed) { + return vec3(unpackHalf2x16(packed.x), unpackHalf2x16(packed.y).x); +} + +vec3 loadAndExpandSHCoeff(uint splatID, int index) { + uvec2 coeff = loadSHCoeff(splatID, index); + return halfToVec3(coeff); +} + +vec3 evaluateSH(uint splatID, vec3 viewDir) { + vec3 result = vec3(0.0); + int coeffIndex = 0; + float x = viewDir.x, y = viewDir.y, z = viewDir.z; + + if (u_shDegree >= 1.) { + vec3 sh1 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh2 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh3 = loadAndExpandSHCoeff(splatID, coeffIndex++); + result += -SH_C1 * y * sh1 + SH_C1 * z * sh2 - SH_C1 * x * sh3; + + if (u_shDegree >= 2.) { + float xx = x * x; + float yy = y * y; + float zz = z * z; + float xy = x * y; + float yz = y * z; + float xz = x * z; + + vec3 sh4 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh5 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh6 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh7 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh8 = loadAndExpandSHCoeff(splatID, coeffIndex++); + result += SH_C2[0] * xy * sh4 + + SH_C2[1] * yz * sh5 + + SH_C2[2] * (2.0f * zz - xx - yy) * sh6 + + SH_C2[3] * xz * sh7 + + SH_C2[4] * (xx - yy) * sh8; + + if (u_shDegree >= 3.) { + vec3 sh9 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh10 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh11 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh12 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh13 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh14 = loadAndExpandSHCoeff(splatID, coeffIndex++); + vec3 sh15 = loadAndExpandSHCoeff(splatID, coeffIndex++); + result += SH_C3[0] * y * (3.0f * xx - yy) * sh9 + + SH_C3[1] * xy * z * sh10 + + SH_C3[2] * y * (4.0f * zz - xx - yy) * sh11 + + SH_C3[3] * z * (2.0f * zz - 3.0f * xx - 3.0f * yy) * sh12 + + SH_C3[4] * x * (4.0f * zz - xx - yy) * sh13 + + SH_C3[5] * z * (xx - yy) * sh14 + + SH_C3[6] * x * (xx - 3.0f * yy) * sh15; + } + } + } + return result; +} +#endif // Transforms and projects splat covariance into screen space and extracts the major and minor axes of the Gaussian ellipsoid // which is used to calculate the vertex position in clip space. vec4 calcCovVectors(vec3 viewPos, mat3 Vrk) { vec4 t = vec4(viewPos, 1.0); - float focal = czm_viewport.z * czm_projection[0][0]; + vec2 focal = vec2(czm_projection[0][0] * czm_viewport.z, czm_projection[1][1] * czm_viewport.w); - float J1 = focal / t.z; - vec2 J2 = -J1 / t.z * t.xy; + vec2 J1 = focal / t.z; + vec2 J2 = -focal * vec2(t.x, t.y) / (t.z * t.z); mat3 J = mat3( - J1, 0.0, J2.x, - 0.0, J1, J2.y, + J1.x, 0.0, J2.x, + 0.0, J1.y, J2.y, 0.0, 0.0, 0.0 ); @@ -87,6 +177,11 @@ void main() { v_vertPos = corner ; v_splatColor = vec4(covariance.w & 0xffu, (covariance.w >> 8) & 0xffu, (covariance.w >> 16) & 0xffu, (covariance.w >> 24) & 0xffu) / 255.0; +#if defined(HAS_SPHERICAL_HARMONICS) + vec4 splatWC = czm_inverseView * splatViewPos; + vec3 viewDirModel = normalize(u_inverseModelRotation * (splatWC.xyz - u_cameraPositionWC.xyz)); + v_splatColor.rgb += evaluateSH(texIdx, viewDirModel).rgb; +#endif v_splitDirection = u_splitDirection; } \ No newline at end of file From a78ab94eebbf9c914e326de2d6a312198b8fe38f Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 1 Aug 2025 03:02:15 -0500 Subject: [PATCH 02/22] SH changes --- CHANGES.md | 1 + packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 9d8fe3a04529..ba41c85c65f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - Expand the CustomShader Sample to support real-time modification of CustomShader. [#12702](https://github.com/CesiumGS/cesium/pull/12702) - Add wrapR property to Sampler and Texture3D, to support the newly added third dimension wrap.[#12701](https://github.com/CesiumGS/cesium/pull/12701) - Added the ability to load a specific changeset for iTwin Mesh Exports using `ITwinData.createTilesetFromIModelId` [#12778](https://github.com/CesiumGS/cesium/issues/12778) +- Added spherical harmonics support for Gaussian Splats. Supports degrees 1, 2, and 3 in the SPZ format. #### Deprecated :hourglass_flowing_sand: diff --git a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl index 48a0a91d9608..6096b4d93545 100644 --- a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl +++ b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl @@ -28,6 +28,7 @@ const float SH_C3[7] = float[7]( -0.59004358 ); +//Retrieve SH coefficient. Currently RG32UI format uvec2 loadSHCoeff(uint splatID, int index) { ivec2 shTexSize = textureSize(u_gaussianSplatSHTexture, 0); uint dims = coefficientCount[uint(u_shDegree)-1u]; @@ -37,6 +38,7 @@ uvec2 loadSHCoeff(uint splatID, int index) { return texelFetch(u_gaussianSplatSHTexture, shPosCoord, 0).rg; } +//Unpack RG32UI half float coefficients to vec3 vec3 halfToVec3(uvec2 packed) { return vec3(unpackHalf2x16(packed.x), unpackHalf2x16(packed.y).x); } From faf01119261968a54e7a51ca74f9c14bbbb44878 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Tue, 26 Aug 2025 14:08:58 -0500 Subject: [PATCH 03/22] update changes for clarity --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a839665480cb..4df2bca88054 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ #### Additions :tada: - Added support for the [EXT_mesh_primitive_restart](https://github.com/KhronosGroup/glTF/pull/2478) glTF extension. [#12764](https://github.com/CesiumGS/cesium/issues/12764) +- Added spherical harmonics support for Gaussian Splats. Currently supported through the SPZ compression format. ## 1.132 - 2025-08-01 @@ -40,7 +41,6 @@ - Expand the CustomShader Sample to support real-time modification of CustomShader. [#12702](https://github.com/CesiumGS/cesium/pull/12702) - Add wrapR property to Sampler and Texture3D, to support the newly added third dimension wrap.[#12701](https://github.com/CesiumGS/cesium/pull/12701) - Added the ability to load a specific changeset for iTwin Mesh Exports using `ITwinData.createTilesetFromIModelId` [#12778](https://github.com/CesiumGS/cesium/issues/12778) -- Added spherical harmonics support for Gaussian Splats. Supports degrees 1, 2, and 3 in the SPZ format. #### Deprecated :hourglass_flowing_sand: From 2448e21e984e1f70fbe9af079795b63808d7f403 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Tue, 26 Aug 2025 15:59:11 -0500 Subject: [PATCH 04/22] gltf spec updates --- .../Scene/Cesium3DTileContentFactory.js | 18 ++---------- .../Scene/GaussianSplat3DTileContent.js | 29 +++++++++++++++++++ packages/engine/Source/Scene/GltfLoader.js | 14 +++++++-- .../Source/Scene/GltfVertexBufferLoader.js | 21 +++++++++++--- .../engine/Source/Scene/PrimitiveLoadPlan.js | 2 +- .../Source/Scene/VertexAttributeSemantic.js | 8 +++-- 6 files changed, 65 insertions(+), 27 deletions(-) diff --git a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js index f896ef709304..f03ac497fe45 100644 --- a/packages/engine/Source/Scene/Cesium3DTileContentFactory.js +++ b/packages/engine/Source/Scene/Cesium3DTileContentFactory.js @@ -93,28 +93,14 @@ const Cesium3DTileContentFactory = { const dataView = new DataView(arrayBuffer, byteOffset); const byteLength = dataView.getUint32(8, true); const glb = new Uint8Array(arrayBuffer, byteOffset, byteLength); - let hasGaussianSplatExtension = false; - if (tileset.isGltfExtensionRequired instanceof Function) { - hasGaussianSplatExtension = tileset.isGltfExtensionRequired( - "KHR_spz_gaussian_splats_compression", - ); - } - if (hasGaussianSplatExtension) { + if (GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt(tileset)) { return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, glb); } return Model3DTileContent.fromGltf(tileset, tile, resource, glb); }, gltf: function (tileset, tile, resource, json) { - const forceGaussianSplats = - tileset.debugTreatTilesetAsGaussianSplats ?? false; - let hasGaussianSplatExtension = false; - if (tileset.isGltfExtensionRequired instanceof Function) { - hasGaussianSplatExtension = tileset.isGltfExtensionRequired( - "KHR_spz_gaussian_splats_compression", - ); - } - if (forceGaussianSplats || hasGaussianSplatExtension) { + if (GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt(tileset)) { return GaussianSplat3DTileContent.fromGltf(tileset, tile, resource, json); } diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index e3491e8feb4a..89fa2833f814 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -7,6 +7,7 @@ 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-spz-splat-compression/extensions/2.0/Khronos/KHR_spz_gaussian_splats_compression|KHR_spz_gaussian_splats_compression} extension. @@ -325,6 +326,34 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { }, }); +GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt = function (tileset) { + let hasGaussianSplatExtension = false; + let hasLegacyGaussianSplatExtension = false; + if (tileset.isGltfExtensionRequired instanceof Function) { + hasGaussianSplatExtension = + tileset.isGltfExtensionRequired("KHR_gaussian_splatting") && + tileset.isGltfExtensionRequired( + "KHR_gaussian_splatting_compression_spz_2", + ); + + hasLegacyGaussianSplatExtension = tileset.isGltfExtensionRequired( + "KHR_spz_gaussian_splats_compression", + ); + } + + if (hasLegacyGaussianSplatExtension) { + deprecationWarning( + "KHR_spz_gaussian_splats_compression", + "Support for the original KHR_spz_gaussian_splats_compression extension has been deprecated in favor " + + "of the up to date KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extensions and will be " + + "removed in CesiumJS 1.134.\n\nPlease retile your tileset with the KHR_gaussian_splatting and " + + "KHR_gaussian_splatting_compression_spz_2 extensions.", + ); + } + + return hasGaussianSplatExtension || hasLegacyGaussianSplatExtension; +}; + /** * Determine Spherical Harmonics degree and coefficient count from attributes * @param {Array} attributes - The list of attributes. diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index abfa577d0554..da7316c86c2a 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -2020,8 +2020,16 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { ); } - const spzExtension = extensions.KHR_spz_gaussian_splats_compression; - if (defined(spzExtension)) { + //support the latest glTF spec and the legacy extension + const gsExtension = extensions.KHR_spz_gaussian_splatting; + const spzExtension = + gsExtension?.extensions.KHR_gaussian_splatting_compression_2; + const legacySpzExtension = extensions.KHR_spz_gaussian_splats_compression; + + if ( + defined(gsExtension) && + (defined(spzExtension) || defined(legacySpzExtension)) + ) { needsPostProcessing = true; primitivePlan.needsGaussianSplats = true; } @@ -2058,7 +2066,7 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { semanticInfo, gltfPrimitive, draco, - spzExtension, + spzExtension || legacySpzExtension, hasInstances, needsPostProcessing, frameState, diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js index 078a24755544..2a3d388cb828 100644 --- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js +++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js @@ -306,8 +306,15 @@ async function loadFromSpz(vertexBufferLoader) { } } +function getShPrefix(attribute) { + const prefix = attribute.startsWith("KHR_gaussian_splatting:") + ? "KHR_gaussian_splatting:" + : "_"; + return `${prefix}SH_DEGREE_`; +} + function extractSHDegreeAndCoef(attribute) { - const prefix = "_SH_DEGREE_"; + const prefix = getShPrefix(attribute); const separator = "_COEF_"; const lStart = prefix.length; @@ -327,9 +334,15 @@ function processSpz(vertexBufferLoader) { if (vertexBufferLoader._attributeSemantic === "POSITION") { vertexBufferLoader._typedArray = gcloudData.positions; - } else if (vertexBufferLoader._attributeSemantic === "_SCALE") { + } else if ( + vertexBufferLoader._attributeSemantic === "_SCALE" || + vertexBufferLoader._attributeSemantic === "KHR_gaussian_splatting:SCALE" + ) { vertexBufferLoader._typedArray = gcloudData.scales; - } else if (vertexBufferLoader._attributeSemantic === "_ROTATION") { + } else if ( + vertexBufferLoader._attributeSemantic === "_ROTATION" || + vertexBufferLoader._attributeSemantic === "KHR_gaussian_splatting:ROTATION" + ) { vertexBufferLoader._typedArray = gcloudData.rotations; } else if (vertexBufferLoader._attributeSemantic === "COLOR_0") { const colors = gcloudData.colors; @@ -357,7 +370,7 @@ function processSpz(vertexBufferLoader) { 255.0, ); } - } else if (vertexBufferLoader._attributeSemantic.startsWith("_SH_DEGREE_")) { + } else if (vertexBufferLoader._attributeSemantic.includes("SH_DEGREE_")) { const { l, n } = extractSHDegreeAndCoef( vertexBufferLoader._attributeSemantic, ); diff --git a/packages/engine/Source/Scene/PrimitiveLoadPlan.js b/packages/engine/Source/Scene/PrimitiveLoadPlan.js index b136ae263407..ce38dc822dbc 100644 --- a/packages/engine/Source/Scene/PrimitiveLoadPlan.js +++ b/packages/engine/Source/Scene/PrimitiveLoadPlan.js @@ -160,7 +160,7 @@ function PrimitiveLoadPlan(primitive) { /** * Set this true to indicate that the primitive has the - * KHR_gaussian_splatting extension and needs to be post-processed + * KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extension and needs to be post-processed * * @type {boolean} * @private diff --git a/packages/engine/Source/Scene/VertexAttributeSemantic.js b/packages/engine/Source/Scene/VertexAttributeSemantic.js index 3663eb5cdb77..a0fa769aa5fe 100644 --- a/packages/engine/Source/Scene/VertexAttributeSemantic.js +++ b/packages/engine/Source/Scene/VertexAttributeSemantic.js @@ -79,14 +79,14 @@ const VertexAttributeSemantic = { * @type {string} * @constant */ - SCALE: "_SCALE", + SCALE: "KHR_gaussian_splatting:SCALE", /** * Gaussian Splat Rotation * * @type {string} * @constant */ - ROTATION: "_ROTATION", + ROTATION: "KHR_gaussian_splatting:ROTATION", }; function semanticToVariableName(semantic) { @@ -193,8 +193,10 @@ VertexAttributeSemantic.fromGltfSemantic = function (gltfSemantic) { case "_FEATURE_ID": return VertexAttributeSemantic.FEATURE_ID; case "_SCALE": + case "KHR_gaussian_splatting:SCALE": return VertexAttributeSemantic.SCALE; case "_ROTATION": + case "KHR_gaussian_splatting:ROTATION": return VertexAttributeSemantic.ROTATION; } @@ -284,7 +286,7 @@ VertexAttributeSemantic.getGlslType = function (semantic) { * @param {VertexAttributeSemantic} semantic The semantic. * @param {number} [setIndex] The set index. * - * @returns {string} The variable name. + * @returns {string} The varia_SCALEble name. * * @private */ From f9f99aaad0f1c4c6d3f4a3ec1122de4e59a3c1f1 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Tue, 26 Aug 2025 16:16:18 -0500 Subject: [PATCH 05/22] fix extension check --- packages/engine/Source/Scene/GltfLoader.js | 6 +++--- packages/engine/Source/Scene/Model/ModelUtility.js | 1 + packages/engine/Source/Scene/VertexAttributeSemantic.js | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index da7316c86c2a..4288102ecca8 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -2023,12 +2023,12 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { //support the latest glTF spec and the legacy extension const gsExtension = extensions.KHR_spz_gaussian_splatting; const spzExtension = - gsExtension?.extensions.KHR_gaussian_splatting_compression_2; + gsExtension?.extensions.KHR_gaussian_splatting_compression_spz_2; const legacySpzExtension = extensions.KHR_spz_gaussian_splats_compression; if ( - defined(gsExtension) && - (defined(spzExtension) || defined(legacySpzExtension)) + (defined(gsExtension) && defined(spzExtension)) || + defined(legacySpzExtension) ) { needsPostProcessing = true; primitivePlan.needsGaussianSplats = true; diff --git a/packages/engine/Source/Scene/Model/ModelUtility.js b/packages/engine/Source/Scene/Model/ModelUtility.js index ad8ab108f3c1..da94991a68bd 100644 --- a/packages/engine/Source/Scene/Model/ModelUtility.js +++ b/packages/engine/Source/Scene/Model/ModelUtility.js @@ -369,6 +369,7 @@ ModelUtility.supportedExtensions = { KHR_texture_basisu: true, KHR_texture_transform: true, KHR_gaussian_splatting: true, + KHR_gaussian_splatting_compression_spz_2: true, KHR_spz_gaussian_splats_compression: true, WEB3D_quantized_attributes: true, }; diff --git a/packages/engine/Source/Scene/VertexAttributeSemantic.js b/packages/engine/Source/Scene/VertexAttributeSemantic.js index a0fa769aa5fe..a3fcf225b5a2 100644 --- a/packages/engine/Source/Scene/VertexAttributeSemantic.js +++ b/packages/engine/Source/Scene/VertexAttributeSemantic.js @@ -79,6 +79,7 @@ const VertexAttributeSemantic = { * @type {string} * @constant */ + _SCALE: "_SCALE", SCALE: "KHR_gaussian_splatting:SCALE", /** * Gaussian Splat Rotation @@ -86,6 +87,7 @@ const VertexAttributeSemantic = { * @type {string} * @constant */ + _ROTATION: "_ROTATION", ROTATION: "KHR_gaussian_splatting:ROTATION", }; From 42e0c8d81b2041daabf036c59bfe44363a778a78 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Wed, 27 Aug 2025 09:53:33 -0500 Subject: [PATCH 06/22] spec update support for SH content --- .../Source/Scene/GaussianSplat3DTileContent.js | 14 ++++++++++---- .../engine/Source/Scene/GltfVertexBufferLoader.js | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 89fa2833f814..834f9e8b3bfa 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -354,15 +354,21 @@ GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt = function (tileset) { return hasGaussianSplatExtension || hasLegacyGaussianSplatExtension; }; +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 {Array} attributes - The list of attributes. * @returns {Object} An object containing the degree (l) and coefficient (n). */ function degreeAndCoefFromAttributes(attributes) { - const prefix = "_SH_DEGREE_"; const shAttributes = attributes.filter((attr) => - attr.name.startsWith(prefix), + attr.name.includes("SH_DEGREE_"), ); switch (shAttributes.length) { @@ -423,7 +429,7 @@ function float32ToFloat16(float32) { * @private */ function extractSHDegreeAndCoef(attribute) { - const prefix = "_SH_DEGREE_"; + const prefix = getShAttributePrefix(attribute); const separator = "_COEF_"; const lStart = prefix.length; @@ -448,7 +454,7 @@ function packSphericalHarmonicData(tileContent) { const packedData = new Uint32Array(totalLength); const shAttributes = tileContent.splatPrimitive.attributes.filter((attr) => - attr.name.startsWith("_SH_DEGREE_"), + attr.name.includes("SH_DEGREE_"), ); let stride = 0; const base = [0, 9, 24]; diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js index 2a3d388cb828..bb69567d2500 100644 --- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js +++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js @@ -306,7 +306,7 @@ async function loadFromSpz(vertexBufferLoader) { } } -function getShPrefix(attribute) { +function getShAttributePrefix(attribute) { const prefix = attribute.startsWith("KHR_gaussian_splatting:") ? "KHR_gaussian_splatting:" : "_"; @@ -314,7 +314,7 @@ function getShPrefix(attribute) { } function extractSHDegreeAndCoef(attribute) { - const prefix = getShPrefix(attribute); + const prefix = getShAttributePrefix(attribute); const separator = "_COEF_"; const lStart = prefix.length; From 81d252b76fe1cf9085e179fd79833c0936d82edc Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Wed, 27 Aug 2025 10:12:16 -0500 Subject: [PATCH 07/22] wording --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4df2bca88054..c4b671197ce0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,7 +16,7 @@ #### Additions :tada: - Added support for the [EXT_mesh_primitive_restart](https://github.com/KhronosGroup/glTF/pull/2478) glTF extension. [#12764](https://github.com/CesiumGS/cesium/issues/12764) -- Added spherical harmonics support for Gaussian Splats. Currently supported through the SPZ compression format. +- Added spherical harmonics support for Gaussian splats. Currently supported with the SPZ compression format. ## 1.132 - 2025-08-01 From 1d03ac0952d22ac8506030aabd133a83c8e3cf75 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Wed, 27 Aug 2025 12:11:17 -0500 Subject: [PATCH 08/22] extension spelling --- packages/engine/Source/Scene/GltfLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index 4288102ecca8..d4fa7816b985 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -2021,7 +2021,7 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { } //support the latest glTF spec and the legacy extension - const gsExtension = extensions.KHR_spz_gaussian_splatting; + const gsExtension = extensions.KHR_gaussian_splatting; const spzExtension = gsExtension?.extensions.KHR_gaussian_splatting_compression_spz_2; const legacySpzExtension = extensions.KHR_spz_gaussian_splats_compression; From 40e4fe65ad9daa76e2db613ed421e7acb6e478a2 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Wed, 27 Aug 2025 12:33:22 -0500 Subject: [PATCH 09/22] spz extension check clean up and sync --- packages/engine/Source/Scene/GltfLoader.js | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index d4fa7816b985..0139d89bc9e0 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -1983,6 +1983,22 @@ function loadMorphTarget( return morphTarget; } +function fetchSpzExtensionFrom(extensions) { + const gaussianSplatting = extensions?.KHR_gaussian_splatting; + const gsExtensions = gaussianSplatting?.extensions; + const spz = gsExtensions?.KHR_gaussian_splatting_compression_spz_2; + if (defined(spz)) { + return spz; + } + + const legacySpz = extensions?.KHR_spz_gaussian_splats_compression; + if (defined(legacySpz)) { + return legacySpz; + } + + return undefined; +} + /** * Load resources associated with a mesh primitive for a glTF node * @param {GltfLoader} loader @@ -2021,15 +2037,8 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { } //support the latest glTF spec and the legacy extension - const gsExtension = extensions.KHR_gaussian_splatting; - const spzExtension = - gsExtension?.extensions.KHR_gaussian_splatting_compression_spz_2; - const legacySpzExtension = extensions.KHR_spz_gaussian_splats_compression; - - if ( - (defined(gsExtension) && defined(spzExtension)) || - defined(legacySpzExtension) - ) { + const spzExtension = fetchSpzExtensionFrom(extensions); + if (defined(spzExtension)) { needsPostProcessing = true; primitivePlan.needsGaussianSplats = true; } @@ -2066,7 +2075,7 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) { semanticInfo, gltfPrimitive, draco, - spzExtension || legacySpzExtension, + spzExtension, hasInstances, needsPostProcessing, frameState, From 8ecb57c738a8d9acba3dae01a734f3b8b7896fed Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Thu, 28 Aug 2025 16:02:12 -0500 Subject: [PATCH 10/22] Add spherical harmonic unit test. Checks 3 rotations around the central splat in the cube. --- .../Specs/Scene/GaussianSplatPrimitiveSpec.js | 128 +++++++++++++++++- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js b/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js index 3a743a5e4d68..2c28f536af90 100644 --- a/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js +++ b/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js @@ -5,21 +5,31 @@ import { RequestScheduler, HeadingPitchRange, GaussianSplat3DTileContent, + Transforms, } from "../../index.js"; import Cesium3DTilesTester from "../../../../Specs/Cesium3DTilesTester.js"; import createScene from "../../../../Specs/createScene.js"; +import createCanvas from "../../../../Specs/createCanvas.js"; +import pollToPromise from "../../../../Specs/pollToPromise.js"; describe( "Scene/GaussianSplatPrimitive", function () { const tilesetUrl = "./Data/Cesium3DTiles/GaussianSplats/tower/tileset.json"; + const sphericalHarmonicUrl = + "./Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json"; let scene; let options; + let camera; + let canvas; + + const canvassize = { width: 512, height: 512 }; beforeAll(function () { - scene = createScene(); + canvas = createCanvas(canvassize.width, canvassize.height); + scene = createScene({ canvas }); }); afterAll(function () { @@ -30,7 +40,7 @@ describe( RequestScheduler.clearForSpecs(); scene.morphTo3D(0.0); - const camera = scene.camera; + camera = scene.camera; camera.frustum = new PerspectiveFrustum(); camera.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; @@ -47,7 +57,7 @@ describe( ResourceCache.clearForSpecs(); }); - it("loads a Gaussian splats tileset", async function () { + xit("loads a Gaussian splats tileset", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, tilesetUrl, @@ -84,17 +94,127 @@ describe( tileset.boundingSphere.center, new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius), ); + const tile = await Cesium3DTilesTester.waitForTileContentReady( scene, tileset.root, ); + expect(tile.content).toBeDefined(); - expect(tileset.gaussianSplatPrimitive).toBeDefined(); + + const gsPrim = tileset.gaussianSplatPrimitive; + expect(gsPrim).toBeDefined(); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._hasGaussianSplatTexture; + }); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._dirty === false && gsPrim._sorterState === 0; + }); expect(scene).notToRender([0, 0, 0, 255]); tileset.show = false; expect(scene).toRender([0, 0, 0, 255]); }); + + it("Check Spherical Harmonic specular on a Gaussian splats tileset", async function () { + const tileset = await Cesium3DTilesTester.loadTileset( + scene, + sphericalHarmonicUrl, + options, + ); + + const boundingSphere = tileset.boundingSphere; + const yellowish = new HeadingPitchRange( + CesiumMath.toRadians(231), + CesiumMath.toRadians(-75), + tileset.boundingSphere.radius / 10, + ); + const orangeish = new HeadingPitchRange( + CesiumMath.toRadians(2), + CesiumMath.toRadians(-76), + tileset.boundingSphere.radius / 10, + ); + const purplish = new HeadingPitchRange( + CesiumMath.toRadians(100), + CesiumMath.toRadians(66), + tileset.boundingSphere.radius / 10, + ); + const targetOrange = { red: 210, green: 156, blue: 98 }; + const targetYellow = { red: 189, green: 173, blue: 97 }; + const targetPurple = { red: 127, green: 80, blue: 141 }; + + const samplePosition = + ((canvassize.width / 2) * canvassize.height + canvassize.width / 2) * 4; + + const enu = Transforms.eastNorthUpToFixedFrame(boundingSphere.center); + + scene.camera.lookAtTransform(enu, yellowish); + + await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root); + + const gsPrim = tileset.gaussianSplatPrimitive; + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._hasGaussianSplatTexture; + }); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._dirty === false && gsPrim._sorterState === 0; + }); + + scene.renderForSpecs(); + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba[samplePosition + 0]).toBeCloseTo(targetYellow.red, -1); + expect(rgba[samplePosition + 1]).toBeCloseTo(targetYellow.green, -1); + expect(rgba[samplePosition + 2]).toBeCloseTo(targetYellow.blue, -1); + }); + + scene.camera.lookAtTransform(enu, orangeish); + + await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._hasGaussianSplatTexture; + }); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._dirty === false && gsPrim._sorterState === 0; + }); + + scene.renderForSpecs(); + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba[samplePosition + 0]).toBeCloseTo(targetOrange.red, -1); + expect(rgba[samplePosition + 1]).toBeCloseTo(targetOrange.green, -1); + expect(rgba[samplePosition + 2]).toBeCloseTo(targetOrange.blue, -1); + }); + scene.camera.lookAtTransform(enu, purplish); + + await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._hasGaussianSplatTexture; + }); + + await pollToPromise(function () { + scene.renderForSpecs(); + return gsPrim._dirty === false && gsPrim._sorterState === 0; + }); + + scene.renderForSpecs(); + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba[samplePosition + 0]).toBeCloseTo(targetPurple.red, -1); + expect(rgba[samplePosition + 1]).toBeCloseTo(targetPurple.green, -1); + expect(rgba[samplePosition + 2]).toBeCloseTo(targetPurple.blue, -1); + }); + }); }, "WebGL", ); From 0b099c36454884c7e9d2b335a6bfadfa6e8ed23e Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Thu, 28 Aug 2025 16:04:27 -0500 Subject: [PATCH 11/22] sh unit test data --- .../GaussianSplats/sh_unit_cube/0/0.glb | Bin 0 -> 2688 bytes .../GaussianSplats/sh_unit_cube/tileset.json | 1 + 2 files changed, 1 insertion(+) create mode 100644 Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/0/0.glb create mode 100644 Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json diff --git a/Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/0/0.glb b/Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/0/0.glb new file mode 100644 index 0000000000000000000000000000000000000000..d52cacc284df492eb5a00d9d27bf5230d1e6d456 GIT binary patch literal 2688 zcmeHJUq}=|99}CfPqH4O9tu+Cp%7NNJOAZfp?02s4=>JBgSfW!9Pb8hcXfBqPURpX z5G|~Sh?1f|lnCXMAURki4j~*@v8WJ9RN58XjhtsuOQW9)^D#vsyvQlNQC?Mv z7)Gqg02h*kUKs;iqVPe^VL%05#WLZe3KtWjL`6aNw`6|Y7bEh&N(iC(PzX3kvI0vO zLX?vvVo?nY^kU>Jw>v6W=!}Ufaylwmq-x!$!{aUmwgMlAS@seeYrGIoAR_b?(~gZ# zFu|q^ZD)}v$pRlGgLFZv9K$Bb%24_U=H#-&XB&ST|M$bOU7!D6A1ZA{!JykA)Q@HA zGRTP>B{q-}>WcOBK%XZ0t^pN$u^3h(AaR>50TcD%4uUTj5Dku;HW*4pZ}^oKZqpZO zX6lI>j`>gA`q4I8^7S3GUZ{JSMy}4&;P-gAQqJx3 z)N>WcX+Lh}wyE%PfPrMaZQ%uRoQgdk>+GZnN540JsMF_|Rq1~#|~{P}8EgJdVYDFU#Hg6^CdIefJb5p?rNu+TA6|?xzK`V=e&Qxx>&QvH7ixF9(%vu znHQXNsl#)wOARjzbC#z@j^ykvx}1B@KE|q_KS-|{q^aP?^_E9ObpP}mT&)|+?Y_H& zs;456PmRs^+{k@vO^)UD@X1~43k?M;Uo1yYc&C@+Z{y4McVDXx_3d-5Md#|RB(gUD z?a8>~{Am98>~(UuWb)0!!s-1pXC5q#UaP)6!;j|Uq6zlJjmJyREyCh~fpB~6gmA0N VJ+Qa)=E`^fGw$itA8Qtd`2~5td%6Gs literal 0 HcmV?d00001 diff --git a/Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json b/Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json new file mode 100644 index 000000000000..30b0151821d5 --- /dev/null +++ b/Specs/Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json @@ -0,0 +1 @@ +{"asset":{"extras":{"ion":{"georeferenced":false,"movable":true}},"version":"1.1"},"extensions":{"3DTILES_content_gltf":{"extensionsRequired":["KHR_gaussian_splatting","KHR_gaussian_splatting_compression_spz_2"],"extensionsUsed":["KHR_gaussian_splatting","KHR_gaussian_splatting_compression_spz_2"]}},"extensionsUsed":["3DTILES_content_gltf"],"geometricError":173.20508075688772,"root":{"boundingVolume":{"box":[50.0,-50.0,-50.0,50.0,0.0,0.0,0.0,50.0,0.0,0.0,0.0,50.0]},"content":{"uri":"0/0.glb"},"geometricError":0.0,"refine":"REPLACE"}} From 3e0ac2b06f40a43b01859ddf2a90092ca27f567b Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Thu, 28 Aug 2025 16:45:58 -0500 Subject: [PATCH 12/22] Fixed re-enabled tests --- .../Specs/Scene/GaussianSplatPrimitiveSpec.js | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js b/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js index 2c28f536af90..c3905b3fa56c 100644 --- a/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js +++ b/packages/engine/Specs/Scene/GaussianSplatPrimitiveSpec.js @@ -16,19 +16,19 @@ import pollToPromise from "../../../../Specs/pollToPromise.js"; describe( "Scene/GaussianSplatPrimitive", function () { - const tilesetUrl = "./Data/Cesium3DTiles/GaussianSplats/tower/tileset.json"; const sphericalHarmonicUrl = "./Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json"; let scene; let options; let camera; - let canvas; const canvassize = { width: 512, height: 512 }; + const samplePosition = + ((canvassize.width / 2) * canvassize.height + canvassize.width / 2) * 4; beforeAll(function () { - canvas = createCanvas(canvassize.width, canvassize.height); + const canvas = createCanvas(canvassize.width, canvassize.height); scene = createScene({ canvas }); }); @@ -57,10 +57,10 @@ describe( ResourceCache.clearForSpecs(); }); - xit("loads a Gaussian splats tileset", async function () { + it("loads a Gaussian splats tileset", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - tilesetUrl, + sphericalHarmonicUrl, options, ); scene.camera.lookAt( @@ -68,11 +68,18 @@ describe( new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius), ); expect(tileset.hasExtension("3DTILES_content_gltf")).toBe(true); + expect(tileset.isGltfExtensionUsed("KHR_gaussian_splatting")).toBe(true); + expect(tileset.isGltfExtensionRequired("KHR_gaussian_splatting")).toBe( + true, + ); + expect( - tileset.isGltfExtensionUsed("KHR_spz_gaussian_splats_compression"), + tileset.isGltfExtensionUsed("KHR_gaussian_splatting_compression_spz_2"), ).toBe(true); expect( - tileset.isGltfExtensionRequired("KHR_spz_gaussian_splats_compression"), + tileset.isGltfExtensionRequired( + "KHR_gaussian_splatting_compression_spz_2", + ), ).toBe(true); const tile = await Cesium3DTilesTester.waitForTileContentReady( @@ -84,10 +91,10 @@ describe( expect(tile.content instanceof GaussianSplat3DTileContent).toBe(true); }); - xit("loads a Gaussian splats tileset and toggles visibility", async function () { + it("loads a Gaussian splats tileset and toggles visibility", async function () { const tileset = await Cesium3DTilesTester.loadTileset( scene, - tilesetUrl, + sphericalHarmonicUrl, options, ); scene.camera.lookAt( @@ -107,17 +114,22 @@ describe( await pollToPromise(function () { scene.renderForSpecs(); - return gsPrim._hasGaussianSplatTexture; + return gsPrim._dirty === false && gsPrim._sorterPromise === undefined; }); - - await pollToPromise(function () { - scene.renderForSpecs(); - return gsPrim._dirty === false && gsPrim._sorterState === 0; + scene.renderForSpecs(); + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba[samplePosition + 0]).not.toBe(0); + expect(rgba[samplePosition + 1]).not.toBe(0); + expect(rgba[samplePosition + 2]).not.toBe(0); }); - expect(scene).notToRender([0, 0, 0, 255]); tileset.show = false; - expect(scene).toRender([0, 0, 0, 255]); + scene.renderForSpecs(); + expect(scene).toRenderAndCall(function (rgba) { + expect(rgba[samplePosition + 0]).toBe(0); + expect(rgba[samplePosition + 1]).toBe(0); + expect(rgba[samplePosition + 2]).toBe(0); + }); }); it("Check Spherical Harmonic specular on a Gaussian splats tileset", async function () { @@ -147,8 +159,7 @@ describe( const targetYellow = { red: 189, green: 173, blue: 97 }; const targetPurple = { red: 127, green: 80, blue: 141 }; - const samplePosition = - ((canvassize.width / 2) * canvassize.height + canvassize.width / 2) * 4; + tileset.show = true; const enu = Transforms.eastNorthUpToFixedFrame(boundingSphere.center); @@ -159,13 +170,12 @@ describe( const gsPrim = tileset.gaussianSplatPrimitive; await pollToPromise(function () { scene.renderForSpecs(); - return gsPrim._hasGaussianSplatTexture; + return gsPrim._dirty === false && gsPrim._sorterPromise === undefined; }); - await pollToPromise(function () { + for (let i = 0; i < 100; ++i) { scene.renderForSpecs(); - return gsPrim._dirty === false && gsPrim._sorterState === 0; - }); + } scene.renderForSpecs(); expect(scene).toRenderAndCall(function (rgba) { @@ -177,12 +187,6 @@ describe( scene.camera.lookAtTransform(enu, orangeish); await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root); - - await pollToPromise(function () { - scene.renderForSpecs(); - return gsPrim._hasGaussianSplatTexture; - }); - await pollToPromise(function () { scene.renderForSpecs(); return gsPrim._dirty === false && gsPrim._sorterState === 0; @@ -197,12 +201,6 @@ describe( scene.camera.lookAtTransform(enu, purplish); await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root); - - await pollToPromise(function () { - scene.renderForSpecs(); - return gsPrim._hasGaussianSplatTexture; - }); - await pollToPromise(function () { scene.renderForSpecs(); return gsPrim._dirty === false && gsPrim._sorterState === 0; From c3cc59ce782d46f51cffe5a61276759468fef1e4 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Thu, 28 Aug 2025 17:39:01 -0500 Subject: [PATCH 13/22] Spherical harmonic naming clarity. Moved read only properties to private and added getters. --- .../Scene/GaussianSplat3DTileContent.js | 60 ++++++++++++++++--- .../Source/Scene/GaussianSplatPrimitive.js | 25 +++++--- .../Source/Scene/GltfVertexBufferLoader.js | 4 +- .../Shaders/PrimitiveGaussianSplatVS.glsl | 8 +-- 4 files changed, 73 insertions(+), 24 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 834f9e8b3bfa..d2f991ad10f0 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -85,14 +85,21 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) { * @type {number} * @private */ - this.shDegree = 0; + this._sphericalHarmonicsDegree = 0; /** * The number of spherical harmonic coefficients used for the Gaussian splats. * @type {number} * @private */ - this.shCoefficientCount = 0; + this._sphericalHarmonicsCoefficientCount = 0; + + /** + * Spherical Harmonic data that has been packed for use in a texture or shader. + * @type {undefined|Uint32Array} + * @private + */ + this._packedSphericalHarmonicsData = undefined; } Object.defineProperties(GaussianSplat3DTileContent.prototype, { @@ -324,6 +331,41 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { this._group = value; }, }, + + /** + * 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; + }, + }, }); GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt = function (tileset) { @@ -444,12 +486,12 @@ function extractSHDegreeAndCoef(attribute) { /** * Packs spherical harmonic data into half-precision floats. * @param {*} data - The input data to pack. - * @param {*} shDegree - The spherical harmonic degree. + * @param {*} sphericalHarmonicsDegree - The spherical harmonic degree. * @returns {Uint32Array} - The packed data. */ -function packSphericalHarmonicData(tileContent) { - const degree = tileContent.shDegree; - const coefs = tileContent.shCoefficientCount; +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); @@ -602,10 +644,10 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) { const { l, n } = degreeAndCoefFromAttributes( this.splatPrimitive.attributes, ); - this.shDegree = l; - this.shCoefficientCount = n; + this._sphericalHarmonicsDegree = l; + this._sphericalHarmonicsCoefficientCount = n; - this._packedShData = packSphericalHarmonicData(this); + this._packedSphericalHarmonicsData = packSphericalHarmonicsData(this); return; } diff --git a/packages/engine/Source/Scene/GaussianSplatPrimitive.js b/packages/engine/Source/Scene/GaussianSplatPrimitive.js index c79704f78024..3e1e235d5908 100644 --- a/packages/engine/Source/Scene/GaussianSplatPrimitive.js +++ b/packages/engine/Source/Scene/GaussianSplatPrimitive.js @@ -630,7 +630,11 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function ( ShaderDestination.VERTEX, ); - shaderBuilder.addUniform("float", "u_shDegree", ShaderDestination.VERTEX); + shaderBuilder.addUniform( + "float", + "u_sphericalHarmonicsDegree", + ShaderDestination.VERTEX, + ); shaderBuilder.addUniform("float", "u_splatScale", ShaderDestination.VERTEX); @@ -652,7 +656,7 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function ( return primitive.gaussianSplatTexture; }; - if (primitive._shDegree > 0) { + if (primitive._sphericalHarmonicsDegree > 0) { shaderBuilder.addDefine( "HAS_SPHERICAL_HARMONICS", "1", @@ -667,8 +671,8 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function ( return primitive.gaussianSplatSHTexture; }; } - uniformMap.u_shDegree = function () { - return primitive._shDegree; + uniformMap.u_sphericalHarmonicsDegree = function () { + return primitive._sphericalHarmonicsDegree; }; uniformMap.u_cameraPositionWC = function () { @@ -872,11 +876,11 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { const aggregateShData = () => { let offset = 0; for (const tile of tiles) { - const shData = tile.content._packedShData; - if (tile.content.shDegree > 0) { + const shData = tile.content.packedSphericalHarmonicsData; + if (tile.content.sphericalHarmonicsDegree > 0) { if (!defined(this._shData)) { let coefs; - switch (tile.content.shDegree) { + switch (tile.content.sphericalHarmonicsDegree) { case 1: coefs = 9; break; @@ -931,7 +935,8 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { ); aggregateShData(); - this._shDegree = tiles[0].content.shDegree; + this._sphericalHarmonicsDegree = + tiles[0].content.sphericalHarmonicsDegree; this._numSplats = totalElements; this.selectedTileLength = tileset._selectedTiles.length; @@ -947,7 +952,9 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { if (defined(this._shData)) { const oldTex = this.gaussianSplatSHTexture; const width = ContextLimits.maximumTextureSize; - const dims = tileset._selectedTiles[0].content.shCoefficientCount / 3; + const dims = + tileset._selectedTiles[0].content + .sphericalHarmonicsCoefficientCount / 3; const splatsPerRow = Math.floor(width / dims); const floatsPerRow = splatsPerRow * (dims * 2); const texBuf = new Uint32Array( diff --git a/packages/engine/Source/Scene/GltfVertexBufferLoader.js b/packages/engine/Source/Scene/GltfVertexBufferLoader.js index bb69567d2500..b40f51e46df3 100644 --- a/packages/engine/Source/Scene/GltfVertexBufferLoader.js +++ b/packages/engine/Source/Scene/GltfVertexBufferLoader.js @@ -374,10 +374,10 @@ function processSpz(vertexBufferLoader) { const { l, n } = extractSHDegreeAndCoef( vertexBufferLoader._attributeSemantic, ); - const shDegree = gcloudData.shDegree; + const sphericalHarmonicDegree = gcloudData.shDegree; let stride = 0; const base = [0, 9, 24]; - switch (shDegree) { + switch (sphericalHarmonicDegree) { case 1: stride = 9; break; diff --git a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl index 6096b4d93545..90a221bf324d 100644 --- a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl +++ b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl @@ -31,7 +31,7 @@ const float SH_C3[7] = float[7]( //Retrieve SH coefficient. Currently RG32UI format uvec2 loadSHCoeff(uint splatID, int index) { ivec2 shTexSize = textureSize(u_gaussianSplatSHTexture, 0); - uint dims = coefficientCount[uint(u_shDegree)-1u]; + uint dims = coefficientCount[uint(u_sphericalHarmonicsDegree)-1u]; uint splatsPerRow = uint(shTexSize.x) / dims; uint shIndex = (splatID%splatsPerRow) * dims + uint(index); ivec2 shPosCoord = ivec2(shIndex, splatID / splatsPerRow); @@ -53,13 +53,13 @@ vec3 evaluateSH(uint splatID, vec3 viewDir) { int coeffIndex = 0; float x = viewDir.x, y = viewDir.y, z = viewDir.z; - if (u_shDegree >= 1.) { + if (u_sphericalHarmonicsDegree >= 1.) { vec3 sh1 = loadAndExpandSHCoeff(splatID, coeffIndex++); vec3 sh2 = loadAndExpandSHCoeff(splatID, coeffIndex++); vec3 sh3 = loadAndExpandSHCoeff(splatID, coeffIndex++); result += -SH_C1 * y * sh1 + SH_C1 * z * sh2 - SH_C1 * x * sh3; - if (u_shDegree >= 2.) { + if (u_sphericalHarmonicsDegree >= 2.) { float xx = x * x; float yy = y * y; float zz = z * z; @@ -78,7 +78,7 @@ vec3 evaluateSH(uint splatID, vec3 viewDir) { SH_C2[3] * xz * sh7 + SH_C2[4] * (xx - yy) * sh8; - if (u_shDegree >= 3.) { + if (u_sphericalHarmonicsDegree >= 3.) { vec3 sh9 = loadAndExpandSHCoeff(splatID, coeffIndex++); vec3 sh10 = loadAndExpandSHCoeff(splatID, coeffIndex++); vec3 sh11 = loadAndExpandSHCoeff(splatID, coeffIndex++); From a18a935299e0ae28a5228ab4bd515b6aebcd22cc Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:17:09 -0500 Subject: [PATCH 14/22] comment cleanup --- packages/engine/Source/Scene/VertexAttributeSemantic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/VertexAttributeSemantic.js b/packages/engine/Source/Scene/VertexAttributeSemantic.js index 2834a4bc1648..2403a75eb4bb 100644 --- a/packages/engine/Source/Scene/VertexAttributeSemantic.js +++ b/packages/engine/Source/Scene/VertexAttributeSemantic.js @@ -286,7 +286,7 @@ VertexAttributeSemantic.getGlslType = function (semantic) { * @param {VertexAttributeSemantic} semantic The semantic. * @param {number} [setIndex] The set index. * - * @returns {string} The varia_SCALEble name. + * @returns {string} The variable name. * * @private */ From a0baf6cdea4ee3390ceece6140c3248dbc615951 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:31:15 -0500 Subject: [PATCH 15/22] rename gaussianSplatSHTexture sphericalHarmonicsTexture Fix param list description --- .../Scene/GaussianSplat3DTileContent.js | 35 ++----------------- .../Source/Scene/GaussianSplatPrimitive.js | 14 ++++---- .../Shaders/PrimitiveGaussianSplatVS.glsl | 4 +-- 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 6bd80d4eea7a..6441428317a2 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -405,34 +405,6 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { }, }); -GaussianSplat3DTileContent.tilesetHasGaussianSplattingExt = function (tileset) { - let hasGaussianSplatExtension = false; - let hasLegacyGaussianSplatExtension = false; - if (tileset.isGltfExtensionRequired instanceof Function) { - hasGaussianSplatExtension = - tileset.isGltfExtensionRequired("KHR_gaussian_splatting") && - tileset.isGltfExtensionRequired( - "KHR_gaussian_splatting_compression_spz_2", - ); - - hasLegacyGaussianSplatExtension = tileset.isGltfExtensionRequired( - "KHR_spz_gaussian_splats_compression", - ); - } - - if (hasLegacyGaussianSplatExtension) { - deprecationWarning( - "KHR_spz_gaussian_splats_compression", - "Support for the original KHR_spz_gaussian_splats_compression extension has been deprecated in favor " + - "of the up to date KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 extensions and will be " + - "removed in CesiumJS 1.134.\n\nPlease retile your tileset with the KHR_gaussian_splatting and " + - "KHR_gaussian_splatting_compression_spz_2 extensions.", - ); - } - - return hasGaussianSplatExtension || hasLegacyGaussianSplatExtension; -}; - function getShAttributePrefix(attribute) { const prefix = attribute.startsWith("KHR_gaussian_splatting:") ? "KHR_gaussian_splatting:" @@ -503,7 +475,7 @@ function float32ToFloat16(float32) { /** * Extracts the spherical harmonic degree and coefficient from the attribute name. - * @param {String} attribute - The attribute name. + * @param {string} attribute - The attribute name. * @returns {Object} An object containing the degree (l) and coefficient (n). * @private */ @@ -522,9 +494,8 @@ function extractSHDegreeAndCoef(attribute) { /** * Packs spherical harmonic data into half-precision floats. - * @param {*} data - The input data to pack. - * @param {*} sphericalHarmonicsDegree - The spherical harmonic degree. - * @returns {Uint32Array} - The packed data. + * @param {GaussianSplat3DTileContent} tileContent - The tile content containing the spherical harmonic data. + * @returns {Uint32Array} - The Float16 packed spherical harmonic data. */ function packSphericalHarmonicsData(tileContent) { const degree = tileContent.sphericalHarmonicsDegree; diff --git a/packages/engine/Source/Scene/GaussianSplatPrimitive.js b/packages/engine/Source/Scene/GaussianSplatPrimitive.js index 8d6b2a6107e1..8aed06385980 100644 --- a/packages/engine/Source/Scene/GaussianSplatPrimitive.js +++ b/packages/engine/Source/Scene/GaussianSplatPrimitive.js @@ -46,7 +46,7 @@ const GaussianSplatSortingState = { ERROR: 4, }; -function createGaussianSplatSHTexture(context, shData) { +function createSphericalHarmonicsTexture(context, shData) { const texture = new Texture({ context: context, source: { @@ -175,7 +175,7 @@ function GaussianSplatPrimitive(options) { * @type {undefined|Texture} * @private */ - this.gaussianSplatSHTexture = undefined; + this.sphericalHarmonicsTexture = undefined; /** * The last width of the Gaussian splat texture. @@ -664,11 +664,11 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function ( ); shaderBuilder.addUniform( "highp usampler2D", - "u_gaussianSplatSHTexture", + "u_sphericalHarmonicsTexture", ShaderDestination.VERTEX, ); - uniformMap.u_gaussianSplatSHTexture = function () { - return primitive.gaussianSplatSHTexture; + uniformMap.u_sphericalHarmonicsTexture = function () { + return primitive.sphericalHarmonicsTexture; }; } uniformMap.u_sphericalHarmonicsDegree = function () { @@ -950,7 +950,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { if (!this._gaussianSplatTexturePending) { GaussianSplatPrimitive.generateSplatTexture(this, frameState); if (defined(this._shData)) { - const oldTex = this.gaussianSplatSHTexture; + const oldTex = this.sphericalHarmonicsTexture; const width = ContextLimits.maximumTextureSize; const dims = tileset._selectedTiles[0].content @@ -969,7 +969,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { ); dataIndex += floatsPerRow; } - this.gaussianSplatSHTexture = createGaussianSplatSHTexture( + this.sphericalHarmonicsTexture = createSphericalHarmonicsTexture( frameState.context, { data: texBuf, diff --git a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl index 90a221bf324d..689be0903087 100644 --- a/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl +++ b/packages/engine/Source/Shaders/PrimitiveGaussianSplatVS.glsl @@ -30,12 +30,12 @@ const float SH_C3[7] = float[7]( //Retrieve SH coefficient. Currently RG32UI format uvec2 loadSHCoeff(uint splatID, int index) { - ivec2 shTexSize = textureSize(u_gaussianSplatSHTexture, 0); + ivec2 shTexSize = textureSize(u_sphericalHarmonicsTexture, 0); uint dims = coefficientCount[uint(u_sphericalHarmonicsDegree)-1u]; uint splatsPerRow = uint(shTexSize.x) / dims; uint shIndex = (splatID%splatsPerRow) * dims + uint(index); ivec2 shPosCoord = ivec2(shIndex, splatID / splatsPerRow); - return texelFetch(u_gaussianSplatSHTexture, shPosCoord, 0).rg; + return texelFetch(u_sphericalHarmonicsTexture, shPosCoord, 0).rg; } //Unpack RG32UI half float coefficients to vec3 From b725db44748873cfa743ba4fc36f3688e7ce347f Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:33:16 -0500 Subject: [PATCH 16/22] param description --- packages/engine/Source/Scene/GaussianSplat3DTileContent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 6441428317a2..be846179176c 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -437,7 +437,7 @@ function degreeAndCoefFromAttributes(attributes) { /** * Converts a 32-bit floating point number to a 16-bit floating point number. - * @param {Float} float32 input + * @param {number} float32 input * @returns {number} Half precision float * @private */ @@ -476,7 +476,7 @@ function float32ToFloat16(float32) { /** * 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). + * @returns {object} An object containing the degree (l) and coefficient (n). * @private */ function extractSHDegreeAndCoef(attribute) { From 2836299a09cc586c1c5693e1baca075935fe3d7c Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:33:59 -0500 Subject: [PATCH 17/22] object param case --- packages/engine/Source/Scene/GaussianSplat3DTileContent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index be846179176c..8a38647519ea 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -415,7 +415,7 @@ function getShAttributePrefix(attribute) { /** * Determine Spherical Harmonics degree and coefficient count from attributes * @param {Array} attributes - The list of attributes. - * @returns {Object} An object containing the degree (l) and coefficient (n). + * @returns {object} An object containing the degree (l) and coefficient (n). */ function degreeAndCoefFromAttributes(attributes) { const shAttributes = attributes.filter((attr) => From 38bba9a53ff2a48a0a7b10a1df7558f7f4d271fd Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:35:34 -0500 Subject: [PATCH 18/22] VertexAttributeSemantic param description --- packages/engine/Source/Scene/GaussianSplat3DTileContent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 8a38647519ea..d92f8c26cab9 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -414,7 +414,7 @@ function getShAttributePrefix(attribute) { /** * Determine Spherical Harmonics degree and coefficient count from attributes - * @param {Array} attributes - The list of attributes. + * @param {VertexAttributeSemantic[]} attributes - The list of attributes. * @returns {object} An object containing the degree (l) and coefficient (n). */ function degreeAndCoefFromAttributes(attributes) { From 032fb1f4106ff388e6ffbfd09563f058ab46004f Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:42:03 -0500 Subject: [PATCH 19/22] renamed splatPrimitive to gltfPrimitive for clarity --- .../Scene/GaussianSplat3DTileContent.js | 20 +++++++------- .../Source/Scene/GaussianSplatPrimitive.js | 26 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index d92f8c26cab9..530802e599c2 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -47,7 +47,7 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) { * @type {undefined|Primitive} * @private */ - this.splatPrimitive = undefined; + this.gltfPrimitive = undefined; /** * Transform matrix to turn model coordinates into world coordinates. @@ -165,7 +165,7 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { */ pointsLength: { get: function () { - return this.splatPrimitive.attributes[0].count; + return this.gltfPrimitive.attributes[0].count; }, }, /** @@ -190,7 +190,7 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { */ geometryByteLength: { get: function () { - return this.splatPrimitive.attributes.reduce((totalLength, attribute) => { + return this.gltfPrimitive.attributes.reduce((totalLength, attribute) => { return totalLength + attribute.byteLength; }, 0); }, @@ -503,7 +503,7 @@ function packSphericalHarmonicsData(tileContent) { const totalLength = tileContent.pointsLength * (coefs * (2 / 3)); //3 packs into 2 const packedData = new Uint32Array(totalLength); - const shAttributes = tileContent.splatPrimitive.attributes.filter((attr) => + const shAttributes = tileContent.gltfPrimitive.attributes.filter((attr) => attr.name.includes("SH_DEGREE_"), ); let stride = 0; @@ -624,34 +624,32 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) { } if (this._resourcesLoaded) { - this.splatPrimitive = loader.components.scene.nodes[0].primitives[0]; + this.gltfPrimitive = loader.components.scene.nodes[0].primitives[0]; this.worldTransform = loader.components.scene.nodes[0].matrix; this._ready = true; this._originalPositions = new Float32Array( ModelUtility.getAttributeBySemantic( - this.splatPrimitive, + this.gltfPrimitive, VertexAttributeSemantic.POSITION, ).typedArray, ); this._originalRotations = new Float32Array( ModelUtility.getAttributeBySemantic( - this.splatPrimitive, + this.gltfPrimitive, VertexAttributeSemantic.ROTATION, ).typedArray, ); this._originalScales = new Float32Array( ModelUtility.getAttributeBySemantic( - this.splatPrimitive, + this.gltfPrimitive, VertexAttributeSemantic.SCALE, ).typedArray, ); - const { l, n } = degreeAndCoefFromAttributes( - this.splatPrimitive.attributes, - ); + const { l, n } = degreeAndCoefFromAttributes(this.gltfPrimitive.attributes); this._sphericalHarmonicsDegree = l; this._sphericalHarmonicsCoefficientCount = n; diff --git a/packages/engine/Source/Scene/GaussianSplatPrimitive.js b/packages/engine/Source/Scene/GaussianSplatPrimitive.js index 8aed06385980..7e9a1a33cce4 100644 --- a/packages/engine/Source/Scene/GaussianSplatPrimitive.js +++ b/packages/engine/Source/Scene/GaussianSplatPrimitive.js @@ -424,7 +424,7 @@ GaussianSplatPrimitive.prototype.onTileVisible = function (tile) {}; */ GaussianSplatPrimitive.transformTile = function (tile) { const computedTransform = tile.computedTransform; - const splatPrimitive = tile.content.splatPrimitive; + const gltfPrimitive = tile.content.gltfPrimitive; const gaussianSplatPrimitive = tile.tileset.gaussianSplatPrimitive; const computedModelMatrix = Matrix4.multiplyTransformation( @@ -454,17 +454,17 @@ GaussianSplatPrimitive.transformTile = function (tile) { const rotations = tile.content._originalRotations; const scales = tile.content._originalScales; const attributePositions = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.POSITION, ).typedArray; const attributeRotations = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.ROTATION, ).typedArray; const attributeScales = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.SCALE, ).typedArray; @@ -858,7 +858,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { let aggregate; let offset = 0; for (const tile of tiles) { - const primitive = tile.content.splatPrimitive; + const primitive = tile.content.gltfPrimitive; const attribute = getAttributeCallback(primitive); if (!defined(aggregate)) { aggregate = ComponentDatatype.createTypedArray( @@ -900,36 +900,36 @@ GaussianSplatPrimitive.prototype.update = function (frameState) { this._positions = aggregateAttributeValues( ComponentDatatype.FLOAT, - (splatPrimitive) => + (gltfPrimitive) => ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.POSITION, ), ); this._scales = aggregateAttributeValues( ComponentDatatype.FLOAT, - (splatPrimitive) => + (gltfPrimitive) => ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.SCALE, ), ); this._rotations = aggregateAttributeValues( ComponentDatatype.FLOAT, - (splatPrimitive) => + (gltfPrimitive) => ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.ROTATION, ), ); this._colors = aggregateAttributeValues( ComponentDatatype.UNSIGNED_BYTE, - (splatPrimitive) => + (gltfPrimitive) => ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.COLOR, ), ); From ae0adcc956862a82ce6652e08834499089a1567e Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Fri, 29 Aug 2025 13:52:38 -0500 Subject: [PATCH 20/22] Fix bad merge --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d1146ced4c66..8f77ff90592a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,10 @@ - Added support for the [EXT_mesh_primitive_restart](https://github.com/KhronosGroup/glTF/pull/2478) glTF extension. [#12764](https://github.com/CesiumGS/cesium/issues/12764) - Added spherical harmonics support for Gaussian splats. Currently supported with the SPZ compression format. +#### Deprecated :hourglass_flowing_sand: + +- Deprecated support of the `KHR_spz_gaussian_splats_compression` extension in favor of the latest 3D Gaussian Splatting extensions for glTF, `KHR_gaussian_splatting` and `KHR_gaussian_splatting_compression_spz_2`. The deprecated extension will be removed in version 1.135. Please retile your existing 3D Tiles using Gaussian splatting before that time. [#12837](https://github.com/CesiumGS/cesium/issues/12837) + ## 1.132 - 2025-08-01 ### @cesium/engine From 7998433a47dd785d64621f2ec040cfdf114e50e7 Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Tue, 2 Sep 2025 10:31:43 -0500 Subject: [PATCH 21/22] coverage fix --- .../Scene/GaussianSplat3DTileContentSpec.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/engine/Specs/Scene/GaussianSplat3DTileContentSpec.js b/packages/engine/Specs/Scene/GaussianSplat3DTileContentSpec.js index ff3a9a68e561..e2e69816cbb3 100644 --- a/packages/engine/Specs/Scene/GaussianSplat3DTileContentSpec.js +++ b/packages/engine/Specs/Scene/GaussianSplat3DTileContentSpec.js @@ -65,26 +65,26 @@ describe( expect(content).toBeDefined(); expect(content instanceof GaussianSplat3DTileContent).toBe(true); - const splatPrimitive = content.splatPrimitive; - expect(splatPrimitive).toBeDefined(); - expect(splatPrimitive.attributes.length).toBeGreaterThan(0); + const gltfPrimitive = content.gltfPrimitive; + expect(gltfPrimitive).toBeDefined(); + expect(gltfPrimitive.attributes.length).toBeGreaterThan(0); const positions = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.POSITION, ).typedArray; const rotations = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.ROTATION, ).typedArray; const scales = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.SCALE, ).typedArray; const colors = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.COLOR, ).typedArray; @@ -174,26 +174,26 @@ describe( expect(content).toBeDefined(); expect(content instanceof GaussianSplat3DTileContent).toBe(true); - const splatPrimitive = content.splatPrimitive; - expect(splatPrimitive).toBeDefined(); - expect(splatPrimitive.attributes.length).toBeGreaterThan(0); + const gltfPrimitive = content.gltfPrimitive; + expect(gltfPrimitive).toBeDefined(); + expect(gltfPrimitive.attributes.length).toBeGreaterThan(0); const positions = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.POSITION, ).typedArray; const rotations = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.ROTATION, ).typedArray; const scales = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.SCALE, ).typedArray; const colors = ModelUtility.getAttributeBySemantic( - splatPrimitive, + gltfPrimitive, VertexAttributeSemantic.COLOR, ).typedArray; From 59d875727082c18af35eee2f0986b776841cab8a Mon Sep 17 00:00:00 2001 From: keyboardspecialist Date: Tue, 2 Sep 2025 11:35:46 -0500 Subject: [PATCH 22/22] marked functions private that should be. jsdoc fix --- packages/engine/Source/Scene/GaussianSplat3DTileContent.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js index 530802e599c2..ea93b2b8a1cf 100644 --- a/packages/engine/Source/Scene/GaussianSplat3DTileContent.js +++ b/packages/engine/Source/Scene/GaussianSplat3DTileContent.js @@ -17,7 +17,6 @@ import deprecationWarning from "../Core/deprecationWarning.js"; * * @alias GaussianSplat3DTileContent * @constructor - * @private */ function GaussianSplat3DTileContent(loader, tileset, tile, resource) { this._tileset = tileset; @@ -414,8 +413,9 @@ function getShAttributePrefix(attribute) { /** * Determine Spherical Harmonics degree and coefficient count from attributes - * @param {VertexAttributeSemantic[]} attributes - The list of 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) => @@ -496,6 +496,7 @@ function extractSHDegreeAndCoef(attribute) { * 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;