Skip to content

Commit c7b82eb

Browse files
mvaligurskyMartin Valigursky
andauthored
Improved splatBudget behaviour to allow both increase and decrease LOD (#8230)
* Improved splatBudget behaviour to allow both increase and decrease LOD * jsdocs --------- Co-authored-by: Martin Valigursky <[email protected]>
1 parent de1e52d commit c7b82eb

File tree

2 files changed

+83
-36
lines changed

2 files changed

+83
-36
lines changed

src/scene/gsplat-unified/gsplat-octree-instance.js

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -459,10 +459,10 @@ class GSplatOctreeInstance {
459459
// Pass 1: Evaluate optimal LOD for each node (distance-based)
460460
const totalOptimalSplats = this.evaluateNodeLods(cameraNode, maxLod, lodDistances, rangeMin, rangeMax, params);
461461

462-
// Enforce splat budget if enabled and over budget
462+
// Enforce splat budget if enabled (bidirectional: degrade or upgrade)
463463
const { splatBudget } = params;
464-
if (splatBudget > 0 && totalOptimalSplats > splatBudget) {
465-
this.enforceSplatBudget(totalOptimalSplats, splatBudget, rangeMax);
464+
if (splatBudget > 0) {
465+
this.enforceSplatBudget(totalOptimalSplats, splatBudget, rangeMin, rangeMax);
466466
}
467467

468468
// Pass 2: Calculate desired LOD (underfill) and apply changes
@@ -559,16 +559,19 @@ class GSplatOctreeInstance {
559559
}
560560

561561
/**
562-
* Adjusts optimal LOD indices to fit within the splat budget by degrading quality
563-
* for lower-importance nodes first. Uses multiple passes, degrading by one level per pass,
564-
* until within budget or all nodes are at maximum coarseness (rangeMax).
562+
* Adjusts optimal LOD indices to fit within the splat budget bidirectionally.
563+
* When over budget: degrades quality for lower-importance nodes first.
564+
* When under budget: upgrades quality for higher-importance nodes first.
565+
* Uses multiple passes, adjusting by one level per pass, until budget is reached
566+
* or all nodes hit their respective limits (rangeMin or rangeMax).
565567
*
566568
* @param {number} totalSplats - Current total splat count with optimal LODs.
567-
* @param {number} splatBudget - Maximum allowed splat count.
569+
* @param {number} splatBudget - Target splat count to reach.
570+
* @param {number} rangeMin - Minimum allowed LOD index.
568571
* @param {number} rangeMax - Maximum allowed LOD index.
569572
* @private
570573
*/
571-
enforceSplatBudget(totalSplats, splatBudget, rangeMax) {
574+
enforceSplatBudget(totalSplats, splatBudget, rangeMin, rangeMax) {
572575
const nodes = this.octree.nodes;
573576
const nodeInfos = this.nodeInfos;
574577

@@ -586,36 +589,76 @@ class GSplatOctreeInstance {
586589

587590
let currentSplats = totalSplats;
588591

589-
// Multiple passes: degrade by one level per pass until within budget
590-
while (currentSplats > splatBudget) {
591-
let degradedAnyNode = false;
592+
// Skip if already at budget
593+
if (currentSplats === splatBudget) {
594+
return;
595+
}
592596

593-
// Try degrading each node by one level (starting from lowest importance)
594-
for (let i = 0; i < nodeIndices.length; i++) {
595-
if (currentSplats <= splatBudget) {
596-
break; // Within budget
597-
}
597+
// Determine direction and set iteration parameters
598+
const isOverBudget = currentSplats > splatBudget;
599+
const lodDelta = isOverBudget ? 1 : -1;
600+
601+
// Multiple passes: adjust by one LOD level per pass until budget is reached
602+
while (isOverBudget ? currentSplats > splatBudget : currentSplats < splatBudget) {
603+
let modified = false;
604+
605+
if (isOverBudget) {
606+
607+
// DEGRADE: process from lowest to highest importance
608+
for (let i = 0; i < nodeIndices.length; i++) {
609+
const nodeIndex = nodeIndices[i];
610+
const nodeInfo = nodeInfos[nodeIndex];
611+
const node = nodes[nodeIndex];
612+
const currentOptimalLod = nodeInfo.optimalLod;
613+
614+
// Try degrading to next coarser LOD (respect rangeMax constraint)
615+
if (currentOptimalLod < rangeMax) {
616+
const currentLod = node.lods[currentOptimalLod];
617+
const nextLod = node.lods[currentOptimalLod + 1];
618+
const splatsSaved = currentLod.count - nextLod.count;
598619

599-
const nodeIndex = nodeIndices[i];
600-
const nodeInfo = nodeInfos[nodeIndex];
601-
const node = nodes[nodeIndex];
602-
const currentOptimalLod = nodeInfo.optimalLod;
603-
604-
// Try degrading to next coarser LOD (respect rangeMax constraint)
605-
if (currentOptimalLod < rangeMax) {
606-
const currentLod = node.lods[currentOptimalLod];
607-
const nextLod = node.lods[currentOptimalLod + 1];
608-
const splatsSaved = currentLod.count - nextLod.count;
609-
610-
// Degrade to coarser LOD
611-
nodeInfo.optimalLod = currentOptimalLod + 1;
612-
currentSplats -= splatsSaved;
613-
degradedAnyNode = true;
620+
// Degrade to coarser LOD
621+
nodeInfo.optimalLod += lodDelta;
622+
currentSplats -= splatsSaved;
623+
modified = true;
624+
625+
if (currentSplats <= splatBudget) {
626+
break; // Within budget
627+
}
628+
}
629+
}
630+
} else {
631+
632+
// UPGRADE: process from highest to lowest importance
633+
for (let i = nodeIndices.length - 1; i >= 0; i--) {
634+
const nodeIndex = nodeIndices[i];
635+
const nodeInfo = nodeInfos[nodeIndex];
636+
const node = nodes[nodeIndex];
637+
const currentOptimalLod = nodeInfo.optimalLod;
638+
639+
// Try upgrading to next finer LOD (respect rangeMin constraint)
640+
if (currentOptimalLod > rangeMin) {
641+
const currentLod = node.lods[currentOptimalLod];
642+
const nextLod = node.lods[currentOptimalLod - 1];
643+
const splatsAdded = nextLod.count - currentLod.count;
644+
645+
// Only upgrade if we won't exceed budget
646+
if (currentSplats + splatsAdded <= splatBudget) {
647+
// Upgrade to finer LOD
648+
nodeInfo.optimalLod += lodDelta;
649+
currentSplats += splatsAdded;
650+
modified = true;
651+
652+
if (currentSplats >= splatBudget) {
653+
break; // At budget
654+
}
655+
}
656+
}
614657
}
615658
}
616659

617-
// If no nodes could be degraded, all are at rangeMax - can't reduce further
618-
if (!degradedAnyNode) {
660+
// If no nodes were modified, we can't adjust further
661+
if (!modified) {
619662
break;
620663
}
621664
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,13 @@ class GSplatParams {
212212
_splatBudget = 0;
213213

214214
/**
215-
* Soft limit on the total number of splats to render. When the optimal LOD selections would
216-
* exceed this budget, the system will adjust LOD levels to stay within the limit while
217-
* prioritizing quality for closer/more important geometry.
215+
* Target number of splats to render. The system will adjust LOD levels bidirectionally to
216+
* reach this budget:
217+
* - When over budget: degrades quality for less important geometry
218+
* - When under budget: upgrades quality for more important geometry
219+
*
220+
* This ensures optimal use of available rendering budget while prioritizing quality for
221+
* closer/more important geometry.
218222
*
219223
* Set to 0 to disable the budget (default). When disabled, optimal LOD is determined purely
220224
* by distance and configured LOD parameters.

0 commit comments

Comments
 (0)