@@ -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 }
0 commit comments