From 5eb57038b49604276446d22891db2e9edac62843 Mon Sep 17 00:00:00 2001 From: Andreas Sundquist Date: Mon, 21 Jul 2025 18:15:13 -0700 Subject: [PATCH] Externalize render settings minAlpha and maxPixelRadius (#130) * Add commented benchmarkSort to compare JS vs Wasm sorting. * Add 32-bit float sort via SparkViewpooint.sort32, using 2-pass radix-65536 sort. Turn on sort32 by default for examples/editor. Implemented Rust sort and JS sort, updated benchmarking code to include float16 and float32 sort. * Add sort32 to docs/spark-viewpoint.md. * Externalized SparkRenderer.maxPixelRadius and .minAlpha settings. Added to documentation. Added to examples/editor under Debug folder, moved sort32 there. --- docs/docs/spark-renderer.md | 2 ++ examples/editor/index.html | 4 ++++ src/SparkRenderer.ts | 20 ++++++++++++++++++++ src/shaders/splatDefines.glsl | 4 ---- src/shaders/splatFragment.glsl | 3 ++- src/shaders/splatVertex.glsl | 14 ++++++++------ 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docs/docs/spark-renderer.md b/docs/docs/spark-renderer.md index 29959ad..84c6ca9 100644 --- a/docs/docs/spark-renderer.md +++ b/docs/docs/spark-renderer.md @@ -57,6 +57,8 @@ 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)`) +| **maxPixelRadius** | Maximum pixel radius for splat rendering. (default: `512.0`) +| **minAlpha** | Minimum alpha value for splat rendering. (default: `0.5 * (1.0 / 255.0)`) | **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. diff --git a/examples/editor/index.html b/examples/editor/index.html index 317c93a..4f17afc 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -515,6 +515,10 @@ }, }, "AA").name("AA preset"); debugFolder.add(spark, "focalAdjustment", 0.1, 2.0, 0.1).name("Tweak focalAdjustment"); + spark.defaultView.sort32 = true; + debugFolder.add(spark.defaultView, "sort32").name("Float32 sort").listen(); + debugFolder.add(spark, "maxPixelRadius", 1, 1024, 1).name("Max pixel radius").listen(); + debugFolder.add(spark, "minAlpha", 0, 1, 0.001).name("Min alpha").listen(); const splatsFolder = secondGui.addFolder("Files"); diff --git a/src/SparkRenderer.ts b/src/SparkRenderer.ts index 18da9a0..ee898fe 100644 --- a/src/SparkRenderer.ts +++ b/src/SparkRenderer.ts @@ -116,6 +116,16 @@ export type SparkRendererOptions = { * @default Math.sqrt(8) */ maxStdDev?: number; + /** + * Maximum pixel radius for splat rendering. + * @default 512.0 + */ + maxPixelRadius?: number; + /** + * Minimum alpha value for splat rendering. + * @default 0.5 * (1.0 / 255.0) + */ + minAlpha?: number; /** * 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 @@ -185,6 +195,8 @@ export class SparkRenderer extends THREE.Mesh { preUpdate: boolean; originDistance: number; maxStdDev: number; + maxPixelRadius: number; + minAlpha: number; enable2DGS: boolean; preBlurAmount: number; blurAmount: number; @@ -301,6 +313,8 @@ export class SparkRenderer extends THREE.Mesh { this.preUpdate = options.preUpdate ?? false; this.originDistance = options.originDistance ?? 1; this.maxStdDev = options.maxStdDev ?? Math.sqrt(8.0); + this.maxPixelRadius = options.maxPixelRadius ?? 512.0; + this.minAlpha = options.minAlpha ?? 0.5 * (1.0 / 255.0); this.enable2DGS = options.enable2DGS ?? false; this.preBlurAmount = options.preBlurAmount ?? 0.0; this.blurAmount = options.blurAmount ?? 0.3; @@ -350,6 +364,10 @@ export class SparkRenderer extends THREE.Mesh { renderToViewPos: { value: new THREE.Vector3() }, // Maximum distance (in stddevs) from Gsplat center to render maxStdDev: { value: 1.0 }, + // Maximum pixel radius for splat rendering + maxPixelRadius: { value: 512.0 }, + // Minimum alpha value for splat rendering + minAlpha: { value: 0.5 * (1.0 / 255.0) }, // Enable interpreting 0-thickness Gsplats as 2DGS enable2DGS: { value: false }, // Add to projected 2D splat covariance diagonal (thickens and brightens) @@ -517,6 +535,8 @@ export class SparkRenderer extends THREE.Mesh { this.uniforms.far.value = typedCamera.far; this.uniforms.encodeLinear.value = viewpoint.encodeLinear; this.uniforms.maxStdDev.value = this.maxStdDev; + this.uniforms.maxPixelRadius.value = this.maxPixelRadius; + this.uniforms.minAlpha.value = this.minAlpha; this.uniforms.enable2DGS.value = this.enable2DGS; this.uniforms.preBlurAmount.value = this.preBlurAmount; this.uniforms.blurAmount.value = this.blurAmount; diff --git a/src/shaders/splatDefines.glsl b/src/shaders/splatDefines.glsl index 4a4fc10..d7b8bae 100644 --- a/src/shaders/splatDefines.glsl +++ b/src/shaders/splatDefines.glsl @@ -21,10 +21,6 @@ const float PI = 3.1415926535897932384626433832795; const float INFINITY = 1.0 / 0.0; const float NEG_INFINITY = -INFINITY; -const float MAX_PIXEL_RADIUS = 512.0; -const float MIN_ALPHA = 0.5 * (1.0 / 255.0); // 0.00196 -const float MAX_STDDEV = sqrt(8.0); - float sqr(float x) { return x * x; } diff --git a/src/shaders/splatFragment.glsl b/src/shaders/splatFragment.glsl index c49de39..4c4ef77 100644 --- a/src/shaders/splatFragment.glsl +++ b/src/shaders/splatFragment.glsl @@ -8,6 +8,7 @@ uniform float near; uniform float far; uniform bool encodeLinear; uniform float maxStdDev; +uniform float minAlpha; uniform bool disableFalloff; uniform float falloff; @@ -60,7 +61,7 @@ void main() { rgba.a *= mix(1.0, exp(-0.5 * z), falloff); - if (rgba.a < MIN_ALPHA) { + if (rgba.a < minAlpha) { discard; } if (encodeLinear) { diff --git a/src/shaders/splatVertex.glsl b/src/shaders/splatVertex.glsl index aea0475..c7086db 100644 --- a/src/shaders/splatVertex.glsl +++ b/src/shaders/splatVertex.glsl @@ -16,9 +16,11 @@ uniform uint numSplats; uniform vec4 renderToViewQuat; uniform vec3 renderToViewPos; uniform float maxStdDev; +uniform float maxPixelRadius; uniform float time; uniform float deltaTime; uniform bool debugFlag; +uniform float minAlpha; uniform bool enable2DGS; uniform float blurAmount; uniform float preBlurAmount; @@ -52,7 +54,7 @@ void main() { vec4 quaternion, rgba; unpackSplat(packed, center, scales, quaternion, rgba); - if (rgba.a < MIN_ALPHA) { + if (rgba.a < minAlpha) { return; } bvec3 zeroScales = equal(scales, vec3(0.0)); @@ -141,13 +143,13 @@ void main() { float fullBlurAmount = blurAmount; if ((focalDistance > 0.0) && (apertureAngle > 0.0)) { - float focusRadius = MAX_PIXEL_RADIUS; + float focusRadius = maxPixelRadius; if (viewCenter.z < 0.0) { float focusBlur = abs((-viewCenter.z - focalDistance) / viewCenter.z); float apertureRadius = focal.x * tan(0.5 * apertureAngle); focusRadius = focusBlur * apertureRadius; } - fullBlurAmount = clamp(sqr(focusRadius), blurAmount, sqr(MAX_PIXEL_RADIUS)); + fullBlurAmount = clamp(sqr(focusRadius), blurAmount, sqr(maxPixelRadius)); } // Do convolution with a 0.5-pixel Gaussian for anti-aliasing: sqrt(0.3) ~= 0.5 @@ -159,7 +161,7 @@ void main() { // Compute anti-aliasing intensity scaling factor float blurAdjust = sqrt(max(0.0, detOrig / det)); rgba.a *= blurAdjust; - if (rgba.a < MIN_ALPHA) { + if (rgba.a < minAlpha) { return; } @@ -172,8 +174,8 @@ void main() { vec2 eigenVec1 = normalize(vec2((abs(b) < 0.001) ? 1.0 : b, eigen1 - a)); vec2 eigenVec2 = vec2(eigenVec1.y, -eigenVec1.x); - float scale1 = position.x * min(MAX_PIXEL_RADIUS, maxStdDev * sqrt(eigen1)); - float scale2 = position.y * min(MAX_PIXEL_RADIUS, maxStdDev * sqrt(eigen2)); + float scale1 = position.x * min(maxPixelRadius, maxStdDev * sqrt(eigen1)); + float scale2 = position.y * min(maxPixelRadius, maxStdDev * sqrt(eigen2)); // Compute the NDC coordinates for the ellipsoid's diagonal axes. vec2 pixelOffset = eigenVec1 * scale1 + eigenVec2 * scale2;