From e90b84bc2859225c23073871353944961fe1789f Mon Sep 17 00:00:00 2001 From: Andreas Sundquist Date: Fri, 11 Jul 2025 10:28:20 -0700 Subject: [PATCH] Expand internal splat scale encoding to e^[-12,9]. Lower zero cut-off to e^-30. Fix background color selection in editor. --- docs/docs/packed-splats.md | 4 ++-- docs/docs/spark-renderer.md | 2 +- examples/editor/index.html | 7 +++++-- rust/spark-internal-rs/src/raycast.rs | 2 +- src/PackedSplats.ts | 2 +- src/SparkRenderer.ts | 2 +- src/defines.ts | 5 ++++- src/shaders/splatDefines.glsl | 2 +- src/utils.ts | 25 +++++++++++++------------ src/worker.ts | 10 ++++------ 10 files changed, 33 insertions(+), 28 deletions(-) diff --git a/docs/docs/packed-splats.md b/docs/docs/packed-splats.md index 322610f..0efe96f 100644 --- a/docs/docs/packed-splats.md +++ b/docs/docs/packed-splats.md @@ -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 diff --git a/docs/docs/spark-renderer.md b/docs/docs/spark-renderer.md index 6b9f1c4..9a86693 100644 --- a/docs/docs/spark-renderer.md +++ b/docs/docs/spark-renderer.md @@ -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`) diff --git a/examples/editor/index.html b/examples/editor/index.html index ddb9e1e..c1e2287 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -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); diff --git a/rust/spark-internal-rs/src/raycast.rs b/rust/spark-internal-rs/src/raycast.rs index c2f0298..a75d2b3 100644 --- a/rust/spark-internal-rs/src/raycast.rs +++ b/rust/spark-internal-rs/src/raycast.rs @@ -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 diff --git a/src/PackedSplats.ts b/src/PackedSplats.ts index 456738a..f040780 100644 --- a/src/PackedSplats.ts +++ b/src/PackedSplats.ts @@ -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. diff --git a/src/SparkRenderer.ts b/src/SparkRenderer.ts index 5d049c3..44d26d3 100644 --- a/src/SparkRenderer.ts +++ b/src/SparkRenderer.ts @@ -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 diff --git a/src/defines.ts b/src/defines.ts index affea6c..5b8e17a 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -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), diff --git a/src/shaders/splatDefines.glsl b/src/shaders/splatDefines.glsl index b56f6a4..4a4fc10 100644 --- a/src/shaders/splatDefines.glsl +++ b/src/shaders/splatDefines.glsl @@ -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 diff --git a/src/utils.ts b/src/utils.ts index e0a1080..efeeb8b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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, ), ); diff --git a/src/worker.ts b/src/worker.ts index ef3cc2e..98c95a5 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -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 = {}; - // 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,