playcanvas
Version:
PlayCanvas WebGL game engine
328 lines (326 loc) • 14.6 kB
JavaScript
function UnifiedSortWorker() {
const myself = typeof self !== 'undefined' && self || require('node:worker_threads').parentPort;
// cache of centers for each splat id
const centersMap = new Map();
let centersData;
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);
const binDivider = new Array(numBins).fill(0);
// Weight tiers for camera-relative precision (distance from camera bin -> weight multiplier)
const weightTiers = [
{
maxDistance: 0,
weight: 40.0
},
{
maxDistance: 2,
weight: 20.0
},
{
maxDistance: 5,
weight: 8.0
},
{
maxDistance: 10,
weight: 3.0
},
{
maxDistance: Infinity,
weight: 1.0
} // Far bins
];
// Pre-calculate weight lookup table by distance from camera (constant)
const weightByDistance = new Array(numBins);
for(let dist = 0; dist < numBins; ++dist){
let weight = 1.0;
for(let j = 0; j < weightTiers.length; ++j){
if (dist <= weightTiers[j].maxDistance) {
weight = weightTiers[j].weight;
break;
}
}
weightByDistance[dist] = weight;
}
const setupCameraRelativeBins = (cameraBin, bucketCount)=>{
const totalBudget = bucketCount;
const bitsPerBin = [];
// Assign weights to bins based on pre-calculated distance lookup
for(let i = 0; i < numBins; ++i){
const distFromCamera = Math.abs(i - cameraBin);
bitsPerBin[i] = weightByDistance[distFromCamera];
}
// Normalize to fit within budget
const totalWeight = bitsPerBin.reduce((a, b)=>a + b, 0);
let accumulated = 0;
for(let i = 0; i < numBins; ++i){
binDivider[i] = Math.max(1, Math.floor(bitsPerBin[i] / totalWeight * totalBudget));
binBase[i] = accumulated;
accumulated += binDivider[i];
}
// Adjust last bin to fit exactly
if (accumulated > bucketCount) {
const excess = accumulated - bucketCount;
binDivider[numBins - 1] = Math.max(1, binDivider[numBins - 1] - excess);
}
// Add safety entry for edge case where bin >= numBins due to floating point
binBase[numBins] = binBase[numBins - 1] + binDivider[numBins - 1];
binDivider[numBins] = 0;
};
// 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++){
const params = sortParams[paramIdx];
// source centers
const id = ids[paramIdx];
const centers = centersMap.get(id);
if (!centers) {
console.error('UnifiedSortWorker: No centers found for id', id);
}
// start index in unified buffer
let targetIndex = lineStarts[paramIdx] * textureSize;
// Use provided intervals or process all centers
const intervalsArray = intervals[paramIdx].length > 0 ? intervals[paramIdx] : [
0,
centers.length / 3
];
// loop over all intervals of centers
for(let i = 0; i < intervalsArray.length; i += 2){
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];
const y = centers[srcIndex + 1];
const z = centers[srcIndex + 2];
const dist = x * sdx + y * sdy + z * sdz + add;
// Bin-based mapping
const d = dist * invBinRange;
const bin = d >>> 0;
const sortKey = binBase[bin] + binDivider[bin] * (d - bin) >>> 0;
distances[targetIndex++] = sortKey;
countBuffer[sortKey]++;
}
return targetIndex;
});
};
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)=>{
// accumulate counts
for(let i = 1; i < bucketCount; i++){
countBuffer[i] += countBuffer[i - 1];
}
// build output array
for(let i = 0; i < numVertices; i++){
const distance = distances[i];
const destIndex = --countBuffer[distance];
order[destIndex] = i;
}
};
// compute min/max effective distance using 8-corner local AABB projection per splat
const computeEffectiveDistanceRangeLinear = (sortParams)=>{
let minDist = Infinity;
let maxDist = -Infinity;
for(let paramIdx = 0; paramIdx < sortParams.length; paramIdx++){
const params = sortParams[paramIdx];
const { transformedDirection, offset, scale, aabbMin, aabbMax } = params;
const dx = transformedDirection.x;
const dy = transformedDirection.y;
const dz = transformedDirection.z;
// For a direction d and AABB [min,max], the min/max of dot(d, p) over the box
// is obtained by picking min/max per component based on the sign of d
const pxMin = dx >= 0 ? aabbMin[0] : aabbMax[0];
const pyMin = dy >= 0 ? aabbMin[1] : aabbMax[1];
const pzMin = dz >= 0 ? aabbMin[2] : aabbMax[2];
const pxMax = dx >= 0 ? aabbMax[0] : aabbMin[0];
const pyMax = dy >= 0 ? aabbMax[1] : aabbMin[1];
const pzMax = dz >= 0 ? aabbMax[2] : aabbMin[2];
const dMin = pxMin * dx + pyMin * dy + pzMin * dz;
const dMax = pxMax * dx + pyMax * dy + pzMax * dz;
const eMin = dMin * scale + offset;
const eMax = dMax * scale + offset;
// handle negative scale by swapping
const localMin = Math.min(eMin, eMax);
const localMax = Math.max(eMin, eMax);
if (localMin < minDist) minDist = localMin;
if (localMax > maxDist) maxDist = localMax;
}
if (minDist === Infinity) {
minDist = 0;
maxDist = 0;
}
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 } = _radialSort ? computeEffectiveDistanceRangeRadial(sortParams) : computeEffectiveDistanceRangeLinear(sortParams);
const numVertices = centersData.totalUsedPixels;
// calculate number of bits needed to store sorting result
const compareBits = Math.max(10, Math.min(20, Math.round(Math.log2(numVertices / 4))));
const bucketCount = 2 ** compareBits + 1;
// create distance buffer
if (distances?.length !== numVertices) {
distances = new Uint32Array(numVertices);
}
if (!countBuffer || countBuffer.length !== bucketCount) {
countBuffer = new Uint32Array(bucketCount);
} else {
countBuffer.fill(0);
}
const range = maxDist - minDist;
// Set up camera-relative bin weighting for near-camera precision
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);
if (_radialSort) {
evaluateSortKeysRadial(sortParams, minDist, range, distances, countBuffer, centersData);
} else {
evaluateSortKeysLinear(sortParams, minDist, range, distances, countBuffer, centersData);
}
countingSort(bucketCount, countBuffer, numVertices, distances, order);
const count = numVertices;
// send results
const transferList = [
order.buffer
];
const response = {
order: order.buffer,
count,
version: centersData.version
};
myself.postMessage(response, transferList);
};
myself.addEventListener('message', (message)=>{
const msgData = message.data ?? message;
switch(msgData.command){
// add centers to map
case 'addCenters':
{
centersMap.set(msgData.id, new Float32Array(msgData.centers));
break;
}
// remove centers from map
case 'removeCenters':
{
centersMap.delete(msgData.id);
break;
}
// sort
case 'sort':
{
_radialSort = msgData.radialSorting || false;
const order = new Uint32Array(msgData.order);
sort(msgData.sortParams, order, centersData);
break;
}
// intervals
case 'intervals':
{
centersData = msgData;
break;
}
}
});
}
export { UnifiedSortWorker };