Skip to content

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

Description

@viethungle0503

I recently switched to extSplats: true + accumExtSplats: true to keep fp16 color precision end to end (picked up from the suggestion in #383 (comment) — it does fix the 8-bit banding nicely). My splats are composited through a HalfFloat render target, and with that combination every scene I load comes out like this:

extSplats stock — black blobs and neon speckles

Same scene and camera through the default packed path, and through the ext path with the one-line fix suggested below:

default packed path — reference

extSplats with the fix

A second, unrelated scene shows the same thing:

second scene, extSplats stock

second scene, default packed path

second scene, extSplats with the fix

What's happening

Trained 3DGS scenes normally contain some negative base colors — 3–8% of splats in my scenes, with no NaN/Inf in the files themselves. The packed path clamps those away at pack time (packSplatEncoding in splatDefines.glsl), but packSplatExt / packSplatExtCov pack raw fp16 and nothing downstream clamps either. With encodeLinear the negatives then hit srgbToLinearpow with a negative base, undefined in GLSL — and come out as NaN on every GPU I tried.

On a normal canvas the fixed-function blend clamps to [0, 1] and hides all of this. In a float render target (any EffectComposer-style pipeline) blending doesn't clamp, so the NaN/negative premultiplied colors land straight in the framebuffer — the black blobs and neon speckles above.

Repro

Any trained 3DGS .ply with extSplats: true, accumExtSplats: true, encodeLinear: true, rendered into a HalfFloatType target and blitted to screen. Drop any one ingredient and it looks fine again — although without encodeLinear it's still wrong, just quieter: no NaN, but the negatives darken the blend and leave dark smears in the halo:

encodeLinear off, stock — dark smears in the halo

encodeLinear off, with the fix

Suggested fix

One line at the top of packSplatExt and packSplatExtCov, mirroring the clamp the packed encoders already do:

rgba = mix(clamp(rgba, 0.0, 65504.0), vec4(0.0), isnan(rgba));

Negatives and NaN go to zero (a NaN alpha then falls below minAlpha and the splat is discarded), +Inf caps at 65504 — the largest finite fp16 value, i.e. the most packHalf2x16 can hold — and everything in between passes through, so the HDR headroom of the ext encoding stays intact. With this the ext path matches the 8-bit reference again, and output on a normal canvas is byte-identical before/after. An alternative would be guarding srgbToLinear against negative input, but sanitizing at pack time covers every downstream consumer at once.

npm 2.1.0 and current main behave identically. I'll open a PR with this change — would appreciate you taking a look.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions