perf: per-node granular SH color updates with angle-based threshold#8593
perf: per-node granular SH color updates with angle-based threshold#8593mvaligursky merged 2 commits intomainfrom
Conversation
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.
There was a problem hiding this comment.
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
colorUpdateAngleto mean viewing-angle threshold (degrees), and remove the old distance/LOD scale params viaDebug.removedstubs. - 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.
| const threshold = ratio * Math.max(1, dist); | ||
| if (splat.colorAccumulatedTranslation >= threshold) { | ||
| _changedColorAllocIds.add(splat.allocId); | ||
| uploadedBlocks += splat.intervalAllocIds.length; | ||
| splat.colorAccumulatedTranslation = 0; |
There was a problem hiding this comment.
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.
| const nodeInfo = splat.nodeInfos[nodeIndices[j]]; | ||
| nodeInfo.colorAccumulatedTranslation += translationDelta; | ||
| const threshold = ratio * Math.max(1, nodeInfo.worldDistance); | ||
| if (nodeInfo.colorAccumulatedTranslation >= threshold) { |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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).
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:
changedAllocIdspartial rendering to only re-evaluate nodes that exceed their viewing angle thresholdAPI Changes:
colorUpdateAnglerepurposed: now controls the viewing angle threshold in degrees for SH re-evaluation (default: 10). Previously controlled camera rotation threshold.colorUpdateDistanceremoved (Debug.removedstub, suggestscolorUpdateAngle)colorUpdateDistanceLodScaleremoved (Debug.removedstub, per-node scaling is automatic)colorUpdateAngleLodScaleremoved (Debug.removedstub, per-node scaling is automatic)Examples:
world.example.mjsandlod-streaming-sh.example.mjsto usecolorUpdateAnglePerformance:
GSplatWorkBufferRenderPassGPU traffic from re-evaluating all splats to only nearby nodes whose viewing angle changed, eliminating the texture cache limiter bottleneck during camera movement