diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index 6bd66f871..3096a732b 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -11,6 +11,7 @@ import { TilesFadePlugin, UpdateOnChangePlugin, TileCompressionPlugin, + UnloadTilesPlugin, } from '3d-tiles-renderer/plugins'; import { Scene, @@ -63,6 +64,7 @@ function reinstantiateTiles() { tiles.registerPlugin( new GoogleCloudAuthPlugin( { apiToken: params.apiKey, autoRefreshToken: true } ) ); tiles.registerPlugin( new TileCompressionPlugin() ); tiles.registerPlugin( new UpdateOnChangePlugin() ); + tiles.registerPlugin( new UnloadTilesPlugin() ); if ( params.useBatchedMesh ) { diff --git a/src/plugins/README.md b/src/plugins/README.md index 4c5ec6a13..e8c21753c 100644 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -539,6 +539,37 @@ transformLatLonHeightToOrigin( lat, lon, height = 0 ) : void Transforms the centers the tile set such that the given coordinates and height are positioned at the origin with "X" facing west and "Z" facing north. +## UnloadTilesPlugin + +Plugin that unloads geometry, textures, and materials of any given tile when the visibility changes to non-visible to save GPU memory. The model still exists on the CPU until it is completely removed from the cache. + +### .estimatedGpuBytes + +```js +estimatedGPUBytes : number +``` + +The number of bytes that are actually uploaded to the GPU for rendering compared to `lruCache.cachedBytes` which reports the amount of texture and geometry buffer bytes actually downloaded. + +### .constructor + +```js +constructor( options : Object ) +``` + +Available options are as follows: + +```js +{ + // The amount of time to wait in milliseconds before unloading tile content from the GPU. This option can be + // used to account for cases where the user is moving the camera and tiles are coming in and out of frame. + delay: 0, + + // The amount of bytes to unload to. + bytesTarget: 0, +} +``` + ## BatchedTilesPlugin _available in the examples directory_ @@ -581,36 +612,3 @@ Available options are as follows: discardOriginalContent: true } ``` - -## UnloadTilesPlugin - -_available in the examples directory_ - -Plugin that unloads geometry, textures, and materials of any given tile when the visibility changes to non-visible to save GPU memory. The model still exists on the CPU until it is completely removed from the cache. - -### .estimatedGpuBytes - -```js -estimatedGPUBytes : number -``` - -The number of bytes that are actually uploaded to the GPU for rendering compared to `lruCache.cachedBytes` which reports the amount of texture and geometry buffer bytes actually downloaded. - -### .constructor - -```js -constructor( options : Object ) -``` - -Available options are as follows: - -```js -{ - // The amount of time to wait in milliseconds before unloading tile content from the GPU. This option can be - // used to account for cases where the user is moving the camera and tiles are coming in and out of frame. - delay: 0, - - // The amount of bytes to unload to. - bytesTarget: 0, -} -``` diff --git a/src/plugins/index.d.ts b/src/plugins/index.d.ts index d40cabcce..fa4655004 100644 --- a/src/plugins/index.d.ts +++ b/src/plugins/index.d.ts @@ -5,6 +5,7 @@ export { UpdateOnChangePlugin } from './three/UpdateOnChangePlugin'; export { TileCompressionPlugin } from './three/TileCompressionPlugin'; export { GLTFExtensionsPlugin } from './three/GLTFExtensionsPlugin'; export { ReorientationPlugin } from './three/ReorientationPlugin'; +export { UnloadTilesPlugin } from './three/UnloadTilesPlugin'; export { TilesFadePlugin } from './three/fade/TilesFadePlugin'; export * from './three/DebugTilesPlugin'; diff --git a/src/plugins/index.js b/src/plugins/index.js index 2cffdad59..0093e67a9 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -5,6 +5,7 @@ export { UpdateOnChangePlugin } from './three/UpdateOnChangePlugin.js'; export { TileCompressionPlugin } from './three/TileCompressionPlugin.js'; export { GLTFExtensionsPlugin } from './three/GLTFExtensionsPlugin.js'; export { ReorientationPlugin } from './three/ReorientationPlugin.js'; +export { UnloadTilesPlugin } from './three/UnloadTilesPlugin.js'; export { TilesFadePlugin } from './three/fade/TilesFadePlugin.js'; export * from './three/DebugTilesPlugin.js'; diff --git a/src/plugins/three/UnloadTilesPlugin.d.ts b/src/plugins/three/UnloadTilesPlugin.d.ts new file mode 100644 index 000000000..5d4ec4524 --- /dev/null +++ b/src/plugins/three/UnloadTilesPlugin.d.ts @@ -0,0 +1,8 @@ +export class UnloadTilesPlugin { + + constructor( options: { + delay: number, + bytesTarget: number, + } ); + +} diff --git a/example/src/plugins/UnloadTilesPlugin.js b/src/plugins/three/UnloadTilesPlugin.js similarity index 99% rename from example/src/plugins/UnloadTilesPlugin.js rename to src/plugins/three/UnloadTilesPlugin.js index 3abb56e84..a8fbfe478 100644 --- a/example/src/plugins/UnloadTilesPlugin.js +++ b/src/plugins/three/UnloadTilesPlugin.js @@ -36,7 +36,7 @@ export class UnloadTilesPlugin { } - constructor( options ) { + constructor( options = {} ) { const { delay = 0, diff --git a/src/plugins/three/fade/FadeManager.js b/src/plugins/three/fade/FadeManager.js index 778d5296a..50abb9d86 100644 --- a/src/plugins/three/fade/FadeManager.js +++ b/src/plugins/three/fade/FadeManager.js @@ -186,6 +186,8 @@ export class FadeManager { const fadeState = this._fadeState; if ( ! fadeState.has( object ) ) return; + const visible = fadeState.get( object ).fadeOutTarget === 0; + fadeState.delete( object ); object.traverse( child => { @@ -204,7 +206,7 @@ export class FadeManager { if ( this.onFadeComplete ) { - this.onFadeComplete( object ); + this.onFadeComplete( object, visible ); } @@ -291,6 +293,19 @@ export class FadeManager { } + isFading( object ) { + + return this._fadeState.has( object ); + + } + + isFadingOut( object ) { + + const state = this._fadeState.get( object ); + return state && state.fadeOutTarget === 1; + + } + // Tick the fade timer for each actively fading object update() { diff --git a/src/plugins/three/fade/TilesFadePlugin.js b/src/plugins/three/fade/TilesFadePlugin.js index ebb71b5d6..6580f7219 100644 --- a/src/plugins/three/fade/TilesFadePlugin.js +++ b/src/plugins/three/fade/TilesFadePlugin.js @@ -1,4 +1,4 @@ -import { Group, Matrix4, Vector3, Quaternion } from 'three'; +import { Matrix4, Vector3, Quaternion } from 'three'; import { FadeManager } from './FadeManager.js'; const HAS_POPPED_IN = Symbol( 'HAS_POPPED_IN' ); @@ -14,10 +14,17 @@ function onTileVisibilityChange( scene, tile, visible ) { // it's possible we disable them when adjusting visibility based on frustum scene.visible = true; + const fadeManager = this._fadeManager; + if ( fadeManager.isFadingOut( scene ) ) { + + this._fadingOutCount --; + + } + if ( ! visible ) { - this._fadeGroup.add( scene ); - this._fadeManager.fadeOut( scene ); + this._fadingOutCount ++; + fadeManager.fadeOut( scene ); } else { @@ -59,12 +66,15 @@ function onDisposeModel( scene ) { } -function onFadeComplete( object ) { +function onFadeComplete( object, visible ) { + + if ( ! visible ) { - // when the fade finishes ensure we dispose the tile and remove it from the fade group - if ( object.parent === this._fadeGroup ) { + // now that the tile is hidden we can run the built-in setTileVisible function for the tile + const tile = this._tileMap.get( object ); + this.tiles.invokeOnePlugin( plugin => plugin !== this && plugin.setTileVisible && plugin.setTileVisible( tile, false ) ); - this._fadeGroup.remove( object ); + this._fadingOutCount --; } @@ -101,7 +111,6 @@ function onUpdateBefore() { function onUpdateAfter() { const fadeManager = this._fadeManager; - const fadeGroup = this._fadeGroup; const displayActiveTiles = this._displayActiveTiles; const fadingBefore = this._fadingBefore; const tiles = this.tiles; @@ -131,13 +140,24 @@ function onUpdateAfter() { tiles.visibleTiles.forEach( t => { - t.cached.scene.visible = t.__inFrustum; + // if a tile is fading out then it may not be traversed and thus will not have + // the frustum flag set correctly. + const scene = t.cached.scene; + if ( fadeManager.isFadingOut( scene ) ) { + + scene.visible = true; + + } else { + + scene.visible = t.__inFrustum; + + } } ); } - if ( this.maximumFadeOutTiles < fadeGroup.children.length ) { + if ( this.maximumFadeOutTiles < this._fadingOutCount ) { // determine whether all the rendering cameras are moving // quickly so we can adjust how tiles fade accordingly @@ -221,12 +241,13 @@ export class TilesFadePlugin { }; this.name = 'FADE_TILES_PLUGIN'; + this.priority = - 1; this.tiles = null; this._fadeManager = new FadeManager(); this._prevCameraTransforms = null; - this._fadeGroup = null; this._tileMap = null; + this._fadingOutCount = 0; this.maximumFadeOutTiles = options.maximumFadeOutTiles; this.fadeRootTiles = options.fadeRootTiles; @@ -236,10 +257,6 @@ export class TilesFadePlugin { init( tiles ) { - const fadeGroup = new Group(); - fadeGroup.name = 'TilesFadeGroup'; - tiles.group.add( fadeGroup ); - const fadeManager = this._fadeManager; fadeManager.onFadeSetStart = () => { @@ -259,7 +276,6 @@ export class TilesFadePlugin { this.tiles = tiles; this._fadeManager = fadeManager; - this._fadeGroup = fadeGroup; this._tileMap = new Map(); this._prevCameraTransforms = new Map(); @@ -277,7 +293,6 @@ export class TilesFadePlugin { this._onLoadModel = e => onLoadModel.call( this, e.scene, e.tile ); this._onDisposeModel = e => onDisposeModel.call( this, e.scene ); - this._onTileVisibilityChange = e => onTileVisibilityChange.call( this, e.scene, e.tile, e.visible ); this._onAddCamera = e => onAddCamera.call( this, e.camera ); this._onDeleteCamera = e => onDeleteCamera.call( this, e.camera ); this._onUpdateBefore = () => onUpdateBefore.call( this ); @@ -285,7 +300,6 @@ export class TilesFadePlugin { tiles.addEventListener( 'load-model', this._onLoadModel ); tiles.addEventListener( 'dispose-model', this._onDisposeModel ); - tiles.addEventListener( 'tile-visibility-change', this._onTileVisibilityChange ); tiles.addEventListener( 'add-camera', this._onAddCamera ); tiles.addEventListener( 'delete-camera', this._onDeleteCamera ); tiles.addEventListener( 'update-before', this._onUpdateBefore ); @@ -293,12 +307,40 @@ export class TilesFadePlugin { } + setTileVisible( tile, visible ) { + + const scene = tile.cached.scene; + const wasFading = this._fadeManager.isFading( scene ); + onTileVisibilityChange.call( this, scene, tile, visible ); + + // if a tile was already fading then it's already marked as visible and in the scene + if ( wasFading ) { + + return true; + + } + + // cancel the visibility change trigger because we're fading and will call this after + // fade completes. + const isFading = this._fadeManager.isFading( scene ); + if ( ! visible && isFading ) { + + return true; + + } + + return false; + + } + dispose() { const tiles = this.tiles; + + this._fadeManager.completeAllFades(); + tiles.removeEventListener( 'load-model', this._onLoadModel ); tiles.removeEventListener( 'dispose-model', this._onDisposeModel ); - tiles.removeEventListener( 'tile-visibility-change', this._onTileVisibilityChange ); tiles.removeEventListener( 'add-camera', this._onAddCamera ); tiles.removeEventListener( 'delete-camera', this._onDeleteCamera ); tiles.removeEventListener( 'update-before', this._onUpdateBefore ); @@ -311,8 +353,6 @@ export class TilesFadePlugin { } ); - this._fadeGroup.removeFromParent(); - } }