diff --git a/example/landformSite.html b/example/landformSiteOverlay.html similarity index 72% rename from example/landformSite.html rename to example/landformSiteOverlay.html index 0faeb3ec..a3f3d7b3 100644 --- a/example/landformSite.html +++ b/example/landformSiteOverlay.html @@ -45,10 +45,10 @@
- Set of tiles from M2020 Drive 1004 generated using NASA JPL's Landform. + Set of tiles from M20 Drive 1004 generated using NASA JPL's Landform with generated slope magnitude overlay.
- See more tile set information here. + See information on this tile sets generation here.
- + diff --git a/example/landformSite.js b/example/landformSiteOverlay.js similarity index 89% rename from example/landformSite.js rename to example/landformSiteOverlay.js index df5a889e..a309a6be 100644 --- a/example/landformSite.js +++ b/example/landformSiteOverlay.js @@ -1,7 +1,7 @@ import { TilesRenderer, EnvironmentControls, -} from '..'; +} from '../src/index.js'; import { Scene, WebGLRenderer, @@ -12,8 +12,8 @@ import { } from 'three'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; import { JPLLandformSiteSceneLoader } from './src/jpl/JPLLandformSceneLoader.js'; -import { TextureOverlayTilesRendererMixin } from './src/plugins/overlays/TextureOverlayTilesRenderer.js'; import { TextureOverlayMaterialMixin } from './src/plugins/overlays/TextureOverlayMaterial.js'; +import { TextureOverlayPlugin } from './src/plugins/overlays/TextureOverlayPlugin.js'; const URLS = [ @@ -29,16 +29,11 @@ const URLS = [ 'NLF_0477_0709298393M010RAS_N0261004NCAM13477_0A0195J02/NLF_0477_0709298393M010RAS_N0261004NCAM13477_0A0195J02_scene.json', // 'NLFS0498_0711156087M000RAS_N0261004NCAM00607_0A0095J01/NLFS0498_0711156087M000RAS_N0261004NCAM00607_0A0095J01_scene.json', - // 'NLF_0482_0709734873M194RAS_N0261004NCAM00347_0A0195J02/NLF_0482_0709734873M194RAS_N0261004NCAM00347_0A0195J02_scene.json', // 'NLF_0482_0709735996M816RAS_N0261004NCAM00709_0A0095J02/NLF_0482_0709735996M816RAS_N0261004NCAM00709_0A0095J02_scene.json', - // 'NLF_0490_0710456117M926RAS_N0261004NCAM00709_0A0095J03/NLF_0490_0710456117M926RAS_N0261004NCAM00709_0A0095J03_scene.json', - // 'NLF_0491_0710536867M784RAS_N0261004NCAM00709_0A0095J02/NLF_0491_0710536867M784RAS_N0261004NCAM00709_0A0095J02_scene.json', - // 'NLF_0495_0710900102M755RAS_N0261004NCAM00709_0A0095J02/NLF_0495_0710900102M755RAS_N0261004NCAM00709_0A0095J02_scene.json', - // 'NLF_0499_0711256332M612RAS_N0261004NCAM00347_0A1195J03/NLF_0499_0711256332M612RAS_N0261004NCAM00347_0A1195J03_scene.json', ].map( n => { @@ -74,7 +69,7 @@ function init() { renderer.domElement.tabIndex = 1; camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.025, 4000 ); - camera.position.set( 20, 10, 20 ); + camera.position.set( - 20, 10, 20 ); camera.lookAt( 0, 0, 0 ); // controls @@ -113,41 +108,38 @@ function init() { const tokens = url.split( /[\\/]/g ); tokens.pop(); - const TextureOverlayTilesRenderer = TextureOverlayTilesRendererMixin( TilesRenderer ); const TextureOverlayMaterial = TextureOverlayMaterialMixin( MeshBasicMaterial ); scene.tilesets.forEach( info => { const url = [ ...tokens, `${ info.id }_tileset.json` ].join( '/' ); - const tiles = new TextureOverlayTilesRenderer( url ); - - // ensure all materials support overlay textures - tiles.addEventListener( 'load-model', ( { tile, scene } )=> { + const tiles = new TilesRenderer( url ); + const plugin = new TextureOverlayPlugin( ( scene, tile, plugin ) => { scene.traverse( c => { if ( c.material ) { - const newMaterial = new TextureOverlayMaterial(); - newMaterial.copy( c.material ); - newMaterial.textures = Object.values( tiles.getTexturesForTile( tile ) ); - newMaterial.displayAsOverlay = params.slopeDisplay === 'OVERLAY'; - c.material = newMaterial; + c.material.textures = Object.values( plugin.getTexturesForTile( tile ) ); + c.material.displayAsOverlay = params.slopeDisplay === 'OVERLAY'; + c.material.needsUpdate = true; } } ); } ); + tiles.registerPlugin( plugin ); - // assign the texture layers - tiles.addEventListener( 'layer-textures-change', ( { tile, scene } ) => { + // ensure all materials support overlay textures + tiles.addEventListener( 'load-model', ( { tile, scene } )=> { scene.traverse( c => { if ( c.material ) { - c.material.textures = Object.values( tiles.getTexturesForTile( tile ) ); - c.material.needsUpdate = true; + const newMaterial = new TextureOverlayMaterial(); + newMaterial.copy( c.material ); + c.material = newMaterial; } @@ -186,9 +178,10 @@ function init() { tileSets.forEach( t => { - if ( ! t.hasLayer( 'slopeLayer' ) ) { + const plugin = t.getPluginByName( 'TEXTURE_OVERLAY_PLUGIN' ); + if ( ! plugin.hasLayer( 'slopeLayer' ) ) { - t.registerLayer( 'slopeLayer', layerFunction ); + plugin.registerLayer( 'slopeLayer', layerFunction ); } @@ -211,7 +204,12 @@ function init() { } else { - tileSets.forEach( t => t.unregisterLayer( 'slopeLayer' ) ); + tileSets.forEach( t => { + + const plugin = t.getPluginByName( 'TEXTURE_OVERLAY_PLUGIN' ); + plugin.unregisterLayer( 'slopeLayer' ); + + } ); } diff --git a/example/src/plugins/overlays/TextureOverlayTilesRenderer.js b/example/src/plugins/overlays/TextureOverlayPlugin.js similarity index 73% rename from example/src/plugins/overlays/TextureOverlayTilesRenderer.js rename to example/src/plugins/overlays/TextureOverlayPlugin.js index 3bbb2bec..1ef11a8a 100644 --- a/example/src/plugins/overlays/TextureOverlayTilesRenderer.js +++ b/example/src/plugins/overlays/TextureOverlayPlugin.js @@ -1,10 +1,5 @@ import { TextureLoader, ImageBitmapLoader } from 'three'; -import { PriorityQueue } from '../../../../src'; - -// TODO: Enable TilesRenderer to delay load model events until all textures have loaded -// TODO: Load textures while the tile geometry is loading - can we start this sooner than parse tile? -// TODO: What happens if a tile starts loading and then a layer is added, meaning it's not in the "loaded tiles" callback -// or active function and we haven't caught it in the parseTile function. Additional callback? Store the loading models? +import { PriorityQueue } from '../../../..'; function canUseImageBitmap() { @@ -185,20 +180,31 @@ class TextureCache { } -export const TextureOverlayTilesRendererMixin = base => class extends base { +export class TextureOverlayPlugin { + + constructor( assignCallback ) { + + this.name = 'TEXTURE_OVERLAY_PLUGIN'; + this.caches = null; + this.queue = null; + this.tiles = null; + this.assignCallback = assignCallback; - constructor( ...args ) { + } + + // plugin functions + init( tiles ) { - super( ...args ); + this.tiles = tiles; this.caches = {}; this.queue = new PriorityQueue(); this.queue.priorityCallback = ( a, b ) => { - return this.downloadQueue.priorityCallback( a, b ); + return tiles.downloadQueue.priorityCallback( a, b ); }; - this.addEventListener( 'dispose-model', ( { tile } ) => { + this._disposeModelCallback = ( { tile } ) => { const caches = this.caches; for ( const key in caches ) { @@ -208,11 +214,21 @@ export const TextureOverlayTilesRendererMixin = base => class extends base { } - } ); + }; + + this._assignTexturesCallback = ( { tile, scene } ) => { + + this.assignCallback( scene, tile, this ); + + }; + + tiles.addEventListener( 'dispose-model', this._disposeModelCallback ); + tiles.addEventListener( 'load-model', this._assignTexturesCallback ); + tiles.addEventListener( 'layer-textures-change', this._assignTexturesCallback ); } - _pluginProcessTileModel( scene, tile ) { + processTileModel( scene, tile ) { const caches = this.caches; const promises = []; @@ -231,9 +247,24 @@ export const TextureOverlayTilesRendererMixin = base => class extends base { } + dispose() { + + const { caches, tiles } = this; + Object.keys( caches ).forEach( key => { + + this.unregisterLayer( key ); + + } ); + + tiles.removeEventListener( 'dispose-model', this._disposeModelCallback ); + tiles.removeEventListener( 'load-model', this._assignTexturesCallback ); + tiles.removeEventListener( 'layer-textures-change', this._assignTexturesCallback ); + + } + + // public functions getTileKey( tile ) { - // TODO return tile.content.uri; } @@ -268,35 +299,20 @@ export const TextureOverlayTilesRendererMixin = base => class extends base { if ( name in this.caches ) { - throw new Error(); + throw new Error( `TextureOverlayPlugin: Texture overlay named ${ name } already exists.` ); } + const tiles = this.tiles; const cache = new TextureCache( customTextureCallback, this.queue ); - cache.fetchOptions = this.fetchOptions; + cache.fetchOptions = tiles.fetchOptions; this.caches[ name ] = cache; - this.forEachLoadedModel( ( scene, tile ) => { + tiles.forEachLoadedModel( ( scene, tile ) => { cache .loadTexture( this.getTileKey( tile ) ) - .then( texture => { - - this.dispatchEvent( { - type: 'load-layer-texture', - layer: name, - tile, - scene, - texture, - } ); - - this.dispatchEvent( { - type: 'layer-textures-change', - tile, - scene, - } ); - - } ) + .then( () => this.assignCallback( scene, tile, this ) ) .catch( () => {} ); } ); @@ -305,30 +321,19 @@ export const TextureOverlayTilesRendererMixin = base => class extends base { unregisterLayer( name ) { + const tiles = this.tiles; const caches = this.caches; if ( name in caches ) { const cache = caches[ name ]; delete caches[ name ]; - this.forEachLoadedModel( ( scene, tile ) => { + tiles.forEachLoadedModel( ( scene, tile ) => { const texture = cache.getTexture( this.getTileKey( tile ) ); if ( texture ) { - this.dispatchEvent( { - type: 'delete-layer-texture', - layer: name, - tile, - scene, - texture, - } ); - - this.dispatchEvent( { - type: 'layer-textures-change', - tile, - scene, - } ); + this.assignCallback( scene, tile, this ); } @@ -340,5 +345,4 @@ export const TextureOverlayTilesRendererMixin = base => class extends base { } -}; - +} diff --git a/example/textureOverlay.html b/example/textureOverlay.html deleted file mode 100644 index 7f865526..00000000 --- a/example/textureOverlay.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - Texture Overlays - - - - - - - diff --git a/example/textureOverlay.js b/example/textureOverlay.js deleted file mode 100644 index e30328d4..00000000 --- a/example/textureOverlay.js +++ /dev/null @@ -1,184 +0,0 @@ -import { - TilesRenderer, -} from '../src/index.js'; -import { - Scene, - DirectionalLight, - AmbientLight, - WebGLRenderer, - PerspectiveCamera, - Group, -} from 'three'; -import { FlyOrbitControls } from './src/controls/FlyOrbitControls.js'; -import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; -import { Box3 } from 'three'; -import { Sphere } from 'three'; -import { TextureOverlayTilesRendererMixin } from './src/TextureOverlayTilesRenderer.js'; -import { DataTexture } from 'three'; - -let camera, controls, scene, renderer; -let tiles; - -const params = { - - layer1: true, - layer2: true, - -}; - -init(); -render(); - -function init() { - - scene = new Scene(); - - // primary camera view - renderer = new WebGLRenderer( { antialias: true } ); - renderer.setPixelRatio( window.devicePixelRatio ); - renderer.setSize( window.innerWidth, window.innerHeight ); - renderer.setClearColor( 0xd8cec0 ); - - document.body.appendChild( renderer.domElement ); - renderer.domElement.tabIndex = 1; - - camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 4000 ); - camera.position.set( 20, 10, 20 ).multiplyScalar( 20 ); - - // controls - controls = new FlyOrbitControls( camera, renderer.domElement ); - controls.screenSpacePanning = false; - controls.minDistance = 1; - controls.maxDistance = 2000; - controls.maxPolarAngle = Math.PI / 2; - controls.baseSpeed = 0.1; - controls.fastSpeed = 0.2; - - // lights - const dirLight = new DirectionalLight( 0xffffff ); - dirLight.position.set( 1, 2, 3 ); - scene.add( dirLight ); - - const ambLight = new AmbientLight( 0xffffff, 0.2 ); - scene.add( ambLight ); - - const tilesParent = new Group(); - tilesParent.rotation.set( Math.PI / 2, 0, 0 ); - scene.add( tilesParent ); - - const cons = TextureOverlayTilesRendererMixin( TilesRenderer ); - - const url = '../data/tileset.json'; - tiles = new cons( url ); - tiles.fetchOptions.mode = 'cors'; - tiles.lruCache.minSize = 900; - tiles.lruCache.maxSize = 1300; - tiles.errorTarget = 12; - - window.tiles = tiles; - - tiles.registerLayer( 'layer1', async () => { - - const dt = new DataTexture( new Uint8Array( [ 255, 0, 0, 50 ] ) ); - dt.needsUpdate = true; - return dt; - - } ); - - tiles.registerLayer( 'layer2', async () => { - - const dt = new DataTexture( new Uint8Array( [ 0, 0, 255, 50 ] ) ); - dt.needsUpdate = true; - return dt; - - } ); - - tilesParent.add( tiles.group ); - - onWindowResize(); - window.addEventListener( 'resize', onWindowResize, false ); - - const gui = new GUI(); - gui.add( params, 'layer1' ).onChange( v => { - - if ( v ) { - - tiles.registerLayer( 'layer1', async () => { - - const dt = new DataTexture( new Uint8Array( [ 255, 0, 0, 50 ] ) ); - dt.needsUpdate = true; - return dt; - - } ); - - } else { - - tiles.unregisterLayer( 'layer1' ); - - } - - } ); - - gui.add( params, 'layer2' ).onChange( v => { - - if ( v ) { - - tiles.registerLayer( 'layer2', async () => { - - const dt = new DataTexture( new Uint8Array( [ 0, 0, 255, 50 ] ) ); - dt.needsUpdate = true; - return dt; - - } ); - - } else { - - tiles.unregisterLayer( 'layer2' ); - - } - - } ); - gui.open(); - -} - -function onWindowResize() { - - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize( window.innerWidth, window.innerHeight ); - renderer.setPixelRatio( window.devicePixelRatio ); - -} - -function render() { - - requestAnimationFrame( render ); - - camera.updateMatrixWorld(); - - tiles.setCamera( camera ); - tiles.setResolutionFromRenderer( camera, renderer ); - tiles.update(); - - tiles.group.rotation.x = - Math.PI / 2; - - const box = new Box3(); - const sphere = new Sphere(); - if ( tiles.getBoundingBox( box ) ) { - - box.getCenter( tiles.group.position ); - tiles.group.position.multiplyScalar( - 1 ); - - } else if ( tiles.getBoundingSphere( sphere ) ) { - - tiles.group.position.copy( sphere.center ); - tiles.group.position.multiplyScalar( - 1 ); - - } - - tiles.group.position.z -= - 150; - - renderer.render( scene, camera ); - -} diff --git a/src/base/TilesRendererBase.js b/src/base/TilesRendererBase.js index 2297cccf..2216d157 100644 --- a/src/base/TilesRendererBase.js +++ b/src/base/TilesRendererBase.js @@ -4,6 +4,8 @@ import { PriorityQueue } from '../utilities/PriorityQueue.js'; import { determineFrustumSet, toggleTiles, skipTraversal, markUsedSetLeaves, traverseSet } from './traverseFunctions.js'; import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js'; +const PLUGIN_REGISTERED = Symbol( 'PLUGIN_REGISTERED' ); + /** * Function for provided to sort all tiles for prioritizing loading/unloading. * @@ -82,6 +84,7 @@ export class TilesRendererBase { this.tileSets = {}; this.rootURL = url; this.fetchOptions = {}; + this.plugins = []; this.preprocessURL = null; @@ -120,6 +123,26 @@ export class TilesRendererBase { } + registerPlugin( plugin ) { + + if ( plugin[ PLUGIN_REGISTERED ] === true ) { + + throw new Error( 'TilesRendererBase: A plugin can only be registered to a single tile set' ); + + } + + this.plugins.push( plugin ); + plugin[ PLUGIN_REGISTERED ] = true; + plugin.init( this ); + + } + + getPluginByName( name ) { + + return this.plugins.find( p => p.name === name ); + + } + traverse( beforecb, aftercb ) { const tileSets = this.tileSets; @@ -170,6 +193,68 @@ export class TilesRendererBase { } + resetFailedTiles() { + + const stats = this.stats; + if ( stats.failed === 0 ) { + + return; + + } + + this.traverse( tile => { + + if ( tile.__loadingState === FAILED ) { + + tile.__loadingState = UNLOADED; + + } + + } ); + + stats.failed = 0; + + } + + dispose() { + + // dispose of all the plugins + this.invokeAllPlugins( plugin => { + + plugin.dispose && plugin.dispose(); + + } ); + + const lruCache = this.lruCache; + + // Make sure we've collected all children before disposing of the internal tilesets to avoid + // dangling children that we inadvertantly skip when deleting the nested tileset. + const toRemove = []; + this.traverse( t => { + + toRemove.push( t ); + return false; + + } ); + for ( let i = 0, l = toRemove.length; i < l; i ++ ) { + + lruCache.remove( toRemove[ i ] ); + + } + + this.stats = { + parsing: 0, + downloading: 0, + failed: 0, + inFrustum: 0, + used: 0, + active: 0, + visible: 0, + }; + this.frameCount = 0; + + } + // Overrideable parseTile( buffer, tile, extension ) { @@ -314,29 +399,6 @@ export class TilesRendererBase { } - resetFailedTiles() { - - const stats = this.stats; - if ( stats.failed === 0 ) { - - return; - - } - - this.traverse( tile => { - - if ( tile.__loadingState === FAILED ) { - - tile.__loadingState = UNLOADED; - - } - - } ); - - stats.failed = 0; - - } - // Private Functions fetchTileSet( url, fetchOptions, parent = null ) { @@ -645,35 +707,40 @@ export class TilesRendererBase { } - dispose() { + invokeOnePlugin( func ) { - const lruCache = this.lruCache; + const plugins = this.plugins; + for ( let i = 0; i < plugins.length; i ++ ) { - // Make sure we've collected all children before disposing of the internal tilesets to avoid - // dangling children that we inadvertantly skip when deleting the nested tileset. - const toRemove = []; - this.traverse( t => { + const result = func( plugins[ i ] ); + if ( result ) { - toRemove.push( t ); - return false; + return result; - } ); - for ( let i = 0, l = toRemove.length; i < l; i ++ ) { + } - lruCache.remove( toRemove[ i ] ); + } + + return null; + + } + + invokeAllPlugins( func ) { + + const plugins = this.plugins; + const pending = []; + for ( let i = 0; i < plugins.length; i ++ ) { + + const result = func( plugins[ i ] ); + if ( result ) { + + pending.push( result ); + + } } - this.stats = { - parsing: 0, - downloading: 0, - failed: 0, - inFrustum: 0, - used: 0, - active: 0, - visible: 0, - }; - this.frameCount = 0; + return pending.length === 0 ? null : Promise.all( pending ); } diff --git a/src/three/TilesRenderer.js b/src/three/TilesRenderer.js index a6fe77ce..d3495907 100644 --- a/src/three/TilesRenderer.js +++ b/src/three/TilesRenderer.js @@ -673,9 +673,12 @@ export class TilesRenderer extends TilesRendererBase { } - // wait for extra processing if needed - // TODO: this should be handled by a plugin - await this._pluginProcessTileModel( scene, tile ); + // wait for extra processing by plugins if needed + await this.invokeAllPlugins( plugin => { + + return plugin.processTileModel && plugin.processTileModel( scene, tile ); + + } ); // exit early if a new request has already started if ( tile._loadIndex !== loadIndex ) { @@ -990,9 +993,4 @@ export class TilesRenderer extends TilesRendererBase { } - /* private */ - // TODO: this should leverage plugin system in the future - async _pluginProcessTileModel( scene, tile ) {} - - }