From 903c8078c9105b58b768f4cd6a9cdf3186996774 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Wed, 4 Mar 2026 12:14:58 +0000 Subject: [PATCH] Fix GSplat LOD re-evaluation when params change via frame:ready event The frame:ready event fires at the end of GSplatManager.update(), but the dirty flag set by event listeners was immediately cleared by frameEnd() before the next update could act on it. This meant changing LOD range in response to streaming completion had no effect until the camera moved. Now, after firing frame:ready, if params were dirtied by listeners, octree instances are marked for LOD re-evaluation so the new range takes effect on the next full update. Also updates the lod-streaming example to demonstrate progressive loading: start with lowest LOD only, then expand to full quality range once the initial low-quality data finishes streaming. Made-with: Cursor --- .../lod-streaming.example.mjs | 19 +++++++++++++++++++ src/scene/gsplat-unified/gsplat-manager.js | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs index 83c9ad81333..6d1203872e1 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs @@ -173,6 +173,25 @@ assetListLoader.load(() => { }; applyPreset(); + + // Start with lowest LOD only for fast initial load + const lodLevels = gs.resource?.octree?.lodLevels; + if (lodLevels) { + const worstLod = lodLevels - 1; + app.scene.gsplat.lodRangeMin = worstLod; + app.scene.gsplat.lodRangeMax = worstLod; + } + + // When lowest LOD is fully loaded, switch to the normal quality range + const gsplatSystem = /** @type {any} */ (app.systems.gsplat); + const onFrameReady = (/** @type {any} */ camera, /** @type {any} */ layer, /** @type {boolean} */ ready, /** @type {number} */ loadingCount) => { + if (ready && loadingCount === 0) { + gsplatSystem.off('frame:ready', onFrameReady); + applyPreset(); + } + }; + gsplatSystem.on('frame:ready', onFrameReady); + data.on('lodPreset:set', applyPreset); const applySplatBudget = () => { diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index ad1ef4a92c6..938a15bc306 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -1530,6 +1530,13 @@ class GSplatManager { // fire frame:ready event this.fireFrameReadyEvent(); + // If event listeners dirtied params (e.g. changed LOD range), ensure LOD is re-evaluated + if (this.scene.gsplat.dirty) { + for (const [, inst] of this.octreeInstances) { + inst.needsLodUpdate = true; + } + } + // return the number of active splats for stats return sortedState ? sortedState.totalActiveSplats : 0; }