Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/scene/gsplat-unified/gsplat-hybrid-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,10 @@ class GSplatHybridRenderer extends GSplatRenderer {
let minDist = 0;
let maxDist = 1;
if (!stochastic) {
const numBits = Math.max(10, Math.min(20, Math.round(Math.log2(elementCount / 4))));
// at least 16 bits whatever the splat count: log spaced keys (see GSplatProjector) are
// spread over every doubling of distance from inside the near clip out to the scene,
// and fewer leave the splats of a small scene far from the camera sharing keys
const numBits = Math.max(16, Math.min(20, Math.round(Math.log2(elementCount / 4))));
roundedNumBits = Math.ceil(numBits / gpuSorter.radixBits) * gpuSorter.radixBits;
({ minDist, maxDist } = this.computeDistanceRange(worldState, cameraNode, params.radialSorting));
}
Expand Down
63 changes: 54 additions & 9 deletions src/scene/gsplat-unified/gsplat-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
GSPLAT_RENDERER_RASTER_GPU_SORT,
GSPLAT_FORWARD,
GSPLAT_SHADOW,
GSPLAT_DEBUG_AABBS
GSPLAT_DEBUG_AABBS,
PROJECTION_ORTHOGRAPHIC
} from '../constants.js';
import { getLodDebugColors, lodDebugColorIndex } from './gsplat-lod-colors.js';

Expand Down Expand Up @@ -137,6 +138,9 @@ class GSplatManager {
/** @type {Vec3} */
lastSortCameraFwd = new Vec3(Infinity, Infinity, Infinity);

/** @type {string|null} */
lastSortKeyMode = null;

/** @type {boolean} */
sortNeeded = true;

Expand Down Expand Up @@ -575,21 +579,33 @@ class GSplatManager {

/**
* Tests if the camera has moved enough to require re-sorting.
* - A change of sort keys (see {@link _sortKeyMode}) always re-sorts
* - For radial sorting: only position matters (rotation doesn't affect sort order)
* - For directional sorting: only forward direction matters (position doesn't affect sort order)
* - For directional sorting: forward direction matters, and position too when the depth keys
* are log spaced
*
* @returns {boolean} True if camera moved enough to require re-sorting, otherwise false.
*/
testCameraMovedForSort() {
// a switch of projection or sort mode needs a new order, even from a camera that is still
if (this._sortKeyMode() !== this.lastSortKeyMode) {
return true;
}

const epsilon = 0.001;
const moved = this.lastSortCameraPos.distance(this.cameraNode.getPosition()) > epsilon;

if (this.gsplat.radialSorting) {
// For radial sorting, only position changes matter
const currentCameraPos = this.cameraNode.getPosition();
return this.lastSortCameraPos.distance(currentCameraPos) > epsilon;
return moved;
}

// For directional sorting, only forward direction changes matter
// For directional sorting, forward direction changes matter, and position changes too when
// the depth keys are log spaced: they are measured from the camera, and the splats at or
// behind it share the nearest key, so a move that brings them into view needs a new order
if (moved && this.lastSortKeyMode === 'logDepth') {
return true;
}
if (Number.isFinite(this.lastSortCameraFwd.x)) {
const currentCameraFwd = this.cameraNode.forward;
const dot = Math.min(1, Math.max(-1, this.lastSortCameraFwd.dot(currentCameraFwd)));
Expand All @@ -600,6 +616,30 @@ class GSplatManager {
return true;
}

/**
* Whether a directional CPU sort spaces its depth keys by log depth, which needs a perspective,
* non-fisheye camera. An orthographic or fisheye camera can draw splats at or behind it, so its
* depth keys keep the bins.
*
* @returns {boolean} True when the depth keys are log spaced.
* @private
*/
_logDepthKeys() {
return this.cameraNode.camera.projection !== PROJECTION_ORTHOGRAPHIC && !(this.gsplat.fisheye > 0);
}

/**
* The keys a CPU sort gives the splats: 'radial', 'logDepth' (see {@link _logDepthKeys}) or
* 'depthBins'.
*
* @returns {string} The sort key mode.
* @private
*/
_sortKeyMode() {
if (this.gsplat.radialSorting) return 'radial';
return this._logDepthKeys() ? 'logDepth' : 'depthBins';
}

/**
* Fires the frame:ready event with current sorting and loading state.
*/
Expand Down Expand Up @@ -721,6 +761,7 @@ class GSplatManager {
// The CPU path posts to the worker only when a re-sort is needed. The version lifecycle
// (markSorted on the first sort of a version) stays here in the manager.
if (lastState) {
let sortSent = true;
if (this.renderer.usesGpuSort) {
this._markSortedIfNeeded(lastState);

Expand All @@ -733,15 +774,18 @@ class GSplatManager {
this.renderer.prepareRenderView(this.world, lastState, this._fillRenderViewParams());
}
} else if (this.sortNeeded) {
this.sortCpu(lastState);
sortSent = this.sortCpu(lastState);
}

if (this.sortNeeded) {
// The CPU sorter turns a sort away while one of the same version is in flight, so the
// request stays until it is sent: the camera tracking below must be the pose sorted for
if (this.sortNeeded && sortSent) {
this.sortNeeded = false;

// Update camera tracking for the next CPU re-sort check.
this.lastSortCameraPos.copy(this.cameraNode.getPosition());
this.lastSortCameraFwd.copy(this.cameraNode.forward);
this.lastSortKeyMode = this._sortKeyMode();
}
}

Expand Down Expand Up @@ -819,10 +863,11 @@ class GSplatManager {
* Sorts the splats using CPU worker (asynchronous).
*
* @param {GSplatWorldState} lastState - The last world state.
* @returns {boolean} True if the sort was sent to the worker; false if it was turned away.
*/
sortCpu(lastState) {
Debug.assert(this.cpuSorter, 'CPU sorter not initialized');
if (!this.cpuSorter) return;
if (!this.cpuSorter) return false;

// Get camera's world-space properties
const cameraNode = this.cameraNode;
Expand Down Expand Up @@ -872,7 +917,7 @@ class GSplatManager {
});
});

this.cpuSorter.setSortParams(sorterRequest, radialSort);
return this.cpuSorter.setSortParams(sorterRequest, radialSort, cameraNode.camera.nearClip, this._logDepthKeys());
}
}

Expand Down
52 changes: 42 additions & 10 deletions src/scene/gsplat-unified/gsplat-projector.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
SHADERLANGUAGE_WGSL,
SHADERSTAGE_COMPUTE,
UNIFORMTYPE_FLOAT,
UNIFORMTYPE_INT,
UNIFORMTYPE_MAT4,
UNIFORMTYPE_UINT,
UNIFORMTYPE_UVEC4,
Expand Down Expand Up @@ -60,6 +61,10 @@ const _viewProj1Data = new Float32Array(16);
const _viewData = new Float32Array(16);
const _shaderProjMat = new Mat4();

// scratch for reading a float's bit pattern, see the sort keys
const _floatBits = new Float32Array(1);
const _floatBitsI32 = new Int32Array(_floatBits.buffer);

/**
* Owns the per-splat compute pass for the hybrid GSplat renderer (Pass B in the pipeline):
*
Expand Down Expand Up @@ -293,7 +298,10 @@ class GSplatProjector {
new UniformFormat('minDist', UNIFORMTYPE_FLOAT),
new UniformFormat('invRange', UNIFORMTYPE_FLOAT),
new UniformFormat('foveationStrength', UNIFORMTYPE_FLOAT),
new UniformFormat('foveationCenter', UNIFORMTYPE_FLOAT)
new UniformFormat('foveationCenter', UNIFORMTYPE_FLOAT),
new UniformFormat('sortKeyMinBits', UNIFORMTYPE_INT),
new UniformFormat('sortKeyScale', UNIFORMTYPE_FLOAT),
new UniformFormat('maxSortKey', UNIFORMTYPE_UINT)
];

this._projectorUniformBufferFormat = new UniformBufferFormat(device, baseFields);
Expand Down Expand Up @@ -705,14 +713,6 @@ class GSplatProjector {
const range = maxDist - minDist;
const invRange = range > 0 ? 1.0 / range : 1.0;

// Bin weights — same pattern as CPU-side sort key preparation.
if (!stochastic) {
const bucketCount = (1 << numBits);
const cameraBin = GSplatSortBinWeights.computeCameraBin(radialMode, minDist, range);
const binWeights = this.binWeightsUtil.compute(cameraBin, bucketCount);
this.binWeightsBuffer.write(0, binWeights);
}

compute.setParameter('compactedSplatIds', compactedSplatIds);
compute.setParameter('sortElementCount', sortElementCountBuffer);
compute.setParameter('projCache', this.projCache);
Expand All @@ -731,6 +731,7 @@ class GSplatProjector {
// Camera + projection uniforms.
const cameraComponent = cameraNode.camera;
const cam = cameraComponent.camera;
const orthographic = cam.projection === PROJECTION_ORTHOGRAPHIC;
const webgpu = this.device.isWebGPU;
// focal length in pixels per axis (they differ when the viewport's pixel aspect doesn't
// match the projection's)
Expand Down Expand Up @@ -786,13 +787,44 @@ class GSplatProjector {
compute.setParameter('minContribution', minContribution);
compute.setParameter('foveationStrength', foveationStrength);
compute.setParameter('foveationCenter', foveationCenter);
compute.setParameter('isOrtho', cam.projection === PROJECTION_ORTHOGRAPHIC ? 1 : 0);
compute.setParameter('isOrtho', orthographic ? 1 : 0);
compute.setParameter('splatTextureSize', workBuffer.textureSize);
compute.setParameter('numBins', GSplatSortBinWeights.NUM_BINS);
compute.setParameter('minDist', minDist);
compute.setParameter('invRange', invRange);
compute.setParameter('stochastic', stochastic ? 1 : 0);

// Sort keys - same as the CPU worker. Radial keys, and the depth keys of a perspective
// camera, are spaced by log distance: they span the bit patterns of the distances from ten
// doublings inside the near clip (the splats closer than it are drawn too, with their depth
// clamped) out to the far end of the range, squared for radial (see the shader). An
// orthographic or fisheye camera can draw splats at or behind it, so its depth keys keep the
// camera-weighted bins.
const logKeys = !stochastic && (radialMode || (!orthographic && !fisheyeMode));
let sortKeyMinBits = 0;
let sortKeyScale = 0;
const maxSortKey = logKeys ? (1 << numBits) - 1 : 0;
if (logKeys) {
// an orthographic near clip can be zero or negative, so a share of the range stands in
// for it then (radial only)
const nearDist = (cam.nearClip > 0 ? cam.nearClip : maxDist * 1e-6) / 1024;
_floatBits[0] = radialMode ? nearDist * nearDist : nearDist;
sortKeyMinBits = _floatBitsI32[0];
// a linear range runs along the camera's +Z, which points backwards, so its far end is
// at -minDist
_floatBits[0] = radialMode ? maxDist * maxDist : -minDist;
const bitsRange = _floatBitsI32[0] - sortKeyMinBits;
sortKeyScale = bitsRange > 0 ? maxSortKey / bitsRange : 0;
} else if (!stochastic) {
const bucketCount = (1 << numBits);
const cameraBin = GSplatSortBinWeights.computeCameraBin(minDist, range);
const binWeights = this.binWeightsUtil.compute(cameraBin, bucketCount);
this.binWeightsBuffer.write(0, binWeights);
}
compute.setParameter('sortKeyMinBits', sortKeyMinBits);
compute.setParameter('sortKeyScale', sortKeyScale);
compute.setParameter('maxSortKey', maxSortKey);

if (fisheyeMode) {
compute.setParameter('fisheye_k', fisheyeProj.k);
compute.setParameter('fisheye_inv_k', fisheyeProj.invK);
Expand Down
13 changes: 5 additions & 8 deletions src/scene/gsplat-unified/gsplat-sort-bin-weights.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,17 @@ class GSplatSortBinWeights {
}

/**
* Computes the camera bin index based on sort mode and distance range.
* Computes the camera bin index from the distance range of a linear sort. Only the depth sort
* of an orthographic or fisheye camera uses the bins; the other sort keys are spaced by log
* distance.
*
* @param {boolean} radialSort - Whether using radial sort mode.
* @param {number} minDist - Minimum distance.
* @param {number} range - Distance range (maxDist - minDist).
* @returns {number} The camera bin index (0 to NUM_BINS-1).
*/
static computeCameraBin(radialSort, minDist, range) {
static computeCameraBin(minDist, range) {
const numBins = GSplatSortBinWeights.NUM_BINS;
if (radialSort) {
// For radial sort with inverted distances, camera (dist=0) maps to the last bin
return numBins - 1;
}
// For linear sort, calculate where camera falls in the projected distance range
// calculate where camera falls in the projected distance range
const cameraOffsetFromRangeStart = -minDist;
const cameraBinFloat = (cameraOffsetFromRangeStart / range) * numBins;
return Math.max(0, Math.min(numBins - 1, Math.floor(cameraBinFloat)));
Expand Down
Loading
Loading