Summary
On Cesium for Unreal v2.29.0 (UE 5.8.1, Linux, headless -game + Movie Render Queue), a Gaussian splat tileset's spherical-harmonic bands 1–3 have no effect on the rendered image. The SH data is present in the tiles, the loader accepts it without warnings, the compute shader contains the evaluation, and the frame is nevertheless identical to one rendered with --sh 0.
I could not locate the break in the ~50 minutes I had, so this is a measurement report rather than a diagnosis. Everything I ruled out is listed below so nobody repeats it.
How it was measured
The subtle part is that at a training viewpoint the real band-1 contribution is only 1–2 levels out of 255, which is below what a visual check or even a naive pixel diff will reliably show. So the test amplifies it:
- Tile the same PLY at
--sh 3, --sh 1 and --sh 0, render the same camera → frames differ by ≤ 1/255.
- Multiply every
f_rest_* coefficient in the PLY by 8 (band-1 mean |c| 0.039 → 0.31, i.e. a ~40-level colour swing at any view direction), re-tile, render the same camera → max Δ 2/255, 0.0 % of pixels differing by more than 8.
An 8× amplification of the view-dependent term producing a 2/255 change means the term is not reaching the output at all.
What was verified (and is therefore not the cause)
| Link |
How it was checked |
Result |
| SH survives PLY → SPZ tiling |
gunzipped bufferView 0 of tiles/4/9.glb; SH block is the trailing n×45 bytes, decoded uint8 − 128 |
mean 3.5/128 ≈ 0.027 ✓; the ×8 copy decodes to 27.8 ✓ |
| glTF declares the accessors |
KHR_gaussian_splatting: SH_DEGREE_{1,2,3}_COEF_*, float, placeholder accessors without bufferView (SPZ supplies the data) |
present |
| Accessor type mismatch |
Accessors are declared VEC4 by the tiler, while the loader reads AccessorView<glm::vec3>. Patched the tiler to emit VEC3, re-tiled the ×8 PLY, re-rendered |
no change — not the cause (patch reverted) |
| Loader |
CesiumGltfGaussianSplatComponent.cpp, countShCoeffsOnPrimitive / writeShCoeffs — these warn on a missing attribute or an invalid accessor |
no SH warnings in any render log |
| GPU upload |
CesiumGaussianSplatDataInterface.cpp, updatePerSplatData |
uploads SphericalHarmonics plus per-tile (count, offset, firstSplat) |
| Compute shader |
Shaders/Private/CesiumGaussianSplatCompute.usf ~176–242 |
evaluates bands 1–3 with the graphdeco basis, unconditionally, OutColor = InColor + SHColor |
| Niagara binding |
GaussianSplatSystem.uasset |
compiled script calls ComputeSplat(..., OutColor, ...); the DI is GPUComputeSim only, so there is no CPU fallback path to blame |
Remaining candidates
Not investigated, in rough order of my suspicion:
- The
CameraPosition input the Niagara graph feeds the shader (Result2 / Result5) — if the view vector is wrong or zero, the SH evaluation would collapse to a constant.
- The element type the SPZ decoder actually produces inside cesium-native, versus what the accessor claims.
- The
Colors + SH sum being clamped or overwritten before it reaches the material.
Separately, and probably worth its own issue
In updatePerSplatData the SH buffer is allocated with × 2 × sizeof(float) but initialised and locked at a 16-byte stride, which appears to force a re-allocation on every update. It reads as a performance bug rather than a functional one, and it is not the cause of the above — noting it here so it is not lost. Happy to file it separately if you would prefer.
Impact
View-dependent shading — specular sheen on glass and water, the shift in appearance as a camera orbits — does not appear in Unreal renders from a Cesium splat tileset today. For matte subjects (brick, terrain) this is barely noticeable, which is probably why it has gone unreported; for glazing it reads visibly flat.
Environment
- Cesium for Unreal v2.29.0, prebuilt Linux binaries, dropped into
Engine/Plugins/Marketplace
- Unreal Engine 5.8.1, prebuilt Linux editor, Vulkan
- Linux x86_64 (Fedora, kernel 7.1.8), NVIDIA GTX 1660 6 GB
- Headless
-game launch driving Movie Render Queue (no editor session)
- Tileset produced locally with
splat-transform 3.3.0 → 3dgs-ply-3dtiles-converter 0.6.2 (3D Tiles 1.1, KHR_gaussian_splatting + _compression_spz_2), no Cesium ion involved
- Source splat trained in Brush (10k steps, ~501k splats, 3 SH bands)
I can supply the tileset, the ×8-amplified variant, and the render logs if they would help.
Summary
On Cesium for Unreal v2.29.0 (UE 5.8.1, Linux, headless
-game+ Movie Render Queue), a Gaussian splat tileset's spherical-harmonic bands 1–3 have no effect on the rendered image. The SH data is present in the tiles, the loader accepts it without warnings, the compute shader contains the evaluation, and the frame is nevertheless identical to one rendered with--sh 0.I could not locate the break in the ~50 minutes I had, so this is a measurement report rather than a diagnosis. Everything I ruled out is listed below so nobody repeats it.
How it was measured
The subtle part is that at a training viewpoint the real band-1 contribution is only 1–2 levels out of 255, which is below what a visual check or even a naive pixel diff will reliably show. So the test amplifies it:
--sh 3,--sh 1and--sh 0, render the same camera → frames differ by ≤ 1/255.f_rest_*coefficient in the PLY by 8 (band-1 mean |c| 0.039 → 0.31, i.e. a ~40-level colour swing at any view direction), re-tile, render the same camera → max Δ 2/255, 0.0 % of pixels differing by more than 8.An 8× amplification of the view-dependent term producing a 2/255 change means the term is not reaching the output at all.
What was verified (and is therefore not the cause)
bufferView 0oftiles/4/9.glb; SH block is the trailingn×45bytes, decodeduint8 − 128KHR_gaussian_splatting: SH_DEGREE_{1,2,3}_COEF_*, float, placeholder accessors withoutbufferView(SPZ supplies the data)AccessorView<glm::vec3>. Patched the tiler to emit VEC3, re-tiled the ×8 PLY, re-renderedCesiumGltfGaussianSplatComponent.cpp,countShCoeffsOnPrimitive/writeShCoeffs— these warn on a missing attribute or an invalid accessorCesiumGaussianSplatDataInterface.cpp,updatePerSplatDataSphericalHarmonicsplus per-tile(count, offset, firstSplat)Shaders/Private/CesiumGaussianSplatCompute.usf~176–242OutColor = InColor + SHColorGaussianSplatSystem.uassetComputeSplat(..., OutColor, ...); the DI isGPUComputeSimonly, so there is no CPU fallback path to blameRemaining candidates
Not investigated, in rough order of my suspicion:
CameraPositioninput the Niagara graph feeds the shader (Result2/Result5) — if the view vector is wrong or zero, the SH evaluation would collapse to a constant.Colors + SHsum being clamped or overwritten before it reaches the material.Separately, and probably worth its own issue
In
updatePerSplatDatathe SH buffer is allocated with× 2 × sizeof(float)but initialised and locked at a 16-byte stride, which appears to force a re-allocation on every update. It reads as a performance bug rather than a functional one, and it is not the cause of the above — noting it here so it is not lost. Happy to file it separately if you would prefer.Impact
View-dependent shading — specular sheen on glass and water, the shift in appearance as a camera orbits — does not appear in Unreal renders from a Cesium splat tileset today. For matte subjects (brick, terrain) this is barely noticeable, which is probably why it has gone unreported; for glazing it reads visibly flat.
Environment
Engine/Plugins/Marketplace-gamelaunch driving Movie Render Queue (no editor session)splat-transform3.3.0 →3dgs-ply-3dtiles-converter0.6.2 (3D Tiles 1.1,KHR_gaussian_splatting+_compression_spz_2), no Cesium ion involvedI can supply the tileset, the ×8-amplified variant, and the render logs if they would help.