Expand internal splat scale encoding to e^[-12,9]. Lower zero cut-off to e^-30. Fix background color selection in editor. (#110)

This commit is contained in:
Andreas Sundquist
2025-07-11 10:48:10 -07:00
committed by GitHub
parent c8394665ec
commit dc07e02a33
10 changed files with 33 additions and 28 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# PackedSplats
A `PackedSplats` is a collection of Gaussian splats, packed into a format that takes exactly 16 bytes per splat to maximize memory and cache efficiency. The `center` xyz coordinates are encoded as float16 (3 x 2 bytes), `scale` xyz as 3 x uint8 that encode a log scale from e^-9 to e^9, `rgba` as 4 x uint8, and quaternion encoded via axis+angle using 2 x uint8 for octahedral encoding of the axis direction and a uint8 to encode rotation amount from 0..Pi.
A `PackedSplats` is a collection of Gaussian splats, packed into a format that takes exactly 16 bytes per splat to maximize memory and cache efficiency. The `center` xyz coordinates are encoded as float16 (3 x 2 bytes), `scale` xyz as 3 x uint8 that encode a log scale from e^-12 to e^9, `rgba` as 4 x uint8, and quaternion encoded via axis+angle using 2 x uint8 for octahedral encoding of the axis direction and a uint8 to encode rotation amount from 0..Pi.
## Creating a `PackedSplats`
@@ -115,7 +115,7 @@ The center x/y/z components are encoded as float16, which provides 10 bits of ma
### Splat scales encoding
The XYZ scales are encoded independently using the following mapping: Any scale values below e^-20 are interpreted as "true zero" scale, and encoded as `uint8(0)`. Any other values quantized by computing `ln(scale_xyz)`, mapping the range e^-9..e^9 to uint8 values 1..255, rounding, and clamping. This logarithmic scale range can encode values from 0.0001 up to 8K in scale, with approximately 7% steps between discrete sizes, and has minimal impact on perceptible visual quality.
The XYZ scales are encoded independently using the following mapping: Any scale values below e^-30 are interpreted as "true zero" scale, and encoded as `uint8(0)`. Any other values quantized by computing `ln(scale_xyz)`, mapping the range e^-12..e^9 to uint8 values 1..255, rounding, and clamping. This logarithmic scale range can encode values from 0.0001 up to 8K in scale, with approximately 7% steps between discrete sizes, and has minimal impact on perceptible visual quality.
### Splat orientation encoding
+1 -1
View File
@@ -56,7 +56,7 @@ const spark = new SparkRenderer({
| **preUpdate** | Controls whether to update the splats before or after rendering. For WebXR this *must* be false in order to complete rendering as soon as possible. (default: `false`)
| **originDistance** | Distance threshold for `SparkRenderer` movement triggering a splat update at the new origin. (default: `1.0`) This can be useful when your `SparkRenderer` is a child of your camera and you want to retain high precision coordinates near the camera.
| **maxStdDev** | Maximum standard deviations from the center to render Gaussians. Values `Math.sqrt(5)`..`Math.sqrt(9)` produce good results and can be tweaked for performance. (default: `Math.sqrt(8)`)
| **enable2DGS** | Enable 2D Gaussian splatting rendering ability. When this mode is enabled, any `scale` x/y/z component that is exactly `0` (minimum quantized value) results in the other two non-zero axes being interpreted as an oriented 2D Gaussian Splat instead of the usual approximate projected 3DGS Z-slice. When reading PLY files, scale values less than e^-20 will be interpreted as `0`. (default: `false`)
| **enable2DGS** | Enable 2D Gaussian splatting rendering ability. When this mode is enabled, any `scale` x/y/z component that is exactly `0` (minimum quantized value) results in the other two non-zero axes being interpreted as an oriented 2D Gaussian Splat instead of the usual approximate projected 3DGS Z-slice. When reading PLY files, scale values less than e^-30 will be interpreted as `0`. (default: `false`)
| **preBlurAmount** | Scalar value to add to 2D splat covariance diagonal, effectively blurring + enlarging splats. In scenes trained without the splat anti-aliasing tweak this value was typically 0.3, but with anti-aliasing it is 0.0 (default: `0.0`)
| **blurAmount** | Scalar value to add to 2D splat covariance diagonal, with opacity adjustment to correctly account for "blurring" when anti-aliasing. Typically 0.3 (equivalent to approx 0.5 pixel radius) in scenes trained with anti-aliasing.
| **focalDistance** | Depth-of-field distance to focal plane (default: `0.0`)
+5 -2
View File
@@ -143,6 +143,7 @@
orbit: false,
reversePointerDir: false,
reversePointerSlide: false,
backgroundColor: "#000000",
openFiles: () => {
fileInput.click();
},
@@ -485,8 +486,10 @@
spark.focalDistance = Math.exp(value);
});
gui.add(spark, "apertureAngle", 0, 0.01 * Math.PI, 0.001).name("Aperture angle").listen();
scene.background = new THREE.Color(0, 0, 0);
gui.addColor(scene, "background").name("Background color").listen();
scene.background = new THREE.Color(guiOptions.backgroundColor);
gui.addColor(guiOptions, "backgroundColor").name("Background color").onChange((value) => {
scene.background.set(value);
});
const debugFolder = gui.addFolder("Debug").close();
const normalColor = dyno.dynoBool(false);
+1 -1
View File
@@ -2,7 +2,7 @@ use half::f16;
const MIN_OPACITY: f32 = 0.1;
pub const LN_SCALE_MIN: f32 = -9.0;
pub const LN_SCALE_MIN: f32 = -12.0;
pub const LN_SCALE_MAX: f32 = 9.0;
pub const LN_RESCALE: f32 = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; // 1..=255
+1 -1
View File
@@ -51,7 +51,7 @@ export type PackedSplatsOptions = {
// A PackedSplats is a collection of Gaussian splats, packed into a format that
// takes exactly 16 bytes per Gsplat to maximize memory and cache efficiency.
// The center xyz coordinates are encoded as float16 (3 x 2 bytes), scale xyz
// as 3 x uint8 that encode a log scale from e^-9 to e^9, rgba as 4 x uint8,
// as 3 x uint8 that encode a log scale from e^-12 to e^9, rgba as 4 x uint8,
// and quaternion encoded via axis+angle using 2 x uint8 for octahedral encoding
// of the axis direction and a uint8 to encode rotation amount from 0..Pi.
+1 -1
View File
@@ -105,7 +105,7 @@ export type SparkRendererOptions = {
// any scale x/y/z component that is exactly 0 (minimum quantized value) results
// in the other two non-0 axis being interpreted as an oriented 2D Gaussian Splat,
// rather instead of the usual projected 3DGS Z-slice. When reading PLY files,
// scale values less than e^-20 will be interpreted as 0. (default: false)
// scale values less than e^-30 will be interpreted as 0. (default: false)
enable2DGS?: boolean;
// Scalar value to add to 2D splat covariance diagonal, effectively blurring +
// enlarging splats. In scenes trained without the Gsplat anti-aliasing tweak
+4 -1
View File
@@ -4,12 +4,15 @@
// If these values are changed, the corresponding values in splatDefines.glsl
// must also be updated to match.
export const LN_SCALE_MIN = -9.0;
export const LN_SCALE_MIN = -12.0;
export const LN_SCALE_MAX = 9.0;
export const LN_RESCALE = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; // 1..=255
export const SCALE_MIN = Math.exp(LN_SCALE_MIN);
export const SCALE_MAX = Math.exp(LN_SCALE_MAX);
export const LN_SCALE_ZERO = -30.0;
export const SCALE_ZERO = Math.exp(LN_SCALE_ZERO);
// Gsplats are stored in textures that are 2^11 x 2^11 x up to 2^11
// Most WebGL2 implementations support 2D textures up to 2^12 x 2^12 (max 16M Gsplats)
// 2D array textures and 3D textures up to 2^11 x 2^11 x 2^11 (max 8G Gsplats),
+1 -1
View File
@@ -1,4 +1,4 @@
const float LN_SCALE_MIN = -9.0;
const float LN_SCALE_MIN = -12.0;
const float LN_SCALE_MAX = 9.0;
const float LN_RESCALE = (LN_SCALE_MAX - LN_SCALE_MIN) / 254.0; // 1..=255
+13 -12
View File
@@ -6,6 +6,7 @@ import * as THREE from "three";
import {
LN_RESCALE,
LN_SCALE_MIN,
SCALE_ZERO,
SPLAT_TEX_HEIGHT,
SPLAT_TEX_MIN_HEIGHT,
SPLAT_TEX_WIDTH,
@@ -370,32 +371,32 @@ export function setPackedSplat(
// Allow scales below LN_SCALE_MIN to be encoded as 0, which signifies a 2DGS
const uScaleX =
scaleX === 0.0
scaleX < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleX) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
const uScaleY =
scaleY === 0.0
scaleY < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleY) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
const uScaleZ =
scaleZ === 0.0
scaleZ < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleZ) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
@@ -441,32 +442,32 @@ export function setPackedSplatScales(
) {
// Allow scales below LN_SCALE_MIN to be encoded as 0, which signifies a 2DGS
const uScaleX =
scaleX === 0.0
scaleX < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleX) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
const uScaleY =
scaleY === 0.0
scaleY < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleY) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
const uScaleZ =
scaleZ === 0.0
scaleZ < SCALE_ZERO
? 0
: Math.min(
255,
Math.max(
0,
1,
Math.round((Math.log(scaleZ) - LN_SCALE_MIN) / LN_RESCALE) + 1,
),
);
+4 -6
View File
@@ -1,7 +1,7 @@
import init_wasm, { sort_splats } from "spark-internal-rs";
import type { PcSogsJson, TranscodeSpzInput } from "./SplatLoader";
import { unpackAntiSplat } from "./antisplat";
import { SCALE_MIN, WASM_SPLAT_SORT } from "./defines";
import { WASM_SPLAT_SORT } from "./defines";
import { unpackKsplat } from "./ksplat";
import { unpackPcSogs, unpackPcSogsZip } from "./pcsogs";
import { PlyReader } from "./ply";
@@ -193,8 +193,6 @@ async function unpackPly({
const numSplats = ply.numSplats;
const extra: Record<string, unknown> = {};
// Anything below this is considered zero and can be rendered as 2DGS
const ZERO_CUTOFF = Math.exp(-20);
ply.parseSplats(
(
@@ -220,9 +218,9 @@ async function unpackPly({
x,
y,
z,
scaleX < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleX),
scaleY < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleY),
scaleZ < ZERO_CUTOFF ? 0 : Math.max(SCALE_MIN, scaleZ),
scaleX,
scaleY,
scaleZ,
quatX,
quatY,
quatZ,