Skip to content

Clamp splat RGB to positive in splatVertex, matching the reference rasterizer#387

Open
viethungle0503 wants to merge 2 commits into
sparkjsdev:mainfrom
viethungle0503:fix/ext-splat-color-sanitize
Open

Clamp splat RGB to positive in splatVertex, matching the reference rasterizer#387
viethungle0503 wants to merge 2 commits into
sparkjsdev:mainfrom
viethungle0503:fix/ext-splat-color-sanitize

Conversation

@viethungle0503

Copy link
Copy Markdown

Fixes #386

Trained 3DGS scenes normally contain some negative base colors. The packed encoders clamp those away at pack time, but packSplatExt/packSplatExtCov pack raw fp16 — the negatives then turn into NaN in srgbToLinear (pow with a negative base is undefined in GLSL) and, when rendering into a float target where blending doesn't clamp, end up in the framebuffer as black blobs and neon speckles. Screenshots and the full story in #386.

This adds the same clamp semantics the packed path already has, at the top of both ext pack functions: negatives and NaN go to zero (a NaN alpha then falls below minAlpha and the splat is discarded), +Inf caps at fp16 max, and everything in between passes through so the ext encoding keeps its HDR headroom.

Output on a normal canvas is byte-identical before/after — the fixed-function blend clamp was already enforcing these semantics there — so the change only shows up where things were broken.

@mrxz

mrxz commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the pull request, though I don't think packSplatExt/packSplatExtCov is the right place for this clamping. The ExtSplats representation can be used at various places in Spark. When used as splat source for a SplatMesh the encoded RGBA values will be without SH contribution, while in the accumulator they will contain the SH contribution. The positive clamping should ideally only be applied in the latter case.

The key difference with PackedSplats is that its representation is limited to positive values. There the clamping is out of necessity and not for correctness, though it does have the benefit that it prevents the issue.

In any case, I suggest moving the logic to splatVertex.glsl and simplify the logic to only clamp the RGB channels to positive values, matching graphdeco-inria/diff-gaussian-rasterization forward.cu#L65-L70. While this wouldn't address the NaN or +Inf values, I'm not sure 'sanitizing' these is ideal. Unlike negative values, these really shouldn't happen and I'd rather they cause noticeably wrong result, instead of flying under the radar when they do show up.

@viethungle0503 viethungle0503 changed the title Sanitize splat rgba when packing ExtSplats to match PackedSplats clamping Clamp splat RGB to positive in splatVertex, matching the reference rasterizer Jul 2, 2026
@viethungle0503

Copy link
Copy Markdown
Author

Thanks for the feedback — I hadn't considered that the same encoder also packs the pre-SH source representation, where clamping would shift the SH-evaluated result. I've moved the clamp to splatVertex.glsl right after the unpack, as a plain max(rgba.rgb, vec3(0.0)) matching the rasterizer you linked, and dropped the NaN/+Inf handling — I see the reasoning there, better to let those stay visible than mask them.

Re-ran my scenes to confirm: the float-target corruption is gone, the ext path matches the 8-bit reference again, and output on a normal canvas is still byte-identical before/after.

@mrxz

mrxz commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Looks good to me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extSplats: unclamped negative colors become NaN and corrupt float render targets

2 participants