diff --git a/src/core/renderer/tiles/TilesRendererBase.js b/src/core/renderer/tiles/TilesRendererBase.js index 4111e1644..8d3937919 100644 --- a/src/core/renderer/tiles/TilesRendererBase.js +++ b/src/core/renderer/tiles/TilesRendererBase.js @@ -885,6 +885,18 @@ export class TilesRendererBase { // remove any tiles that are loading but no longer used this.removeUnusedPendingTiles(); + const plugins = this.plugins; + for ( let i = 0, l = plugins.length; i < l; i ++ ) { + + const plugin = plugins[ i ]; + if ( plugin.pruneUnusedSubtrees ) { + + plugin.pruneUnusedSubtrees(); + + } + + } + // TODO: This will only sort for one tileset. We may want to store this queue on the // LRUCache so multiple tilesets can use it at once // start the downloads of the tiles as needed @@ -1347,6 +1359,18 @@ export class TilesRendererBase { ensureChildrenArePreprocessed( tile, forceImmediate = this.stats.tilesProcessed < this.maxTilesProcessed ) { + const plugins = this.plugins; + for ( let i = 0, l = plugins.length; i < l; i ++ ) { + + const plugin = plugins[ i ]; + if ( plugin.ensureChildrenAreExpanded ) { + + plugin.ensureChildrenAreExpanded( tile ); + + } + + } + const children = tile.children; if ( children.length === 0 || children[ children.length - 1 ].traversal ) { diff --git a/src/core/renderer/tiles/optimizedTraverseFunctions.js b/src/core/renderer/tiles/optimizedTraverseFunctions.js index 1725e8830..a02e31f07 100644 --- a/src/core/renderer/tiles/optimizedTraverseFunctions.js +++ b/src/core/renderer/tiles/optimizedTraverseFunctions.js @@ -48,8 +48,6 @@ function canUnconditionallyRefine( tile ) { // Resets the frame information for the given tile function resetFrameState( tile, renderer ) { - renderer.ensureChildrenArePreprocessed( tile ); - if ( tile.traversal.lastFrameVisited !== renderer.frameCount ) { tile.traversal.lastFrameVisited = renderer.frameCount; @@ -164,6 +162,8 @@ function canTraverse( tile, renderer ) { } + renderer.ensureChildrenArePreprocessed( tile ); + // Early out if the children haven't been processed, yet if ( ! areChildrenProcessed( tile ) ) { diff --git a/src/core/renderer/tiles/traverseFunctions.js b/src/core/renderer/tiles/traverseFunctions.js index 8283d815b..86171ff52 100644 --- a/src/core/renderer/tiles/traverseFunctions.js +++ b/src/core/renderer/tiles/traverseFunctions.js @@ -72,14 +72,19 @@ function resetFrameState( tile, renderer ) { // Recursively mark tiles used down to the next layer, skipping external tilesets function recursivelyMarkUsed( tile, renderer, cacheOnly = false ) { - renderer.ensureChildrenArePreprocessed( tile ); - resetFrameState( tile, renderer ); markUsed( tile, renderer, cacheOnly ); // don't traverse if the children have not been processed, yet but tileset content // should be considered to be "replaced" by the loaded children so await that here. - if ( canUnconditionallyRefine( tile ) && areChildrenProcessed( tile ) ) { + if ( canUnconditionallyRefine( tile ) ) { + + renderer.ensureChildrenArePreprocessed( tile ); + if ( ! areChildrenProcessed( tile ) ) { + + return; + + } const children = tile.children; for ( let i = 0, l = children.length; i < l; i ++ ) { @@ -95,8 +100,6 @@ function recursivelyMarkUsed( tile, renderer, cacheOnly = false ) { // Recursively traverses to the next tiles with unloaded renderable content to load them function recursivelyLoadNextRenderableTiles( tile, renderer ) { - renderer.ensureChildrenArePreprocessed( tile ); - // exit the recursion if the tile hasn't been used this frame if ( isUsedThisFrame( tile, renderer.frameCount ) ) { @@ -167,6 +170,8 @@ function canTraverse( tile, renderer ) { } + renderer.ensureChildrenArePreprocessed( tile ); + // Early out if the children haven't been processed, yet if ( ! areChildrenProcessed( tile ) ) { @@ -181,10 +186,6 @@ function canTraverse( tile, renderer ) { // Determine which tiles are used by the renderer given the current camera configuration function markUsedTiles( tile, renderer ) { - // determine frustum set is run first so we can ensure the preprocessing of all the necessary - // child tiles has happened here. - renderer.ensureChildrenArePreprocessed( tile ); - resetFrameState( tile, renderer ); if ( ! tile.traversal.inFrustum ) { diff --git a/src/three/plugins/images/ImageFormatPlugin.js b/src/three/plugins/images/ImageFormatPlugin.js index 2f24669be..9cbfcb433 100644 --- a/src/three/plugins/images/ImageFormatPlugin.js +++ b/src/three/plugins/images/ImageFormatPlugin.js @@ -146,13 +146,23 @@ export class ImageFormatPlugin { } - preprocessNode( tile ) { + ensureChildrenAreExpanded( tile ) { + + if ( ! ( TILE_LEVEL in tile ) ) { + + return; + + } - // generate children - const { tiling } = this; - const maxLevel = tiling.maxLevel; const level = tile[ TILE_LEVEL ]; - if ( level < maxLevel && tile.parent !== null ) { + if ( level === - 1 || level < this.tiling.maxLevel ) { + + // skip if all children are already present + if ( tile._imageChildrenComplete ) { + + return; + + } this.expandChildren( tile ); @@ -160,6 +170,19 @@ export class ImageFormatPlugin { } + pruneUnusedSubtrees() { + + const { tiles } = this; + if ( ! tiles.root ) { + + return; + + } + + this._pruneUnusedSubtrees( tiles.root ); + + } + disposeTile( tile ) { const tx = tile[ TILE_X ]; @@ -178,26 +201,19 @@ export class ImageFormatPlugin { // Local functions getTileset( baseUrl ) { - const { tiling, tiles } = this; - const minLevel = tiling.minLevel; - const { tileCountX, tileCountY } = tiling.getLevel( minLevel ); - - // generate all children for the root - const children = []; - for ( let x = 0; x < tileCountX; x ++ ) { - - for ( let y = 0; y < tileCountY; y ++ ) { - - const child = this.createChild( x, y, minLevel ); - if ( child !== null ) { - - children.push( child ); - - } + const { tiles } = this; + const root = { + refine: 'REPLACE', + geometricError: Infinity, + boundingVolume: this.createBoundingVolume( 0, 0, - 1 ), + children: [], - } + [ TILE_LEVEL ]: - 1, + [ TILE_X ]: 0, + [ TILE_Y ]: 0, + }; - } + this.expandChildren( root ); // generate tileset const tileset = { @@ -205,16 +221,7 @@ export class ImageFormatPlugin { version: '1.1' }, geometricError: Infinity, - root: { - refine: 'REPLACE', - geometricError: Infinity, - boundingVolume: this.createBoundingVolume( 0, 0, - 1 ), - children, - - [ TILE_LEVEL ]: - 1, - [ TILE_X ]: 0, - [ TILE_Y ]: 0, - } + root, }; tiles.preprocessTileset( tileset, baseUrl ); @@ -321,25 +328,185 @@ export class ImageFormatPlugin { expandChildren( tile ) { const level = tile[ TILE_LEVEL ]; - const x = tile[ TILE_X ]; - const y = tile[ TILE_Y ]; + const existingChildren = new Set(); + const { children } = tile; + for ( let i = 0, l = children.length; i < l; i ++ ) { + + const child = children[ i ]; + if ( TILE_LEVEL in child ) { + + existingChildren.add( `${ child[ TILE_LEVEL ] }:${ child[ TILE_X ] }:${ child[ TILE_Y ] }` ); + + } + + } + + const insertionPoint = children.length - ( tile.internal?.virtualChildCount || 0 ); + const pendingChildren = []; + if ( level === - 1 ) { + + const minLevel = this.tiling.minLevel; + const { tileCountX, tileCountY } = this.tiling.getLevel( minLevel ); + for ( let x = 0; x < tileCountX; x ++ ) { + + for ( let y = 0; y < tileCountY; y ++ ) { - const { tileSplitX, tileSplitY } = this.tiling.getLevel( level ); - for ( let cx = 0; cx < tileSplitX; cx ++ ) { + const key = `${ minLevel }:${ x }:${ y }`; + if ( existingChildren.has( key ) ) { - for ( let cy = 0; cy < tileSplitY; cy ++ ) { + continue; - const child = this.createChild( tileSplitX * x + cx, tileSplitY * y + cy, level + 1 ); - if ( child ) { + } - tile.children.push( child ); + const child = this.createChild( x, y, minLevel ); + if ( child ) { + + pendingChildren.push( child ); + + } } } + } else if ( level < this.tiling.maxLevel ) { + + const x = tile[ TILE_X ]; + const y = tile[ TILE_Y ]; + const { tileSplitX, tileSplitY } = this.tiling.getLevel( level ); + for ( let cx = 0; cx < tileSplitX; cx ++ ) { + + for ( let cy = 0; cy < tileSplitY; cy ++ ) { + + const childX = tileSplitX * x + cx; + const childY = tileSplitY * y + cy; + const childLevel = level + 1; + const key = `${ childLevel }:${ childX }:${ childY }`; + if ( existingChildren.has( key ) ) { + + continue; + + } + + const child = this.createChild( childX, childY, childLevel ); + if ( child ) { + + pendingChildren.push( child ); + + } + + } + + } + + } + + if ( pendingChildren.length > 0 ) { + + children.splice( insertionPoint, 0, ...pendingChildren ); + tile._imageChildrenComplete = false; + + } else { + + tile._imageChildrenComplete = true; + + } + + } + + _pruneUnusedSubtrees( tile ) { + + const { children } = tile; + for ( let i = 0; i < children.length; i ++ ) { + + const child = children[ i ]; + if ( ! ( TILE_LEVEL in child ) ) { + + continue; + + } + + if ( this._canPruneSubtree( child ) ) { + + this._disposeSubtree( child ); + children.splice( i, 1 ); + i --; + tile._imageChildrenComplete = false; + + } else { + + this._pruneUnusedSubtrees( child ); + + } + + } + + } + + _canPruneSubtree( tile ) { + + if ( this._isTileRetained( tile ) ) { + + return false; + + } + + const { children } = tile; + for ( let i = 0, l = children.length; i < l; i ++ ) { + + if ( ! this._canPruneSubtree( children[ i ] ) ) { + + return false; + + } + } + return true; + + } + + _isTileRetained( tile ) { + + const { tiles } = this; + return tiles.lruCache.has( tile ) || Boolean( tile.traversal && ( + tile.traversal.used || + tile.traversal.usedLastFrame || + tile.traversal.active || + tile.traversal.visible || + tile.traversal.wasSetActive || + tile.traversal.wasSetVisible + ) ); + + } + + _disposeSubtree( tile ) { + + const { tiles } = this; + const { children } = tile; + for ( let i = 0, l = children.length; i < l; i ++ ) { + + this._disposeSubtree( children[ i ] ); + + } + + tiles.processNodeQueue.remove( tile ); + const queuedIndex = tiles.queuedTiles.indexOf( tile ); + if ( queuedIndex !== - 1 ) { + + tiles.queuedTiles.splice( queuedIndex, 1 ); + + } + + tiles.usedSet.delete( tile ); + tiles.loadingTiles.delete( tile ); + tiles.activeTiles.delete( tile ); + tiles.visibleTiles.delete( tile ); + tiles.lruCache.remove( tile ); + + tile.parent = null; + tile.children.length = 0; + } } diff --git a/test/core/TilesRendererBase.test.js b/test/core/TilesRendererBase.test.js index 43d9388c9..60a924e24 100644 --- a/test/core/TilesRendererBase.test.js +++ b/test/core/TilesRendererBase.test.js @@ -1,4 +1,6 @@ import { TilesRendererBase } from '../../src/core/renderer'; +import { XYZTilesPlugin } from '../../src/three/plugins/images/EPSGTilesPlugin.js'; +import { TILE_LEVEL, TILE_X, TILE_Y } from '../../src/three/plugins/images/ImageFormatPlugin.js'; describe( 'TilesRendererBase', () => { @@ -34,4 +36,54 @@ describe( 'TilesRendererBase', () => { } ); + it( 'should prune and rebuild image tile children without duplicating retained nodes', async () => { + + const renderer = new TilesRendererBase(); + const plugin = new XYZTilesPlugin( { + url: 'https://example.com/{z}/{x}/{y}.png', + levels: 3, + } ); + + renderer.registerPlugin( plugin ); + await plugin.imageSource.init(); + + const root = { + children: [], + [ TILE_LEVEL ]: - 1, + [ TILE_X ]: 0, + [ TILE_Y ]: 0, + }; + + renderer.preprocessNode( root, '', null ); + renderer.rootTileset = { root }; + renderer.ensureChildrenArePreprocessed( root, true ); + + const tile = root.children[ 0 ]; + renderer.ensureChildrenArePreprocessed( tile, true ); + + const originalKeys = new Set( tile.children.map( child => `${ child[ TILE_LEVEL ] }:${ child[ TILE_X ] }:${ child[ TILE_Y ] }` ) ); + expect( originalKeys.size ).toBeGreaterThan( 1 ); + + root.traversal.used = true; + root.traversal.usedLastFrame = true; + tile.traversal.used = true; + tile.traversal.usedLastFrame = true; + + const retainedChild = tile.children[ 0 ]; + retainedChild.traversal.used = true; + retainedChild.traversal.usedLastFrame = true; + + plugin.pruneUnusedSubtrees(); + + expect( tile.children ).toHaveLength( 1 ); + expect( tile.children[ 0 ] ).toBe( retainedChild ); + + renderer.ensureChildrenArePreprocessed( tile, true ); + + const rebuiltKeys = tile.children.map( child => `${ child[ TILE_LEVEL ] }:${ child[ TILE_X ] }:${ child[ TILE_Y ] }` ); + expect( new Set( rebuiltKeys ) ).toEqual( originalKeys ); + expect( rebuiltKeys ).toHaveLength( originalKeys.size ); + + } ); + } );