I followed your suggestion and modified the way I generate the scale values. The previous issue has now been resolved. Thank you so much!
However, I’ve encountered a new issue while converting my data from .splat to .glb for rendering in Cesium using the KHR_gaussian_splatting_compression_spz_2 extension.
The issue is that the rendered result in Cesium doesn’t look as expected — it doesn’t show the correct Gaussian Splatting effect. I’m not sure which attribute might be causing the rendering issue.
Below are my abnormal rendering result.
My guesses are: scale, alpha, color, rotation, or sh (spherical harmonics?).
I’ll attach my code below.
Could you please help me analyze which attribute might be incorrectly converted, and how I can fix it?
Here are my code.
def splat_to_gltf_with_gaussian_extension(points: List[Point], output_path: str):
# --- Extract data ---
positions = np.array([p.position for p in points], dtype=np.float32)
colors = np.array([p.color for p in points], dtype=np.uint8) # RGBA bytes
scales = np.array([p.scale for p in points], dtype=np.float32)
rotations = np.array([p.rotation for p in points], dtype=np.uint8) # quaternion bytes (w,x,y,z)
sh_coeffs_list = np.array([p.sh_coeffs for p in points], dtype=np.float32) # shape: (N, 15, 3)
positions = positions[:, [0, 2, 1]] # Swap Y and Z
# --- Convert to SPZ format ---
gaussian_cloud = spz.GaussianCloud()
# 1. Process positions (placed first as required)
gaussian_cloud.positions = np.ascontiguousarray(positions.reshape(-1), dtype=np.float32)
# 2. Process colors
colors_rgb = colors[:, :3].astype(np.float32) / 255.0 # Normalize to [0,1]
colors_rgb = 0.282095 * colors_rgb # Apply spherical harmonics constant
gaussian_cloud.colors = np.ascontiguousarray(colors_rgb.reshape(-1), dtype=np.float32)
# 3. Process Alpha
# Alpha processing - store logit values (sigmoid input)
alphas_raw = colors[:, 3].astype(np.float32) / 255.0 # Normalize to [0,1]
alphas_clamped = np.clip(alphas_raw, 1e-6, 1 - 1e-6)
alphas_logit = np.log(alphas_clamped / (1 - alphas_clamped))
gaussian_cloud.alphas = np.ascontiguousarray(alphas_logit.reshape(-1), dtype=np.float32)
# 4. Process scales
# Fix: .splat files store exp(scale) values, but GLB needs log(scale) values
# Since the renderer applies exp() transformation, we need to apply log() here
scales = np.log(np.maximum(scales, 1e-10)) # Avoid log(0)
gaussian_cloud.scales = np.ascontiguousarray(scales.reshape(-1), dtype=np.float32)
# 5. Process rotations
# 5.1: Adjust quaternion order [w,x,y,z] → [x,y,z,w] (compliant with glTF specification)
rotations_xyzw = rotations[:, [1, 2, 3, 0]].astype(np.float32)
# 5.2: Dequantize uint8[0,255] → float[-1,1]
rotations_xyzw = (rotations_xyzw - 128.0) / 128.0
# 5.3: Normalize to unit quaternion q̂ = q / |q|
# |q| = sqrt(qx² + qy² + qz² + qw²)
norms = np.linalg.norm(rotations_xyzw, axis=1, keepdims=True)
norms[norms == 0] = 1 # Prevent division by zero
rotations_xyzw /= norms
gaussian_cloud.rotations = np.ascontiguousarray(rotations_xyzw.reshape(-1), dtype=np.float32)
# 6. Process spherical harmonics
# [point0_coef0_r, point0_coef0_g, point0_coef0_b, point0_coef1_r, ..., point1_coef0_r, ...]
sh_coeffs_flat = sh_coeffs_list.reshape(-1) # (N*15*3,)
gaussian_cloud.sh_degree = 3
gaussian_cloud.sh = np.ascontiguousarray(sh_coeffs_flat, dtype=np.float32)
pack_options = spz.PackOptions()
pack_options.from_coord = spz.CoordinateSystem.LUF
temp_spz_path = "temp.spz"
success = spz.save_spz(gaussian_cloud, pack_options, temp_spz_path)
if not success:
raise RuntimeError("SPZ compression failed")
with open(temp_spz_path, "rb") as f:
compressed_bytes = f.read()
# --- Build glTF ---
gltf = GLTF2()
gltf.extensionsUsed = ["KHR_gaussian_splatting", "KHR_gaussian_splatting_compression_spz_2","KHR_materials_unlit"]
gltf.extensionsRequired = gltf.extensionsUsed.copy()
buffer = Buffer()
gltf.buffers.append(buffer)
buffer_view = BufferView(buffer=0, byteOffset=0, byteLength=len(compressed_bytes))
gltf.bufferViews.append(buffer_view)
gltf.materials.append({
"extensions": {
"KHR_materials_unlit": {}
}
})
num_points = len(positions)
# Accessors for base attributes
accessor_position = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC3",
max=positions.max(axis=0).tolist(),
min=positions.min(axis=0).tolist()
)
accessor_color = Accessor(
componentType=5121, normalized=True,
count=num_points, type="VEC4"
)
accessor_scale = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC3"
)
accessor_rotation = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC4"
)
# Add accessors for spherical harmonics coefficients
# Degree 1: 3 coefficients, each coefficient is VEC4 (RGB + padding)
sh_degree_1_accessors = []
for i in range(3):
accessor = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC4"
)
sh_degree_1_accessors.append(accessor)
# Degree 2: 5 coefficients, each coefficient is VEC4 (RGB + padding)
sh_degree_2_accessors = []
for i in range(5):
accessor = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC4"
)
sh_degree_2_accessors.append(accessor)
# Degree 3: 7 coefficients, each coefficient is VEC4 (RGB + padding)
sh_degree_3_accessors = []
for i in range(7):
accessor = Accessor(
componentType=5126, normalized=False,
count=num_points, type="VEC4"
)
sh_degree_3_accessors.append(accessor)
# Add all accessors
gltf.accessors.extend([accessor_position, accessor_color, accessor_scale, accessor_rotation])
gltf.accessors.extend(sh_degree_1_accessors) # accessor indices 4-6
gltf.accessors.extend(sh_degree_2_accessors) # accessor indices 7-11
gltf.accessors.extend(sh_degree_3_accessors) # accessor indices 12-18
attributes = {
"POSITION": 0,
"COLOR_0": 1,
"KHR_gaussian_splatting:SCALE": 2,
"KHR_gaussian_splatting:ROTATION": 3,
}
# Add 3 coefficients for SH Degree 1 (accessor indices 4-6)
for i in range(3):
attributes[f"KHR_gaussian_splatting:SH_DEGREE_1_COEF_{i}"] = 4 + i
# Add 5 coefficients for SH Degree 2 (accessor indices 7-11)
for i in range(5):
attributes[f"KHR_gaussian_splatting:SH_DEGREE_2_COEF_{i}"] = 7 + i
# Add 7 coefficients for SH Degree 3 (accessor indices 12-18)
for i in range(7):
attributes[f"KHR_gaussian_splatting:SH_DEGREE_3_COEF_{i}"] = 12 + i
primitive = Primitive(
attributes=attributes,
mode=0,
material= 0,
extensions={
"KHR_gaussian_splatting": {
"extensions": {
"KHR_gaussian_splatting_compression_spz_2": {
"bufferView": 0
}
}
}
}
)
mesh = Mesh(primitives=[primitive])
gltf.meshes.append(mesh)
# Create Node and Scene
node = Node(mesh=0)
gltf.nodes.append(node)
scene = Scene(nodes=[0])
gltf.scenes.append(scene)
gltf.scene = 0
# Write compressed data to buffer URI
gltf.buffers[0].uri = "data:application/octet-stream;base64," + base64.b64encode(compressed_bytes).decode()
for accessor in gltf.accessors:
accessor.bufferView = None
accessor.byteOffset = None
gltf.save(output_path)
Thank you very much for your help and technical support!