diff --git a/example/fadingTiles.js b/example/fadingTiles.js index 8ef8a1579..bbb541b98 100644 --- a/example/fadingTiles.js +++ b/example/fadingTiles.js @@ -5,10 +5,10 @@ import { OrthographicCamera, Group, } from 'three'; -import { FadeTilesRenderer, } from './src/FadeTilesRenderer.js'; +import { FadeTilesRenderer, } from './src/plugins/fade/FadeTilesRenderer.js'; import { EnvironmentControls } from '../src/index.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; -import { CameraTransitionManager } from './src/CameraTransitionManager.js'; +import { CameraTransitionManager } from './src/camera/CameraTransitionManager.js'; let controls, scene, renderer; let groundTiles, skyTiles, tilesParent, transition; diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index 118708187..b34a15e9f 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -16,7 +16,7 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; import { estimateBytesUsed } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; import Stats from 'three/examples/jsm/libs/stats.module.js'; -import { CameraTransitionManager } from './src/CameraTransitionManager.js'; +import { CameraTransitionManager } from './src/camera/CameraTransitionManager.js'; let controls, scene, renderer, tiles, transition; let statsContainer, stats; diff --git a/example/landformSite.js b/example/landformSite.js index 43387281d..ccea90c23 100644 --- a/example/landformSite.js +++ b/example/landformSite.js @@ -4,14 +4,16 @@ import { } from '..'; import { Scene, - DirectionalLight, - AmbientLight, WebGLRenderer, PerspectiveCamera, Group, + TextureLoader, + MeshBasicMaterial, } from 'three'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; -import { JPLLandformSiteSceneLoader } from './src/JPLLandformSceneLoader.js'; +import { JPLLandformSiteSceneLoader } from './src/jpl/JPLLandformSceneLoader.js'; +import { TextureOverlayTilesRendererMixin } from './src/plugins/overlays/TextureOverlayTilesRenderer.js'; +import { TextureOverlayMaterialMixin } from './src/plugins/overlays/TextureOverlayMaterial.js'; const URLS = [ @@ -51,6 +53,7 @@ let camera, controls, scene, renderer; const params = { errorTarget: 12, + slopeLayer: false, }; @@ -89,17 +92,69 @@ function init() { let downloadQueue = null; let parseQueue = null; let lruCache = null; + const layerFunction = async tileUrl => { + + const url = tileUrl.replace( '/tilesets/', '/textures/SMG/' ).replace( /\.[0-9a-z]+$/i, '.png' ); + + return new TextureLoader() + .loadAsync( url ) + .then( tex => { + + tex.flipY = false; + return tex; + + } ); + + }; + URLS.forEach( async url => { const scene = await new JPLLandformSiteSceneLoader().load( url ); 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 TilesRenderer( url ); + const tiles = new TextureOverlayTilesRenderer( url ); + + // ensure all materials support overlay textures + tiles.addEventListener( 'load-model', ( { tile, scene } )=> { + + scene.traverse( c => { + + if ( c.material ) { + + const newMaterial = new TextureOverlayMaterial(); + newMaterial.copy( c.material ); + newMaterial.textures = Object.values( tiles.getTexturesForTile( tile ) ); + c.material = newMaterial; + + } + + } ); + + } ); + + // assign the texture layers + tiles.addEventListener( 'layer-textures-change', ( { tile, scene } ) => { + scene.traverse( c => { + + if ( c.material ) { + + c.material.textures = Object.values( tiles.getTexturesForTile( tile ) ); + c.material.needsUpdate = true; + + } + + } ); + + } ); + + // assign a common cache and data lruCache = lruCache || tiles.lruCache; parseQueue = parseQueue || tiles.parseQueue; downloadQueue = downloadQueue || tiles.downloadQueue; @@ -109,6 +164,7 @@ function init() { tiles.parseQueue = parseQueue; tiles.setCamera( camera ); + // update the scene const frame = scene.frames.find( f => f.id === info.frame_id ); frame.sceneMatrix.decompose( tiles.group.position, tiles.group.quaternion, tiles.group.scale ); tilesParent.add( tiles.group ); @@ -123,6 +179,19 @@ function init() { const gui = new GUI(); gui.add( params, 'errorTarget', 0, 100 ); + gui.add( params, 'slopeLayer' ).onChange( v => { + + if ( v ) { + + tileSets.forEach( t => t.registerLayer( 'slopeLayer', layerFunction ) ); + + } else { + + tileSets.forEach( t => t.unregisterLayer( 'slopeLayer' ) ); + + } + + } ); gui.open(); } diff --git a/example/src/CameraTransitionManager.js b/example/src/camera/CameraTransitionManager.js similarity index 100% rename from example/src/CameraTransitionManager.js rename to example/src/camera/CameraTransitionManager.js diff --git a/example/src/JPLLandformSceneLoader.js b/example/src/jpl/JPLLandformSceneLoader.js similarity index 100% rename from example/src/JPLLandformSceneLoader.js rename to example/src/jpl/JPLLandformSceneLoader.js diff --git a/example/src/FadeManager.js b/example/src/plugins/fade/FadeManager.js similarity index 100% rename from example/src/FadeManager.js rename to example/src/plugins/fade/FadeManager.js diff --git a/example/src/FadeTilesRenderer.js b/example/src/plugins/fade/FadeTilesRenderer.js similarity index 98% rename from example/src/FadeTilesRenderer.js rename to example/src/plugins/fade/FadeTilesRenderer.js index 500d299b3..85dcf6111 100644 --- a/example/src/FadeTilesRenderer.js +++ b/example/src/plugins/fade/FadeTilesRenderer.js @@ -1,5 +1,5 @@ import { Group, Matrix4, Vector3, Quaternion } from 'three'; -import { TilesRenderer } from '../../src/index.js'; +import { TilesRenderer } from '../../../../src/index.js'; import { FadeManager } from './FadeManager.js'; const _fromPos = new Vector3(); diff --git a/example/src/plugins/overlays/TextureOverlayMaterial.js b/example/src/plugins/overlays/TextureOverlayMaterial.js new file mode 100644 index 000000000..9e5fc826a --- /dev/null +++ b/example/src/plugins/overlays/TextureOverlayMaterial.js @@ -0,0 +1,59 @@ +export const TextureOverlayMaterialMixin = base => class extends base { + + constructor( ...args ) { + + super( ...args ); + this.textures = []; + + } + + onBeforeCompile( shader ) { + + const textures = this.textures; + const material = this; + + shader.uniforms.textures = { + get value() { + + return material.textures; + + }, + }; + + // WebGL does not seem to like empty texture arrays + if ( textures.length !== 0 ) { + + shader.fragmentShader = shader.fragmentShader + .replace( /void main/, m => /* glsl */` + uniform sampler2D textures[ ${ textures.length } ]; + ${ m } + + ` ) + .replace( /#include /, m => /* glsl */` + + ${ m } + + vec4 col; + #pragma unroll_loop_start + for ( int i = 0; i < ${ textures.length }; i ++ ) { + + col = texture( textures[ i ], vMapUv ); + col = vec4( 0, 0, 1, col.r ); + diffuseColor = mix( diffuseColor, vec4( 0.35, 0.65, 1, 1 ), col.a ); + + } + #pragma unroll_loop_end + + ` ); + + } + + } + + customProgramCacheKey() { + + return this.textures.length + this.onBeforeCompile.toString(); + + } + +}; diff --git a/example/src/plugins/overlays/TextureOverlayTilesRenderer.js b/example/src/plugins/overlays/TextureOverlayTilesRenderer.js new file mode 100644 index 000000000..a5f7d18f4 --- /dev/null +++ b/example/src/plugins/overlays/TextureOverlayTilesRenderer.js @@ -0,0 +1,338 @@ +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? + +function canUseImageBitmap() { + + let isSafari = false; + let isFirefox = false; + let firefoxVersion = - 1; + + if ( typeof navigator !== 'undefined' ) { + + isSafari = /^((?!chrome|android).)*safari/i.test( navigator.userAgent ) === true; + isFirefox = navigator.userAgent.indexOf( 'Firefox' ) > - 1; + firefoxVersion = isFirefox ? navigator.userAgent.match( /Firefox\/([0-9]+)\./ )[ 1 ] : - 1; + + } + + + return ! ( typeof createImageBitmap === 'undefined' || isSafari || ( isFirefox && firefoxVersion < 98 ) ); + +} + +class TextureCache { + + constructor( loadTextureCallback, queue ) { + + this.cache = {}; + this.fetchOptions = {}; + this.loadTextureCallback = loadTextureCallback; + this.queue = queue; + + } + + getTextureLoader() { + + const fetchOptions = this.fetchOptions; + + let loader; + if ( canUseImageBitmap() ) { + + loader = new ImageBitmapLoader(); + + } else { + + loader = new TextureLoader(); + + } + + if ( fetchOptions.credentials === 'include' && fetchOptions.mode === 'cors' ) { + + loader.setCrossOrigin( 'use-credentials' ); + + } + + if ( 'credentials' in fetchOptions ) { + + loader.setWithCredentials( fetchOptions.credentials === 'include' ); + + } + + if ( fetchOptions.headers ) { + + loader.setRequestHeader( fetchOptions.headers ); + + } + + return loader; + + } + + loadTexture( key ) { + + const cache = this.cache; + if ( key in cache ) { + + return cache[ key ].promise; + + } + + const abortController = new AbortController(); + const promise = this.queue + .add( key, () => { + + return this.loadTextureCallback( key ); + + } ) + .then( tex => { + + if ( ! abortController.signal.aborted ) { + + cache[ key ].texture = tex; + return tex; + + } else { + + throw new Error( 'TextureCache: Texture load aborted.' ); + + } + + } ); + + this.cache[ key ] = { + texture: null, + abortController, + promise, + }; + + return promise; + + } + + getTexture( key ) { + + const cache = this.cache; + if ( key in cache ) { + + return cache[ key ].texture; + + } else { + + return null; + + } + + } + + deleteTexture( key ) { + + const cache = this.cache; + if ( key in cache ) { + + const info = cache[ key ]; + info.refs --; + + if ( info.refs === 0 ) { + + if ( info.texture ) { + + info.texture.dispose(); + + } else if ( info.abortController ) { + + info.abortController.abort(); + this.queue.remove( key ); + + } + + delete this.cache[ key ]; + + } + + } + + } + + dispose() { + + const cache = this.cache; + for ( const key in cache ) { + + const info = cache[ key ]; + if ( info.texture ) { + + info.texture.dispose(); + if ( info.texture.image instanceof ImageBitmap ) { + + info.texture.image.close(); + + } + + } else if ( info.abortController ) { + + info.abortController.abort(); + + } + + } + + } + +} + +export const TextureOverlayTilesRendererMixin = base => class extends base { + + constructor( ...args ) { + + super( ...args ); + this.caches = {}; + this.queue = new PriorityQueue(); + this.queue.priorityCallback = ( a, b ) => { + + return this.downloadQueue.priorityCallback( a, b ); + + }; + + this.addEventListener( 'dispose-model', ( { tile } ) => { + + const caches = this.caches; + for ( const key in caches ) { + + const cache = caches[ key ]; + cache.deleteTexture( this.getTileKey( tile ) ); + + } + + } ); + + } + + _pluginProcessTileModel( scene, tile ) { + + const caches = this.caches; + const promises = []; + for ( const key in caches ) { + + const cache = caches[ key ]; + const pr = cache + .loadTexture( this.getTileKey( tile ) ) + .catch( () => {} ); + + promises.push( pr ); + + } + + return Promise.all( promises ); + + } + + getTileKey( tile ) { + + // TODO + return tile.content.uri; + + } + + getTexturesForTile( tile, target = {} ) { + + const tileKey = this.getTileKey( tile ); + const caches = this.caches; + for ( const key in target ) { + + if ( ! ( key in caches ) ) delete target[ key ]; + + } + + for ( const key in caches ) { + + target[ key ] = caches[ key ].getTexture( tileKey ); + + } + + return target; + + } + + registerLayer( name, customTextureCallback ) { + + if ( name in this.caches ) { + + throw new Error(); + + } + + const cache = new TextureCache( customTextureCallback, this.queue ); + cache.fetchOptions = this.fetchOptions; + this.caches[ name ] = cache; + + this.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, + } ); + + } ) + .catch( () => {} ); + + } ); + + } + + unregisterLayer( name ) { + + const caches = this.caches; + if ( name in caches ) { + + const cache = caches[ name ]; + delete caches[ name ]; + + this.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, + } ); + + } + + } ); + + cache.dispose(); + + } + + } + +}; + diff --git a/example/textureOverlay.html b/example/textureOverlay.html new file mode 100644 index 000000000..7f865526e --- /dev/null +++ b/example/textureOverlay.html @@ -0,0 +1,19 @@ + + + + + + + Texture Overlays + + + + + + + diff --git a/example/textureOverlay.js b/example/textureOverlay.js new file mode 100644 index 000000000..e30328d46 --- /dev/null +++ b/example/textureOverlay.js @@ -0,0 +1,184 @@ +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/three/TilesRenderer.js b/src/three/TilesRenderer.js index 86aa904c4..a6fe77ce5 100644 --- a/src/three/TilesRenderer.js +++ b/src/three/TilesRenderer.js @@ -541,7 +541,7 @@ export class TilesRenderer extends TilesRendererBase { } - parseTile( buffer, tile, extension ) { + async parseTile( buffer, tile, extension ) { tile._loadIndex = tile._loadIndex || 0; tile._loadIndex ++; @@ -648,130 +648,133 @@ export class TilesRenderer extends TilesRendererBase { // check if this is the beginning of a new set of tiles to load and dispatch and event const stats = this.stats; - const currentlyLoading = stats.parsing + stats.downloading; - if ( this._loadingTiles === false && currentlyLoading > 0 ) { + if ( this._loadingTiles === false && stats.parsing + stats.downloading > 0 ) { this.dispatchEvent( { type: 'tiles-load-start' } ); this._loadingTiles = true; } - return promise.then( result => { + // wait for the tile to load + const result = await promise; - let scene; - let metadata; - if ( result.isObject3D ) { + // get the scene data + let scene; + let metadata; + if ( result.isObject3D ) { - scene = result; - metadata = null; + scene = result; + metadata = null; - } else { + } else { - scene = result.scene; - metadata = result; + scene = result.scene; + metadata = result; - } + } - if ( tile._loadIndex !== loadIndex ) { + // wait for extra processing if needed + // TODO: this should be handled by a plugin + await this._pluginProcessTileModel( scene, tile ); - return; + // exit early if a new request has already started + if ( tile._loadIndex !== loadIndex ) { - } + return; - // ensure the matrix is up to date in case the scene has a transform applied - scene.updateMatrix(); + } - // apply the local up-axis correction rotation - // GLTFLoader seems to never set a transformation on the root scene object so - // any transformations applied to it can be assumed to be applied after load - // (such as applying RTC_CENTER) meaning they should happen _after_ the z-up - // rotation fix which is why "multiply" happens here. - if ( fileType === 'glb' || fileType === 'gltf' ) { + // ensure the matrix is up to date in case the scene has a transform applied + scene.updateMatrix(); - scene.matrix.multiply( upAdjustment ); + // apply the local up-axis correction rotation + // GLTFLoader seems to never set a transformation on the root scene object so + // any transformations applied to it can be assumed to be applied after load + // (such as applying RTC_CENTER) meaning they should happen _after_ the z-up + // rotation fix which is why "multiply" happens here. + if ( fileType === 'glb' || fileType === 'gltf' ) { - } + scene.matrix.multiply( upAdjustment ); - scene.matrix.premultiply( cachedTransform ); - scene.matrix.decompose( scene.position, scene.quaternion, scene.scale ); - scene.traverse( c => { + } - c[ INITIAL_FRUSTUM_CULLED ] = c.frustumCulled; + scene.matrix.premultiply( cachedTransform ); + scene.matrix.decompose( scene.position, scene.quaternion, scene.scale ); + scene.traverse( c => { - } ); - updateFrustumCulled( scene, ! this.autoDisableRendererCulling ); + c[ INITIAL_FRUSTUM_CULLED ] = c.frustumCulled; - if ( REVISION_165 ) { + } ); + updateFrustumCulled( scene, ! this.autoDisableRendererCulling ); - // We handle raycasting in a custom way so remove it from here - scene.traverse( c => { + if ( REVISION_165 ) { - c.raycast = this._overridenRaycast; + // We handle raycasting in a custom way so remove it from here + scene.traverse( c => { - } ); + c.raycast = this._overridenRaycast; - } + } ); - const materials = []; - const geometry = []; - const textures = []; - scene.traverse( c => { + } - if ( c.geometry ) { + const materials = []; + const geometry = []; + const textures = []; + scene.traverse( c => { - geometry.push( c.geometry ); + if ( c.geometry ) { - } + geometry.push( c.geometry ); - if ( c.material ) { + } - const material = c.material; - materials.push( c.material ); + if ( c.material ) { - for ( const key in material ) { + const material = c.material; + materials.push( c.material ); - const value = material[ key ]; - if ( value && value.isTexture ) { + for ( const key in material ) { - textures.push( value ); + const value = material[ key ]; + if ( value && value.isTexture ) { - } + textures.push( value ); } } - } ); + } - cached.materials = materials; - cached.geometry = geometry; - cached.textures = textures; - cached.scene = scene; - cached.metadata = metadata; + } ); - // dispatch an event indicating that this model has completed - this.dispatchEvent( { - type: 'load-model', - scene, - tile, - } ); + cached.materials = materials; + cached.geometry = geometry; + cached.textures = textures; + cached.scene = scene; + cached.metadata = metadata; - if ( this.onLoadModel ) { + // dispatch an event indicating that this model has completed + this.dispatchEvent( { + type: 'load-model', + scene, + tile, + } ); - this.onLoadModel( scene, tile ); + if ( this.onLoadModel ) { - } + this.onLoadModel( scene, tile ); - // dispatch an "end" event if all tiles have finished loading - const currentlyLoading = stats.parsing + stats.downloading; - if ( this._loadingTiles === true && currentlyLoading === 1 ) { + } - this.dispatchEvent( { type: 'tiles-load-end' } ); - this._loadingTiles = false; + // dispatch an "end" event if all tiles have finished loading + if ( this._loadingTiles === true && stats.parsing + stats.downloading === 1 ) { - } + this.dispatchEvent( { type: 'tiles-load-end' } ); + this._loadingTiles = false; - } ); + } } @@ -987,4 +990,9 @@ export class TilesRenderer extends TilesRendererBase { } + /* private */ + // TODO: this should leverage plugin system in the future + async _pluginProcessTileModel( scene, tile ) {} + + } diff --git a/src/three/controls/EnvironmentControls.js b/src/three/controls/EnvironmentControls.js index c00ca4c44..c06860514 100644 --- a/src/three/controls/EnvironmentControls.js +++ b/src/three/controls/EnvironmentControls.js @@ -63,13 +63,14 @@ export class EnvironmentControls extends EventDispatcher { } - constructor( scene = null, camera = null, domElement = null ) { + constructor( scene = null, camera = null, domElement = null, tilesRenderer = null ) { super(); this.domElement = null; this.camera = null; this.scene = null; + this.tilesRenderer = null; // settings this._enabled = true; @@ -116,10 +117,14 @@ export class EnvironmentControls extends EventDispatcher { this._detachCallback = null; this._upInitialized = false; + // always update the zoom target point in case the tiles are changing + this._tilesOnChangeCallback = () => this.zoomPointSet = false; + // init if ( domElement ) this.attach( domElement ); if ( camera ) this.setCamera( camera ); if ( scene ) this.setScene( scene ); + if ( tilesRenderer ) this.setTilesRenderer( tilesRenderer ); } @@ -135,6 +140,29 @@ export class EnvironmentControls extends EventDispatcher { } + setTilesRenderer( tilesRenderer ) { + + if ( this.tilesRenderer ) { + + this.tilesRenderer.removeEventListener( 'tile-visibility-change', this._tilesOnChangeCallback ); + + } + + this.tilesRenderer = tilesRenderer; + if ( this.tilesRenderer !== null ) { + + this.tilesRenderer.addEventListener( 'tile-visibility-change', this._tilesOnChangeCallback ); + + if ( this.scene === null ) { + + this.setScene( this.tilesRenderer.group ); + + } + + } + + } + attach( domElement ) { if ( this.domElement ) { @@ -657,8 +685,7 @@ export class EnvironmentControls extends EventDispatcher { // track the zoom direction we're going to use const finalZoomDirection = _vec.copy( zoomDirection ); - // always update the zoom target point in case the tiles are changing - if ( this._updateZoomPoint() ) { + if ( this.zoomPointSet || this._updateZoomPoint() ) { const dist = zoomPoint.distanceTo( camera.position ); diff --git a/src/three/controls/GlobeControls.js b/src/three/controls/GlobeControls.js index ee0e738e1..b8e9b6846 100644 --- a/src/three/controls/GlobeControls.js +++ b/src/three/controls/GlobeControls.js @@ -34,13 +34,13 @@ export class GlobeControls extends EnvironmentControls { get ellipsoid() { - return this._tilesRenderer ? this._tilesRenderer.ellipsoid : null; + return this.tilesRenderer ? this.tilesRenderer.ellipsoid : null; } get tilesGroup() { - return this._tilesRenderer ? this._tilesRenderer.group : null; + return this.tilesRenderer ? this.tilesRenderer.group : null; } @@ -48,7 +48,6 @@ export class GlobeControls extends EnvironmentControls { // store which mode the drag stats are in super( scene, camera, domElement ); - this._tilesRenderer = null; this._dragMode = 0; this._rotationMode = 0; this.useFallbackPlane = false; @@ -57,22 +56,11 @@ export class GlobeControls extends EnvironmentControls { } - setTilesRenderer( tilesRenderer ) { - - this._tilesRenderer = tilesRenderer; - if ( this.scene === null && this._tilesRenderer !== null ) { - - this.setScene( this._tilesRenderer.group ); - - } - - } - setScene( scene ) { - if ( scene === null && this._tilesRenderer !== null ) { + if ( scene === null && this.tilesRenderer !== null ) { - super.setScene( this._tilesRenderer.group ); + super.setScene( this.tilesRenderer.group ); } else {