Skip to content

Commit f451f17

Browse files
mvaligurskyMartin Valigursky
andauthored
Add foveated contribution culling to the GPU-sort gsplat renderer (#8869)
Raises the contribution-culling threshold radially from the screen centre, so low-impact splats are culled increasingly toward the edges where peripheral overdraw dominates XR frame cost. Effective threshold: minContribution + foveationStrength * smoothstep(foveationCenter, 1, length(ndc)) - GSplatParams: new foveationStrength (default 0 = off) and foveationCenter (default 0.3, protected-centre radius) — GPU-sort (hybrid) renderer only. - Projector: values plumbed via dispatch params and the projector UBO; the cull in computeSplatCov applies the radial boost (exact no-op at strength 0). - Compute-local renderer is unaffected (passes inert constants). - vr-lod example: in-XR HUD number row to tune strength (+/- 5, 0..50). Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
1 parent fd9d063 commit f451f17

8 files changed

Lines changed: 105 additions & 3 deletions

File tree

examples/src/examples/gaussian-splatting-xr/vr-lod.example.mjs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ assetListLoader.load(() => {
216216
data.set('splatBudget', 2);
217217
// XR framebuffer scale factor (applied only when starting an XR session; does not affect 2D).
218218
data.set('framebufferScaleFactor', 1);
219+
// Foveated contribution culling strength (GPU-sort renderer only; 0 = off).
220+
data.set('foveationStrength', 0);
219221
data.set('data.stats.gsplats', '—');
220222
data.set('data.stats.resolution', '—');
221223

@@ -288,7 +290,9 @@ assetListLoader.load(() => {
288290
{ type: 'number', label: 'Contribution', value: '5', decEvent: 'contribution:dec', incEvent: 'contribution:inc' },
289291
// [7] number row: alphaClipForward, stepped 1/255 .. 1/2
290292
{ type: 'number', label: 'AlphaClip', value: '1/16', decEvent: 'alphaclip:dec', incEvent: 'alphaclip:inc' },
291-
{ label: 'EXIT XR', eventName: 'xr:end' } // [8] interactive: ends the XR session
293+
// [8] number row: foveated contribution culling strength (GPU-sort renderer only)
294+
{ type: 'number', label: 'FovCull', value: '0.0', decEvent: 'fovcull:dec', incEvent: 'fovcull:inc' },
295+
{ label: 'EXIT XR', eventName: 'xr:end' } // [9] interactive: ends the XR session
292296
],
293297
fontAsset: assets.font,
294298
alwaysVisible: true,
@@ -387,6 +391,23 @@ assetListLoader.load(() => {
387391
});
388392
applyAlphaClip(); // seed the readout
389393

394+
// Foveated contribution culling strength (number row [8]). +/- 5, clamped to 0..50
395+
// (default 0 = off). Only affects the GPU-sort (hybrid) renderer.
396+
const FOVCULL_ROW = 8;
397+
const applyFovCull = () => {
398+
const v = data.get('foveationStrength') ?? 0;
399+
app.scene.gsplat.foveationStrength = v;
400+
xrHud?.setItemValue(FOVCULL_ROW, v.toFixed(1));
401+
};
402+
data.on('foveationStrength:set', applyFovCull);
403+
app.on('fovcull:dec', () => {
404+
data.set('foveationStrength', Math.max(0, (data.get('foveationStrength') ?? 0) - 5));
405+
});
406+
app.on('fovcull:inc', () => {
407+
data.set('foveationStrength', Math.min(50, (data.get('foveationStrength') ?? 0) + 5));
408+
});
409+
applyFovCull(); // seed the engine value and readout
410+
390411
/** @type {pc.Entity|null} */
391412
let gsplatEntity = null;
392413
/** @type {any} */

src/scene/gsplat-unified/gsplat-manager.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,6 +1762,8 @@ class GSplatManager {
17621762
alphaClip,
17631763
minPixelSize: gsplat.minPixelSize * 0.5,
17641764
minContribution: gsplat.minContribution,
1765+
foveationStrength: gsplat.foveationStrength,
1766+
foveationCenter: gsplat.foveationCenter,
17651767
viewportWidth,
17661768
viewportHeight,
17671769
flipY: !!cameraNode.camera.renderTarget?.flipY,

src/scene/gsplat-unified/gsplat-params.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class GSplatParams {
7272
this._material.setParameter('alphaClipForward', 1.0 / 255.0);
7373
this._material.setParameter('minPixelSize', 2.0);
7474
this._material.setParameter('minContribution', 3.0);
75+
this._material.setParameter('foveationStrength', 0);
76+
this._material.setParameter('foveationCenter', 0.3);
7577
}
7678

7779
/**
@@ -658,6 +660,53 @@ class GSplatParams {
658660
return this._material.getParameter('minContribution')?.data ?? 3.0;
659661
}
660662

663+
/**
664+
* Sets the foveated contribution culling strength. Only used by the
665+
* {@link GSPLAT_RENDERER_RASTER_GPU_SORT} renderer. When greater than zero, the contribution
666+
* culling threshold is raised radially from the screen centre: the effective threshold is
667+
* `minContribution + foveationStrength * smoothstep(foveationCenter, 1, length(ndc))`, so the
668+
* centre of the screen is unaffected and low-contribution splats are culled increasingly
669+
* toward the edges, reaching full strength at the screen edge and beyond (corners). Set to 0
670+
* to disable. Defaults to 0.
671+
*
672+
* @type {number}
673+
*/
674+
set foveationStrength(value) {
675+
this._material.setParameter('foveationStrength', value);
676+
this._material.update();
677+
}
678+
679+
/**
680+
* Gets the foveated contribution culling strength.
681+
*
682+
* @type {number}
683+
*/
684+
get foveationStrength() {
685+
return this._material.getParameter('foveationStrength')?.data ?? 0;
686+
}
687+
688+
/**
689+
* Sets the protected centre radius for foveated contribution culling. Only used by the
690+
* {@link GSPLAT_RENDERER_RASTER_GPU_SORT} renderer. Expressed in NDC radius units (0 at the
691+
* screen centre, 1 at the edge): within this radius {@link foveationStrength} has no effect,
692+
* and the falloff ramps smoothly from this radius to the screen edge. Defaults to 0.3.
693+
*
694+
* @type {number}
695+
*/
696+
set foveationCenter(value) {
697+
this._material.setParameter('foveationCenter', value);
698+
this._material.update();
699+
}
700+
701+
/**
702+
* Gets the protected centre radius for foveated contribution culling.
703+
*
704+
* @type {number}
705+
*/
706+
get foveationCenter() {
707+
return this._material.getParameter('foveationCenter')?.data ?? 0.3;
708+
}
709+
661710
/**
662711
* Enables anti-aliasing compensation for Gaussian splats. Defaults to false.
663712
*

src/scene/gsplat-unified/gsplat-projector.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,9 @@ class GSplatProjector {
271271
new UniformFormat('alphaClip', UNIFORMTYPE_FLOAT),
272272
new UniformFormat('minContribution', UNIFORMTYPE_FLOAT),
273273
new UniformFormat('minDist', UNIFORMTYPE_FLOAT),
274-
new UniformFormat('invRange', UNIFORMTYPE_FLOAT)
274+
new UniformFormat('invRange', UNIFORMTYPE_FLOAT),
275+
new UniformFormat('foveationStrength', UNIFORMTYPE_FLOAT),
276+
new UniformFormat('foveationCenter', UNIFORMTYPE_FLOAT)
275277
];
276278

277279
this._projectorUniformBufferFormat = new UniformBufferFormat(device, baseFields);
@@ -590,6 +592,10 @@ class GSplatProjector {
590592
* @param {number} params.alphaClip - Alpha cull threshold.
591593
* @param {number} params.minPixelSize - Minimum on-screen pixel size before culling.
592594
* @param {number} params.minContribution - Minimum total contribution before culling.
595+
* @param {number} params.foveationStrength - Foveated culling strength added to
596+
* `minContribution` toward the screen edges (0 disables).
597+
* @param {number} params.foveationCenter - Protected centre radius (NDC units) within which
598+
* foveated culling has no effect.
593599
* @param {number} params.viewportWidth - Render viewport width in pixels.
594600
* @param {number} params.viewportHeight - Render viewport height in pixels.
595601
* @param {boolean} params.flipY - Whether the active render target uses `flipY` (must match
@@ -610,6 +616,7 @@ class GSplatProjector {
610616
workBuffer, cameraNode, compactedSplatIds, sortElementCountBuffer,
611617
totalCapacity, radialSort, numBits, minDist, maxDist,
612618
alphaClip, minPixelSize, minContribution,
619+
foveationStrength = 0, foveationCenter = 0.3,
613620
viewportWidth, viewportHeight, flipY,
614621
pickMode = false,
615622
fisheyeProj,
@@ -734,6 +741,8 @@ class GSplatProjector {
734741
compute.setParameter('alphaClip', alphaClip);
735742
compute.setParameter('minPixelSize', minPixelSize);
736743
compute.setParameter('minContribution', minContribution);
744+
compute.setParameter('foveationStrength', foveationStrength);
745+
compute.setParameter('foveationCenter', foveationCenter);
737746
compute.setParameter('isOrtho', cam.projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0);
738747
compute.setParameter('splatTextureSize', workBuffer.textureSize);
739748
compute.setParameter('numBins', GSplatSortBinWeights.NUM_BINS);

src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-common.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ fn computeSplatCov(
4646
isOrtho: u32,
4747
alphaClip: f32,
4848
minContribution: f32,
49+
foveationStrength: f32,
50+
foveationCenter: f32,
4951
#ifdef GSPLAT_FISHEYE
5052
fisheye_k: f32,
5153
fisheye_inv_k: f32,
@@ -174,8 +176,18 @@ fn computeSplatCov(
174176
// Rejects splats whose total visual contribution (opacity * projected area) is
175177
// negligible. Near the camera, projected areas are large so contributions naturally
176178
// exceed the threshold; at distance, areas shrink and low-impact splats are culled.
179+
//
180+
// Foveation: the threshold is raised radially from the screen centre. Within
181+
// foveationCenter (NDC radius) there is no effect; the boost ramps with a smoothstep
182+
// to full foveationStrength at the screen edge (r = 1) and beyond (corners). With
183+
// strength 0 this is an exact no-op.
184+
let fovNdc = screen * vec2f(2.0 / viewportWidth, 2.0 / viewportHeight) - vec2f(1.0);
185+
// manual smoothstep with a guard so foveationCenter near 1.0 cannot divide by zero
186+
let fovT = saturate((length(fovNdc) - foveationCenter) / max(1.0 - foveationCenter, 1e-4));
187+
let effMinContribution = minContribution + foveationStrength * fovT * fovT * (3.0 - 2.0 * fovT);
188+
177189
let totalContribution = opacity * 6.283185 * sqrt(det);
178-
if (totalContribution < minContribution) {
190+
if (totalContribution < effMinContribution) {
179191
return result;
180192
}
181193

src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-tile-count.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ fn main(
101101
uniforms.alphaClip,
102102
uniforms.minPixelSize,
103103
uniforms.minContribution,
104+
// foveated culling is not supported by the compute-local renderer (strength 0 = off)
105+
0.0, 0.3,
104106
uniforms.viewMatrix,
105107
uniforms.viewProj,
106108
uniforms.focal,

src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-project-common.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ fn projectSplatCommon(
3232
alphaClip: f32,
3333
minPixelSize: f32,
3434
minContribution: f32,
35+
foveationStrength: f32,
36+
foveationCenter: f32,
3537
viewMatrix: mat4x4f,
3638
viewProj: mat4x4f,
3739
focal: f32,
@@ -78,6 +80,7 @@ fn projectSplatCommon(
7880
focal, viewportWidth, viewportHeight,
7981
nearClip, farClip, opacity, minPixelSize,
8082
isOrtho, alphaClip, minContribution,
83+
foveationStrength, foveationCenter,
8184
#ifdef GSPLAT_FISHEYE
8285
fisheye_k, fisheye_inv_k,
8386
fisheye_projMat00, fisheye_projMat11,

src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-projector.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ struct ProjectorUniforms {
5757
minContribution: f32,
5858
minDist: f32,
5959
invRange: f32,
60+
foveationStrength: f32,
61+
foveationCenter: f32,
6062
#ifdef GSPLAT_XR
6163
// Eye-1 view-projection (raw XR projViewOffMat). Eye 0 uses viewProj above.
6264
// Appended only for the stereo variant - matches the conditional CPU UBO field.
@@ -118,6 +120,8 @@ fn main(
118120
uniforms.alphaClip,
119121
uniforms.minPixelSize,
120122
uniforms.minContribution,
123+
uniforms.foveationStrength,
124+
uniforms.foveationCenter,
121125
uniforms.viewMatrix,
122126
uniforms.viewProj,
123127
uniforms.focal,

0 commit comments

Comments
 (0)