Skip to content

fix: Keep one non-finite value from corrupting its compressed PLY chunk - #355

Open
rudycelekli wants to merge 1 commit into
playcanvas:mainfrom
rudycelekli:fix/compressed-ply-nonfinite-chunk-20261015
Open

rudycelekli wants to merge 1 commit into
playcanvas:mainfrom
rudycelekli:fix/compressed-ply-nonfinite-chunk-20261015

Conversation

@rudycelekli

@rudycelekli rudycelekli commented Oct 15, 2026 •

Copy link
Copy Markdown

Problem

In compressed PLY output, CompressedChunk.pack() computes each 256-splat chunk's min/max with Math.min/Math.max. A single NaN in a chunk's position, scale or color makes the bounds NaN, so all 256 splats in the chunk decode as NaN (a plain PLY would lose only the one bad splat). Related symptom class: playcanvas/supersplat#494 and playcanvas/supersplat#356 (chunk bounds degrading splats). I could not reproduce those two issues themselves with synthetic data; this is a separate, provable robustness defect in the same code.

Evidence (synthetic 256-splat table, one NaN each in x, scale_0, f_dc_1, round-tripped through writeCompressedPly + readPly)

  • Before: x: 256 NaN values after round-trip (same for the other two columns)
  • After: 0 NaN values (the NaN splat itself decodes to the chunk minimum; its 255 neighbours are intact)

Change

  • calcMinMax ignores non-finite values (falls back to 0..0 if none are finite).
  • normalize maps NaN to 0.
  • Side effect: a +/-Infinity scale no longer widens the chunk's scale bounds to the +/-20 clamp; it saturates to the finite bound of its chunk.

Verified / not verified

  • Verified: new test in test/formats.test.mjs fails before the change and passes after; npm run lint, tsc --noEmit, and test/formats.test.mjs, write-sog, process, quantize-1d tests pass. I did not run the GPU/render tests.
  • Not verified: byte-for-byte output on real scenes (finite data takes the identical code path apart from the bounds loop), visual results in a viewer.

I reviewed and tested it before opening.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@slimbuck

Copy link
Copy Markdown
Member

Thanks for this. The NaN-poisons-the-whole-chunk bug is real and worth fixing, but the change as written introduces a regression for -Infinity scales.

-Infinity log-scales are valid data here (flat / zero-thickness axes, e.g. 2DGS-style scenes); filterNaNRows deliberately keeps them, and the existing ±20 clamp in pack() exists to handle them. Skipping all non-finite values in calcMinMax bypasses that clamp. Running CompressedChunk.pack() from main vs this branch:

Chunk contents main this PR
all 256 splats have scale_2 = -Inf bounds [-20, -20] → decodes to exp(-20) ≈ 2e-9 bounds fall back to [0, 0] → decodes to exp(0) = 1.0
one scale_2 = -Inf, rest in [-4, -2] that splat stays at -20 that splat inflates to the chunk's finite min (≈ -3.99)

So a chunk of entirely flat splats becomes unit-thickness slabs, which is worse than the original bug because it affects clean input.

Could you:

  1. Keep -Infinity scale behaviour unchanged, e.g. only skip NaN in calcMinMax, or clamp scale values to [-20, 20] before computing bounds (skipping ±Infinity for position and color is fine).
  2. Add a test covering a chunk where every splat has a -Infinity scale.

Minor: with this change a NaN-position splat decodes to the chunk's min corner with its own color/opacity, i.e. a visible stray splat rather than one the renderer drops. Still better than losing 256 splats, but the description's comparison to plain PLY isn't quite accurate; --filter-nan remains the way to drop those cleanly.

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.

2 participants