From 15d34e15c7839d2a14e959077ad113e80bb573ae Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Wed, 18 Mar 2026 09:02:51 +0900 Subject: [PATCH 1/8] Add guards --- src/three/plugins/DebugTilesPlugin.js | 11 ++++++++--- src/three/plugins/TileFlatteningPlugin.js | 9 +++++++-- src/three/plugins/batched/BatchedTilesPlugin.js | 6 ++++++ src/three/plugins/fade/TilesFadePlugin.js | 6 ++++++ src/three/renderer/tiles/raycastTraverse.js | 6 ++++++ 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/three/plugins/DebugTilesPlugin.js b/src/three/plugins/DebugTilesPlugin.js index 66e9c9915..84b0d914d 100644 --- a/src/three/plugins/DebugTilesPlugin.js +++ b/src/three/plugins/DebugTilesPlugin.js @@ -324,7 +324,7 @@ export class DebugTilesPlugin { }; - this._onTileVisibilityChangeCB = ( { scene, tile, visible } ) => { + this._onTileVisibilityChangeCB = ( { tile, visible } ) => { this._onTileVisibilityChange( tile, visible ); @@ -367,7 +367,7 @@ export class DebugTilesPlugin { if ( result ) { - return true; + return; } @@ -647,7 +647,12 @@ export class DebugTilesPlugin { // update tile materials visibleTiles.forEach( tile => { - const scene = tile.engineData.scene; + const { scene } = tile.engineData; + if ( ! scene ) { + + return; + + } // create a random color per-tile let h, s, l; diff --git a/src/three/plugins/TileFlatteningPlugin.js b/src/three/plugins/TileFlatteningPlugin.js index 396da98f4..570284de5 100644 --- a/src/three/plugins/TileFlatteningPlugin.js +++ b/src/three/plugins/TileFlatteningPlugin.js @@ -97,9 +97,15 @@ export class TileFlatteningPlugin { _updateTile( tile ) { const { positionsUpdated, positionsMap, shapes, tiles } = this; + const { scene } = tile.engineData; positionsUpdated.add( tile ); - const scene = tile.engineData.scene; + if ( ! scene ) { + + return; + + } + if ( ! positionsMap.has( tile ) ) { // save the geometry positions for resetting after @@ -222,7 +228,6 @@ export class TileFlatteningPlugin { } ); - this.tiles.dispatchEvent( { type: 'needs-render' } ); } diff --git a/src/three/plugins/batched/BatchedTilesPlugin.js b/src/three/plugins/batched/BatchedTilesPlugin.js index 7591a75f4..c5ef02a90 100644 --- a/src/three/plugins/batched/BatchedTilesPlugin.js +++ b/src/three/plugins/batched/BatchedTilesPlugin.js @@ -169,6 +169,12 @@ export class BatchedTilesPlugin { setTileVisible( tile, visible ) { const scene = tile.engineData.scene; + if ( ! scene ) { + + return false; + + } + if ( visible ) { // Add tileset to the batched mesh if it hasn't been added already diff --git a/src/three/plugins/fade/TilesFadePlugin.js b/src/three/plugins/fade/TilesFadePlugin.js index 09889bbd5..7ae904cbc 100644 --- a/src/three/plugins/fade/TilesFadePlugin.js +++ b/src/three/plugins/fade/TilesFadePlugin.js @@ -399,6 +399,12 @@ export class TilesFadePlugin { // callback for fading to prevent tiles from being removed until the fade effect has completed setTileVisible( tile, visible ) { + if ( ! tile.engineData.scene ) { + + return false; + + } + const fadeManager = this._fadeManager; // track the fade state diff --git a/src/three/renderer/tiles/raycastTraverse.js b/src/three/renderer/tiles/raycastTraverse.js index ef49cd5a4..2e2003360 100644 --- a/src/three/renderer/tiles/raycastTraverse.js +++ b/src/three/renderer/tiles/raycastTraverse.js @@ -14,6 +14,12 @@ function distanceSort( a, b ) { function intersectTileScene( tile, raycaster, renderer, intersects ) { const { scene } = tile.engineData; + if ( ! scene ) { + + return; + + } + const didRaycast = renderer.invokeOnePlugin( plugin => plugin.raycastTile && plugin.raycastTile( tile, scene, raycaster, intersects ) ); if ( ! didRaycast ) { From dd3bf0b35dc8992cd9d5da3ed932b32fcf35b20a Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Wed, 18 Mar 2026 09:17:59 +0900 Subject: [PATCH 2/8] Adjust traversal functions --- src/core/renderer/tiles/optimizedTraverseFunctions.js | 7 ++++--- src/core/renderer/tiles/traverseFunctions.js | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/renderer/tiles/optimizedTraverseFunctions.js b/src/core/renderer/tiles/optimizedTraverseFunctions.js index 1725e8830..aa11830d2 100644 --- a/src/core/renderer/tiles/optimizedTraverseFunctions.js +++ b/src/core/renderer/tiles/optimizedTraverseFunctions.js @@ -434,7 +434,7 @@ function toggleTiles( tile, renderer ) { } - } else { + } else if ( tile.internal.hasContent ) { tile.traversal.active = false; @@ -449,7 +449,7 @@ function toggleTiles( tile, renderer ) { } // if the tile is loaded and in frustum we can mark it as visible - tile.traversal.visible = tile.internal.hasRenderableContent && tile.traversal.active && tile.traversal.inFrustum && tile.internal.loadingState === LOADED; + tile.traversal.visible = tile.traversal.active && tile.traversal.inFrustum && ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED || ! tile.internal.hasContent ); renderer.stats.used ++; if ( tile.traversal.inFrustum ) { @@ -488,7 +488,8 @@ function toggleTiles( tile, renderer ) { } // If the active or visible state changed then call the functions. - if ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) { + // Fire for tiles with loaded renderable content, or for empty tiles (no content at all). + if ( ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) || ! tile.internal.hasContent ) { if ( tile.traversal.wasSetActive !== setActive ) { diff --git a/src/core/renderer/tiles/traverseFunctions.js b/src/core/renderer/tiles/traverseFunctions.js index 8283d815b..38107090d 100644 --- a/src/core/renderer/tiles/traverseFunctions.js +++ b/src/core/renderer/tiles/traverseFunctions.js @@ -327,7 +327,7 @@ function markVisibleTiles( tile, renderer ) { // Request the tile contents or mark it as visible if we've found a leaf. if ( tile.traversal.isLeaf ) { - if ( tile.internal.loadingState === LOADED ) { + if ( tile.internal.loadingState === LOADED || ! tile.internal.hasContent ) { if ( tile.traversal.inFrustum ) { @@ -449,7 +449,8 @@ function toggleTiles( tile, renderer ) { } // If the active or visible state changed then call the functions. - if ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) { + // Fire for tiles with loaded renderable content, or for empty tiles (no content at all). + if ( ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) || ! tile.internal.hasContent ) { if ( tile.traversal.wasSetActive !== setActive ) { From e5b2235b2ce3596e6d512b762ee03aebc9a69d65 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 12:07:21 +0900 Subject: [PATCH 3/8] Updates --- .../tiles/optimizedTraverseFunctions.js | 13 ++++++++--- src/core/renderer/tiles/traverseFunctions.js | 23 ++++++++++++++++--- src/three/plugins/DebugTilesPlugin.js | 13 ++++++----- src/three/plugins/TileFlatteningPlugin.js | 8 +------ .../plugins/batched/BatchedTilesPlugin.js | 6 ----- src/three/plugins/fade/TilesFadePlugin.js | 6 ----- src/three/renderer/tiles/raycastTraverse.js | 6 ----- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/core/renderer/tiles/optimizedTraverseFunctions.js b/src/core/renderer/tiles/optimizedTraverseFunctions.js index e66293047..7f5809497 100644 --- a/src/core/renderer/tiles/optimizedTraverseFunctions.js +++ b/src/core/renderer/tiles/optimizedTraverseFunctions.js @@ -487,7 +487,7 @@ function toggleTiles( tile, renderer ) { } // if the tile is loaded and in frustum we can mark it as visible - tile.traversal.visible = tile.traversal.active && tile.traversal.inFrustum && ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED || ! tile.internal.hasContent ); + tile.traversal.visible = tile.traversal.active && tile.traversal.inFrustum && ( ! tile.internal.hasRenderableContent || tile.internal.loadingState === LOADED ); renderer.stats.used ++; if ( tile.traversal.inFrustum ) { @@ -526,8 +526,7 @@ function toggleTiles( tile, renderer ) { } // If the active or visible state changed then call the functions. - // Fire for tiles with loaded renderable content, or for empty tiles (no content at all). - if ( ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) || ! tile.internal.hasContent ) { + if ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) { if ( setActive ) { @@ -553,6 +552,14 @@ function toggleTiles( tile, renderer ) { } + } else if ( ! tile.internal.hasRenderableContent ) { + + if ( tile.traversal.wasSetVisible !== setVisible ) { + + renderer.invokeOnePlugin( plugin => plugin.setEmptyTileVisible && plugin.setEmptyTileVisible( tile, setVisible ) ); + + } + } tile.traversal.wasSetActive = setActive; diff --git a/src/core/renderer/tiles/traverseFunctions.js b/src/core/renderer/tiles/traverseFunctions.js index 38107090d..9259de57d 100644 --- a/src/core/renderer/tiles/traverseFunctions.js +++ b/src/core/renderer/tiles/traverseFunctions.js @@ -327,7 +327,7 @@ function markVisibleTiles( tile, renderer ) { // Request the tile contents or mark it as visible if we've found a leaf. if ( tile.traversal.isLeaf ) { - if ( tile.internal.loadingState === LOADED || ! tile.internal.hasContent ) { + if ( tile.internal.loadingState === LOADED ) { if ( tile.traversal.inFrustum ) { @@ -339,6 +339,16 @@ function markVisibleTiles( tile, renderer ) { tile.traversal.active = true; stats.active ++; + } else if ( ! tile.internal.hasRenderableContent ) { + + if ( tile.traversal.inFrustum ) { + + tile.traversal.visible = true; + + } + + tile.traversal.active = true; + } else if ( tile.internal.hasContent ) { renderer.queueTileForDownload( tile ); @@ -449,8 +459,7 @@ function toggleTiles( tile, renderer ) { } // If the active or visible state changed then call the functions. - // Fire for tiles with loaded renderable content, or for empty tiles (no content at all). - if ( ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) || ! tile.internal.hasContent ) { + if ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) { if ( tile.traversal.wasSetActive !== setActive ) { @@ -464,6 +473,14 @@ function toggleTiles( tile, renderer ) { } + } else if ( ! tile.internal.hasRenderableContent ) { + + if ( tile.traversal.wasSetVisible !== setVisible ) { + + renderer.invokeOnePlugin( plugin => plugin.setEmptyTileVisible && plugin.setEmptyTileVisible( tile, setVisible ) ); + + } + } tile.traversal.wasSetActive = setActive; diff --git a/src/three/plugins/DebugTilesPlugin.js b/src/three/plugins/DebugTilesPlugin.js index a263ed85f..75b607de0 100644 --- a/src/three/plugins/DebugTilesPlugin.js +++ b/src/three/plugins/DebugTilesPlugin.js @@ -648,12 +648,7 @@ export class DebugTilesPlugin { // update tile materials visibleTiles.forEach( tile => { - const { scene } = tile.engineData; - if ( ! scene ) { - - return; - - } + const scene = tile.engineData.scene; // create a random color per-tile let h, s, l; @@ -727,6 +722,12 @@ export class DebugTilesPlugin { } + setEmptyTileVisible( tile, visible ) { + + this._onTileVisibilityChange( tile, visible ); + + } + _onTileVisibilityChange( tile, visible ) { if ( this.displayParentBounds ) { diff --git a/src/three/plugins/TileFlatteningPlugin.js b/src/three/plugins/TileFlatteningPlugin.js index cbeb726fd..0675a5332 100644 --- a/src/three/plugins/TileFlatteningPlugin.js +++ b/src/three/plugins/TileFlatteningPlugin.js @@ -98,15 +98,9 @@ export class TileFlatteningPlugin { _updateTile( tile ) { const { positionsUpdated, positionsMap, shapes, tiles } = this; - const { scene } = tile.engineData; positionsUpdated.add( tile ); - if ( ! scene ) { - - return; - - } - + const scene = tile.engineData.scene; if ( ! positionsMap.has( tile ) ) { // save the geometry positions for resetting after diff --git a/src/three/plugins/batched/BatchedTilesPlugin.js b/src/three/plugins/batched/BatchedTilesPlugin.js index c496da67f..ade47055f 100644 --- a/src/three/plugins/batched/BatchedTilesPlugin.js +++ b/src/three/plugins/batched/BatchedTilesPlugin.js @@ -156,12 +156,6 @@ export class BatchedTilesPlugin { setTileVisible( tile, visible ) { const scene = tile.engineData.scene; - if ( ! scene ) { - - return false; - - } - if ( visible ) { // Add tileset to the batched mesh if it hasn't been added already diff --git a/src/three/plugins/fade/TilesFadePlugin.js b/src/three/plugins/fade/TilesFadePlugin.js index 7ae904cbc..09889bbd5 100644 --- a/src/three/plugins/fade/TilesFadePlugin.js +++ b/src/three/plugins/fade/TilesFadePlugin.js @@ -399,12 +399,6 @@ export class TilesFadePlugin { // callback for fading to prevent tiles from being removed until the fade effect has completed setTileVisible( tile, visible ) { - if ( ! tile.engineData.scene ) { - - return false; - - } - const fadeManager = this._fadeManager; // track the fade state diff --git a/src/three/renderer/tiles/raycastTraverse.js b/src/three/renderer/tiles/raycastTraverse.js index 2e2003360..ef49cd5a4 100644 --- a/src/three/renderer/tiles/raycastTraverse.js +++ b/src/three/renderer/tiles/raycastTraverse.js @@ -14,12 +14,6 @@ function distanceSort( a, b ) { function intersectTileScene( tile, raycaster, renderer, intersects ) { const { scene } = tile.engineData; - if ( ! scene ) { - - return; - - } - const didRaycast = renderer.invokeOnePlugin( plugin => plugin.raycastTile && plugin.raycastTile( tile, scene, raycaster, intersects ) ); if ( ! didRaycast ) { From 9e18062812df97829dcd0eb50598c13fe2705019 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 12:10:58 +0900 Subject: [PATCH 4/8] Add empty function --- src/core/renderer/tiles/TilesRendererBase.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/renderer/tiles/TilesRendererBase.js b/src/core/renderer/tiles/TilesRendererBase.js index 7b00a89ac..c33641270 100644 --- a/src/core/renderer/tiles/TilesRendererBase.js +++ b/src/core/renderer/tiles/TilesRendererBase.js @@ -1319,6 +1319,10 @@ export class TilesRendererBase { } + setEmptyTileVisible( tile, visible ) { + + } + setTileVisible( tile, visible ) { visible ? this.visibleTiles.add( tile ) : this.visibleTiles.delete( tile ); From 3df1657f5001b10e5fb6354bfcdfe31fd3c6b12b Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 12:17:44 +0900 Subject: [PATCH 5/8] Cleanup --- src/core/renderer/tiles/optimizedTraverseFunctions.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/renderer/tiles/optimizedTraverseFunctions.js b/src/core/renderer/tiles/optimizedTraverseFunctions.js index 7f5809497..4cb9fa8dd 100644 --- a/src/core/renderer/tiles/optimizedTraverseFunctions.js +++ b/src/core/renderer/tiles/optimizedTraverseFunctions.js @@ -463,10 +463,6 @@ function toggleTiles( tile, renderer ) { } - } else if ( tile.internal.hasContent ) { - - tile.traversal.active = false; - } // when loading parent tiles as fallbacks, keep all used tiles downloaded From 0f188356e5b5bb765342653454cb9db9bbd11dec Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 12:22:11 +0900 Subject: [PATCH 6/8] Updates --- src/core/renderer/tiles/TilesRendererBase.js | 3 +++ src/core/renderer/tiles/optimizedTraverseFunctions.js | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/renderer/tiles/TilesRendererBase.js b/src/core/renderer/tiles/TilesRendererBase.js index c33641270..0ebeda7da 100644 --- a/src/core/renderer/tiles/TilesRendererBase.js +++ b/src/core/renderer/tiles/TilesRendererBase.js @@ -1321,6 +1321,9 @@ export class TilesRendererBase { setEmptyTileVisible( tile, visible ) { + // callback specifically for indicating whether an "empty", non geometry tile is visible. Ued specifically + // for the DebugTilesPlugin to show intermediate parent tiles. + } setTileVisible( tile, visible ) { diff --git a/src/core/renderer/tiles/optimizedTraverseFunctions.js b/src/core/renderer/tiles/optimizedTraverseFunctions.js index 4cb9fa8dd..d32a55876 100644 --- a/src/core/renderer/tiles/optimizedTraverseFunctions.js +++ b/src/core/renderer/tiles/optimizedTraverseFunctions.js @@ -494,7 +494,7 @@ function toggleTiles( tile, renderer ) { } - if ( isUsed || isProcessed( tile ) && tile.traversal?.usedLastFrame ) { + if ( isUsed || isProcessed( tile ) && tile.traversal.usedLastFrame ) { let setActive = false; let setVisible = false; @@ -550,6 +550,7 @@ function toggleTiles( tile, renderer ) { } else if ( ! tile.internal.hasRenderableContent ) { + // Empty tiles are not tracked in activeTiles so setTileActive is not called here. if ( tile.traversal.wasSetVisible !== setVisible ) { renderer.invokeOnePlugin( plugin => plugin.setEmptyTileVisible && plugin.setEmptyTileVisible( tile, setVisible ) ); From a882f113cf3fd9d47c61e36b597a3489ac503634 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 14:04:23 +0900 Subject: [PATCH 7/8] Demo update --- example/three/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example/three/index.js b/example/three/index.js index c61d7deff..a2dcbe2d1 100644 --- a/example/three/index.js +++ b/example/three/index.js @@ -56,6 +56,8 @@ const params = { errorTarget: 6, maxDepth: 15, + loadSiblings: true, + loadAncestors: true, displayActiveTiles: false, resolutionScale: 1.0, @@ -270,7 +272,9 @@ function init() { const tileOptions = gui.addFolder( 'Tiles Options' ); tileOptions.add( params, 'displayActiveTiles' ); tileOptions.add( params, 'errorTarget' ).min( 0 ).max( 50 ); - tileOptions.add( params, 'maxDepth' ).min( 1 ).max( 100 ); + tileOptions.add( params, 'maxDepth' ).min( 1 ).max( 100 ).step( 1 ); + tileOptions.add( params, 'loadAncestors' ); + tileOptions.add( params, 'loadSiblings' ); tileOptions.add( params, 'up', [ '+Y', '+Z', '-Z' ] ); tileOptions.open(); @@ -460,6 +464,8 @@ function animate() { tiles.errorTarget = params.errorTarget; tiles.displayActiveTiles = params.displayActiveTiles; tiles.maxDepth = params.maxDepth; + tiles.loadAncestors = params.loadAncestors; + tiles.loadSiblings = params.loadSiblings; // update plugin const plugin = tiles.getPluginByName( 'DEBUG_TILES_PLUGIN' ); From 8c9a274d190557e9168328bf03204875e10bc211 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 22 May 2026 14:13:56 +0900 Subject: [PATCH 8/8] Fix optimizedLoadStrategy warning --- src/core/renderer/tiles/TilesRendererBase.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/renderer/tiles/TilesRendererBase.js b/src/core/renderer/tiles/TilesRendererBase.js index 0ebeda7da..2a93409ff 100644 --- a/src/core/renderer/tiles/TilesRendererBase.js +++ b/src/core/renderer/tiles/TilesRendererBase.js @@ -405,8 +405,13 @@ export class TilesRendererBase { set optimizedLoadStrategy( v ) { - console.warn( 'TilesRenderer: "optimizedLoadStrategy" has been deprecated. Please toggle "loadAncestors" to adjust the tile load behavior.' ); - this._optimizedLoadStrategy = v; + if ( v !== this._optimizedLoadStrategy ) { + + console.warn( 'TilesRenderer: "optimizedLoadStrategy" has been deprecated. Please toggle "loadAncestors" to adjust the tile load behavior.' ); + this._optimizedLoadStrategy = v; + + } + }