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:

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


A second, unrelated scene shows the same thing:



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 srgbToLinear — pow 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:


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.
I recently switched to
extSplats: true+accumExtSplats: trueto 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:Same scene and camera through the default packed path, and through the ext path with the one-line fix suggested below:
A second, unrelated scene shows the same thing:
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 (
packSplatEncodinginsplatDefines.glsl), butpackSplatExt/packSplatExtCovpack raw fp16 and nothing downstream clamps either. WithencodeLinearthe negatives then hitsrgbToLinear—powwith 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 aHalfFloatTypetarget and blitted to screen. Drop any one ingredient and it looks fine again — although withoutencodeLinearit's still wrong, just quieter: no NaN, but the negatives darken the blend and leave dark smears in the halo:Suggested fix
One line at the top of
packSplatExtandpackSplatExtCov, mirroring the clamp the packed encoders already do:Negatives and NaN go to zero (a NaN alpha then falls below
minAlphaand the splat is discarded), +Inf caps at 65504 — the largest finite fp16 value, i.e. the mostpackHalf2x16can 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 guardingsrgbToLinearagainst 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.