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
6 changes: 5 additions & 1 deletion src/scene/gsplat-unified/gsplat-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ class GSplatManager {
// transform by the full inverse matrix and then normalize, which cancels the (1/s) scaling factor
const transformedDirection = invModelMat.transformVector(cameraDirection).normalize();

// camera position in splat's local space (for circular sorting)
const transformedPosition = invModelMat.transformPoint(cameraPosition);

// world-space offset
modelMat.getTranslation(translation);
const offset = translation.sub(cameraPosition).dot(cameraDirection);
Expand All @@ -518,6 +521,7 @@ class GSplatManager {

sorterRequest.push({
transformedDirection,
transformedPosition,
offset,
scale: uniformScale,
modelMat: modelMat.data.slice(),
Expand All @@ -526,7 +530,7 @@ class GSplatManager {
});
});

this.sorter.setSortParams(sorterRequest);
this.sorter.setSortParams(sorterRequest, this.scene.gsplat.radialSorting);
this.renderer.updateViewport(cameraNode);
}

Expand Down
11 changes: 11 additions & 0 deletions src/scene/gsplat-unified/gsplat-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ class GSplatParams {
*/
debugAabbs = false;

/**
* Enables radial sorting based on distance from camera (for cubemap rendering). When false,
* uses directional sorting along camera forward vector. Defaults to false.
*
* Note: Radial sorting helps reduce sorting artifacts when the camera rotates (looks around),
* while linear sorting is better at minimizing artifacts when the camera translates (moves).
*
* @type {boolean}
*/
radialSorting = false;

/**
* Enables debug rendering of AABBs for GSplat octree nodes. Defaults to false.
*
Expand Down
155 changes: 128 additions & 27 deletions src/scene/gsplat-unified/gsplat-unified-sort-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ function UnifiedSortWorker() {
let distances;
let countBuffer;

// Sorting mode: false = forward vector (directional), true = radial distance (for cubemaps)
let _radialSort = false;

// camera-relative bin-based precision optimization
const numBins = 32;
const binBase = new Array(numBins).fill(0);
Expand Down Expand Up @@ -66,27 +69,16 @@ function UnifiedSortWorker() {
binDivider[numBins] = 0;
};

const evaluateSortKeys = (sortParams, minDist, range, distances, countBuffer, centersData) => {
// Common sort key evaluation logic
const evaluateSortKeysCommon = (sortParams, minDist, range, distances, countBuffer, centersData, processSplatFn) => {
const { ids, lineStarts, padding, intervals, textureSize } = centersData;

// pre-calculate inverse bin range
const invBinRange = numBins / range;

// loop over all the splat placements
for (let paramIdx = 0; paramIdx < sortParams.length; paramIdx++) {

// camera related params
const params = sortParams[paramIdx];
const { transformedDirection, offset, scale } = params;
const dx = transformedDirection.x;
const dy = transformedDirection.y;
const dz = transformedDirection.z;

// pre-calculate camera related constants
const sdx = dx * scale;
const sdy = dy * scale;
const sdz = dz * scale;
const add = offset - minDist;

// source centers
const id = ids[paramIdx];
Expand All @@ -106,6 +98,36 @@ function UnifiedSortWorker() {
const intervalStart = intervalsArray[i] * 3;
const intervalEnd = intervalsArray[i + 1] * 3;

// Process each center in this interval using the provided function
targetIndex = processSplatFn(centers, params, intervalStart, intervalEnd, targetIndex,
invBinRange, minDist, range, distances, countBuffer);
}

// add padding, to make sure the whole buffer (including padding) is sorted
const pad = padding[paramIdx];
countBuffer[0] += pad;

// set distance values for padding positions to prevent garbage data
distances.fill(0, targetIndex, targetIndex + pad);
targetIndex += pad;
}
};

const evaluateSortKeysLinear = (sortParams, minDist, range, distances, countBuffer, centersData) => {
evaluateSortKeysCommon(sortParams, minDist, range, distances, countBuffer, centersData,
(centers, params, intervalStart, intervalEnd, targetIndex, invBinRange, minDist, range, distances, countBuffer) => {
// camera related params
const { transformedDirection, offset, scale } = params;
const dx = transformedDirection.x;
const dy = transformedDirection.y;
const dz = transformedDirection.z;

// pre-calculate camera related constants
const sdx = dx * scale;
const sdy = dy * scale;
const sdz = dz * scale;
const add = offset - minDist;

// Process each center in this interval
for (let srcIndex = intervalStart; srcIndex < intervalEnd; srcIndex += 3) {
const x = centers[srcIndex];
Expand All @@ -122,16 +144,45 @@ function UnifiedSortWorker() {
distances[targetIndex++] = sortKey;
countBuffer[sortKey]++;
}
}

// add padding, to make sure the whole buffer (including padding) is sorted
const pad = padding[paramIdx];
countBuffer[0] += pad;
return targetIndex;
});
};

// set distance values for padding positions to prevent garbage data
distances.fill(0, targetIndex, targetIndex + pad);
targetIndex += pad;
}
const evaluateSortKeysRadial = (sortParams, minDist, range, distances, countBuffer, centersData) => {
evaluateSortKeysCommon(sortParams, minDist, range, distances, countBuffer, centersData,
(centers, params, intervalStart, intervalEnd, targetIndex, invBinRange, minDist, range, distances, countBuffer) => {
// camera related params
const { transformedPosition, scale } = params;

// camera position in local space
const cx = transformedPosition.x;
const cy = transformedPosition.y;
const cz = transformedPosition.z;

// Process each center in this interval
for (let srcIndex = intervalStart; srcIndex < intervalEnd; srcIndex += 3) {
const dx = centers[srcIndex] - cx;
const dy = centers[srcIndex + 1] - cy;
const dz = centers[srcIndex + 2] - cz;

const distSq = dx * dx + dy * dy + dz * dz;
// World-space radial distance from camera
const dist = Math.sqrt(distSq) * scale;

// Bin-based mapping (normalize by minDist for binning)
// Invert distance so far objects get small keys (rendered first, back-to-front)
const invertedDist = range - dist;
const d = invertedDist * invBinRange;
const bin = d >>> 0;
const sortKey = (binBase[bin] + binDivider[bin] * (d - bin)) >>> 0;

distances[targetIndex++] = sortKey;
countBuffer[sortKey]++;
}

return targetIndex;
});
};

const countingSort = (bucketCount, countBuffer, numVertices, distances, order) => {
Expand All @@ -150,7 +201,7 @@ function UnifiedSortWorker() {
};

// compute min/max effective distance using 8-corner local AABB projection per splat
const computeEffectiveDistanceRange = (sortParams) => {
const computeEffectiveDistanceRangeLinear = (sortParams) => {
let minDist = Infinity;
let maxDist = -Infinity;

Expand Down Expand Up @@ -191,10 +242,48 @@ function UnifiedSortWorker() {
return { minDist, maxDist };
};

// compute min/max radial distance from camera to AABB corners (for radial sort)
const computeEffectiveDistanceRangeRadial = (sortParams) => {
let maxDist = -Infinity;

for (let paramIdx = 0; paramIdx < sortParams.length; paramIdx++) {
const params = sortParams[paramIdx];
const { transformedPosition, scale, aabbMin, aabbMax } = params;
const cx = transformedPosition.x;
const cy = transformedPosition.y;
const cz = transformedPosition.z;

// Check all 8 corners of the AABB for max radial distance
for (let i = 0; i < 8; i++) {
const px = (i & 1) ? aabbMax[0] : aabbMin[0];
const py = (i & 2) ? aabbMax[1] : aabbMin[1];
const pz = (i & 4) ? aabbMax[2] : aabbMin[2];

const dx = px - cx;
const dy = py - cy;
const dz = pz - cz;

const distSq = dx * dx + dy * dy + dz * dz;
const dist = Math.sqrt(distSq) * scale;

if (dist > maxDist) maxDist = dist;
}
}

// For radial sort, minDist is always 0 (camera is the origin of radial distances)
const minDist = 0;
if (maxDist < 0) {
maxDist = 0;
}
return { minDist, maxDist };
};

const sort = (sortParams, order, centersData) => {

// distance bounds from AABB projections per splat
const { minDist, maxDist } = computeEffectiveDistanceRange(sortParams);
const { minDist, maxDist } = _radialSort ?
computeEffectiveDistanceRangeRadial(sortParams) :
computeEffectiveDistanceRangeLinear(sortParams);

const numVertices = centersData.totalUsedPixels;

Expand All @@ -217,13 +306,24 @@ function UnifiedSortWorker() {
const range = maxDist - minDist;

// Set up camera-relative bin weighting for near-camera precision
const cameraOffsetFromRangeStart = 0 - minDist;
const cameraBinFloat = (cameraOffsetFromRangeStart / range) * numBins;
const cameraBin = Math.max(0, Math.min(numBins - 1, Math.floor(cameraBinFloat)));
let cameraBin;
if (_radialSort) {
// For radial sort with inverted distances, camera (dist=0) maps to the last bin
cameraBin = numBins - 1;
} else {
// For linear sort, calculate where camera falls in the projected distance range
const cameraOffsetFromRangeStart = 0 - minDist;
const cameraBinFloat = (cameraOffsetFromRangeStart / range) * numBins;
cameraBin = Math.max(0, Math.min(numBins - 1, Math.floor(cameraBinFloat)));
}

setupCameraRelativeBins(cameraBin, bucketCount);

evaluateSortKeys(sortParams, minDist, range, distances, countBuffer, centersData);
if (_radialSort) {
evaluateSortKeysRadial(sortParams, minDist, range, distances, countBuffer, centersData);
} else {
evaluateSortKeysLinear(sortParams, minDist, range, distances, countBuffer, centersData);
}

countingSort(bucketCount, countBuffer, numVertices, distances, order);

Expand Down Expand Up @@ -259,6 +359,7 @@ function UnifiedSortWorker() {

// sort
case 'sort': {
_radialSort = msgData.radialSorting || false;
const order = new Uint32Array(msgData.order);
sort(msgData.sortParams, order, centersData);
break;
Expand Down
4 changes: 3 additions & 1 deletion src/scene/gsplat-unified/gsplat-unified-sorter.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ class GSplatUnifiedSorter extends EventHandler {
* Sends sorting parameters to the sorter. Called every frame sorting is needed.
*
* @param {object} params - The sorting parameters - per-splat directions, offsets, scales, AABBs.
* @param {boolean} radialSorting - Whether to use radial distance sorting.
*/
setSortParams(params) {
setSortParams(params, radialSorting) {

// only process job requests if we have a new version or no jobs are in flight
if (this.hasNewVersion || this.jobsInFlight === 0) {
Expand All @@ -148,6 +149,7 @@ class GSplatUnifiedSorter extends EventHandler {
this.worker.postMessage({
command: 'sort',
sortParams: params,
radialSorting: radialSorting,
order: orderData.buffer
}, [
orderData.buffer
Expand Down