Skip to content

perf: per-node granular SH color updates with angle-based threshold#8593

Merged
mvaligursky merged 2 commits intomainfrom
mv-per-node-sh-updates
Apr 13, 2026
Merged

perf: per-node granular SH color updates with angle-based threshold#8593
mvaligursky merged 2 commits intomainfrom
mv-per-node-sh-updates

Conversation

@mvaligursky
Copy link
Copy Markdown
Contributor

Dramatically reduces GPU cost of spherical harmonics color updates by moving from per-splat to per-octree-node granularity and using the existing partial rendering mechanism.

Changes:

  • SH color updates now operate at per-octree-node granularity instead of per-splat, using changedAllocIds partial rendering to only re-evaluate nodes that exceed their viewing angle threshold
  • Distant nodes naturally update less frequently since more camera translation is needed to change the viewing angle by the threshold amount
  • Non-octree splats use AABB closest-point distance for threshold scaling

API Changes:

  • colorUpdateAngle repurposed: now controls the viewing angle threshold in degrees for SH re-evaluation (default: 10). Previously controlled camera rotation threshold.
  • colorUpdateDistance removed (Debug.removed stub, suggests colorUpdateAngle)
  • colorUpdateDistanceLodScale removed (Debug.removed stub, per-node scaling is automatic)
  • colorUpdateAngleLodScale removed (Debug.removed stub, per-node scaling is automatic)

Examples:

  • Updated world.example.mjs and lod-streaming-sh.example.mjs to use colorUpdateAngle

Performance:

  • For scenes with ~50 splats / ~50K octree nodes / 40M splats, this reduces GSplatWorkBufferRenderPass GPU traffic from re-evaluating all splats to only nearby nodes whose viewing angle changed, eliminating the texture cache limiter bottleneck during camera movement
  • With LOD forced to level 0 (all 40M splats active), SH update cost dropped from ~10ms to <1ms per frame

Move SH color accumulation from per-splat to per-octree-node granularity,
using the existing changedAllocIds partial rendering mechanism to only
re-evaluate nodes whose viewing angle threshold is exceeded.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes spherical harmonics (SH) color updates for gaussian splats by switching from per-splat updates to per-octree-node updates and leveraging the existing partial work-buffer rendering path (changedAllocIds) to only re-evaluate nodes whose viewing angle threshold has been exceeded.

Changes:

  • Switch SH color update triggering to per-octree-node (octree) / distance-scaled per-splat (non-octree) logic and render only changed alloc blocks via partial rendering.
  • Repurpose colorUpdateAngle to mean viewing-angle threshold (degrees), and remove the old distance/LOD scale params via Debug.removed stubs.
  • Update examples to the new SH update parameter (colorUpdateAngle).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/scene/gsplat-unified/gsplat-work-buffer.js Extends renderColor to accept changedAllocIds for partial color-only updates.
src/scene/gsplat-unified/gsplat-params.js Repurposes colorUpdateAngle; removes legacy params via Debug.removed accessors.
src/scene/gsplat-unified/gsplat-octree-instance.js Adds per-node SH translation accumulator; exports NodeInfo.
src/scene/gsplat-unified/gsplat-manager.js Implements per-node/per-splat SH update thresholds and batches partial color updates by allocId.
src/scene/gsplat-unified/gsplat-info.js Updates nodeInfos typing and removes old accumulator reset helper.
examples/src/examples/gaussian-splatting/world.example.mjs Updates example configuration to use colorUpdateAngle.
examples/src/examples/gaussian-splatting/lod-streaming-sh.example.mjs Updates example configuration to use colorUpdateAngle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +989 to +993
const threshold = ratio * Math.max(1, dist);
if (splat.colorAccumulatedTranslation >= threshold) {
_changedColorAllocIds.add(splat.allocId);
uploadedBlocks += splat.intervalAllocIds.length;
splat.colorAccumulatedTranslation = 0;
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using >= here means colorUpdateAngle = 0 makes ratio/threshold 0 and will trigger color updates even when translationDelta is 0 (camera stationary). Switch to a strict > comparison (or guard on translationDelta > 0) to match the documented semantics and avoid unnecessary GPU work.

Copilot uses AI. Check for mistakes.
const nodeInfo = splat.nodeInfos[nodeIndices[j]];
nodeInfo.colorAccumulatedTranslation += translationDelta;
const threshold = ratio * Math.max(1, nodeInfo.worldDistance);
if (nodeInfo.colorAccumulatedTranslation >= threshold) {
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using >= here means colorUpdateAngle = 0 makes ratio/threshold 0 and will trigger SH color updates even when translationDelta is 0 (camera stationary). Switch to a strict > comparison (or guard on translationDelta > 0) to match the documented semantics and avoid unnecessary GPU work.

Copilot uses AI. Check for mistakes.
for (let j = 0; j < nodeIndices.length; j++) {
const nodeInfo = splat.nodeInfos[nodeIndices[j]];
nodeInfo.colorAccumulatedTranslation += translationDelta;
const threshold = ratio * Math.max(1, nodeInfo.worldDistance);
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeInfo.worldDistance is assigned in GSplatOctreeInstance.evaluateNodeLods using an FOV-compensated (and optionally behind-penalized) distance, not the geometric camera→node distance. Using it here to scale the SH viewing-angle threshold makes SH update frequency depend on camera FOV / behind-penalty settings, which is unexpected given colorUpdateAngle is documented as a viewing-angle threshold. Consider scaling by the true world-space distance to the node/AABB (or storing a separate non-FOV-adjusted distance on NodeInfo for SH updates).

Copilot uses AI. Check for mistakes.
@mvaligursky mvaligursky merged commit 322f4ea into main Apr 13, 2026
8 checks passed
@mvaligursky mvaligursky deleted the mv-per-node-sh-updates branch April 13, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants