playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
115 lines (114 loc) • 4.08 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { NUM_BUCKETS } from "./constants.js";
class GSplatBudgetBalancer {
constructor() {
/**
* Buckets storing NodeInfo references.
* @type {Array<Array>|null}
* @private
*/
__publicField(this, "_buckets", null);
}
/**
* Initialize bucket infrastructure on first use.
* @private
*/
_initBuckets() {
if (!this._buckets) {
this._buckets = new Array(NUM_BUCKETS);
for (let i = 0; i < NUM_BUCKETS; i++) {
this._buckets[i] = [];
}
}
}
/**
* Balances splat budget across all octree instances by adjusting LOD levels.
* Uses sqrt-based bucket distribution to give more precision to nearby geometry.
* Makes multiple passes, adjusting by one LOD level per pass, until budget is reached
* or all nodes hit their respective limits (per-instance rangeMin or rangeMax).
*
* @param {Map<GSplatPlacement, GSplatOctreeInstance>} octreeInstances - Map of
* GSplatOctreeInstance objects.
* @param {number} budget - Target splat budget for octrees.
*/
balance(octreeInstances, budget) {
this._initBuckets();
for (let i = 0; i < NUM_BUCKETS; i++) {
this._buckets[i].length = 0;
}
let totalOptimalSplats = 0;
for (const [, inst] of octreeInstances) {
const nodes = inst.octree.nodes;
const nodeInfos = inst.nodeInfos;
for (let nodeIndex = 0, len = nodes.length; nodeIndex < len; nodeIndex++) {
const nodeInfo = nodeInfos[nodeIndex];
const optimalLod = nodeInfo.optimalLod;
if (optimalLod < 0) continue;
const lods = nodes[nodeIndex].lods;
nodeInfo.lods = lods;
this._buckets[nodeInfo.budgetBucket].push(nodeInfo);
totalOptimalSplats += lods[optimalLod].count;
}
}
let currentSplats = totalOptimalSplats;
if (currentSplats === budget) {
return;
}
const isOverBudget = currentSplats > budget;
let done = false;
while (!done && (isOverBudget ? currentSplats > budget : currentSplats < budget)) {
let modified = false;
if (isOverBudget) {
for (let b = NUM_BUCKETS - 1; b >= 0 && !done; b--) {
const bucket = this._buckets[b];
for (let i = 0, len = bucket.length; i < len; i++) {
const nodeInfo = bucket[i];
if (nodeInfo.optimalLod < nodeInfo.inst.rangeMax) {
const lods = nodeInfo.lods;
const optimalLod = nodeInfo.optimalLod;
currentSplats -= lods[optimalLod].count - lods[optimalLod + 1].count;
nodeInfo.optimalLod = optimalLod + 1;
modified = true;
if (currentSplats <= budget) {
done = true;
break;
}
}
}
}
} else {
for (let b = 0; b < NUM_BUCKETS && !done; b++) {
const bucket = this._buckets[b];
for (let i = 0, len = bucket.length; i < len; i++) {
const nodeInfo = bucket[i];
if (nodeInfo.optimalLod > nodeInfo.inst.rangeMin) {
const lods = nodeInfo.lods;
const optimalLod = nodeInfo.optimalLod;
const splatsAdded = lods[optimalLod - 1].count - lods[optimalLod].count;
if (currentSplats + splatsAdded <= budget) {
nodeInfo.optimalLod = optimalLod - 1;
currentSplats += splatsAdded;
modified = true;
if (currentSplats >= budget) {
done = true;
break;
}
} else {
done = true;
break;
}
}
}
}
}
if (!modified) {
break;
}
}
}
}
export {
GSplatBudgetBalancer
};