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.
This commit is contained in:
Andreas Sundquist
2025-07-21 18:15:13 -07:00
committed by GitHub
parent 23b9b190a0
commit 5eb57038b4
6 changed files with 36 additions and 11 deletions
+2
View File
@@ -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.
+4
View File
@@ -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");
+20
View File
@@ -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;
-4
View File
@@ -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;
}
+2 -1
View File
@@ -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) {
+8 -6
View File
@@ -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;