UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

525 lines (323 loc) 11.1 kB
import { EventDispatcher } from 'three'; const UNLOADED = 0; const LOADING = 1; const LOADED = 2; const FAILED = 3; const TIMER_DURATION = 150; function getChildKey( x, y ) { return ( x * 1 ) | ( y * 2 ); } function getKey( x, y, level ) { return `${ x }_${ y }_${ level }`; } class MVTTile { constructor() { this.parent = null; this.x = 0; this.y = 0; this.level = 0; this.children = new Array( 4 ).fill( null ); this.childCount = 0; this.loadingState = UNLOADED; this.visible = false; this.target = 0; this.showTimer = 0; this.hideTimer = 0; // set when a sibling in this tile's quad is targeted and the hierarchy is displaying // complete sibling sets, forcing this tile to load and display along with it this.siblingForced = false; // the "force" keep-alive state recorded by the last visibility pass, consumed by the next // state pass to keep timers and loads alive while an ancestor target loads this.forced = false; this._key = null; this._index = null; } getKey() { if ( this._key === null ) { this._key = `${ this.x }_${ this.y }_${ this.level }`; } return this._key; } getIndex() { if ( this._index === null ) { this._index = getChildKey( this.x % 2, this.y % 2 ); } return this._index; } addChild( child ) { const index = child.getIndex(); if ( this.children[ index ] || ( child.x >> 1 ) !== this.x || ( child.y >> 1 ) !== this.y || ( child.level - 1 ) !== this.level ) { throw new Error(); } child.parent = this; this.children[ index ] = child; this.childCount ++; } remove() { if ( this.childCount > 0 ) { throw new Error(); } this.parent.childCount --; this.parent.children[ this.getIndex() ] = null; this.parent = null; } } const _toPrune = new Set(); export class MVTHierarchy extends EventDispatcher { constructor() { super(); this.root = new MVTTile(); this.cache = { [ this.root.getKey() ]: this.root, }; this.contentCache = null; this._lastTime = - 1; // When true a targeted tile pulls in its full sibling set, and the set only displays once // every sibling has loaded - replacing the parent tile entirely so it never displays for // coverage. this.loadSiblings = true; } update() { const now = performance.now(); const lastTime = this._lastTime === - 1 ? now : this._lastTime; const dt = now - lastTime; this._lastTime = now; const { root } = this; const scope = this; // first pass: advance timers, maintain sibling sets, and kick off loads so the visibility // pass sees this frame's final loading state - including synchronous cache-hit loads updateState( root ); // second pass: resolve visibility now that every load for the frame has been started updateVisibility( root ); _toPrune.forEach( tile => this._deleteTile( tile ) ); _toPrune.clear(); function updateState( tile ) { // whether this tile's content is wanted - targeted directly or pulled in as part of a // displayed sibling set. The keep-alive "forced" state comes from the last visibility // pass, keeping timers and loads alive for visible tiles covering a loading ancestor. const targeted = tile.target > 0 || tile.siblingForced; const forcedTarget = tile.visible && tile.forced; // increment / decrement timers for determining whether to hide / show content if ( targeted || forcedTarget ) { tile.showTimer += dt; tile.showTimer = Math.min( tile.showTimer, TIMER_DURATION ); if ( tile.showTimer === TIMER_DURATION ) { tile.hideTimer = 0; } } else if ( tile.visible || tile.showTimer > 0 ) { tile.hideTimer += dt; tile.hideTimer = Math.min( tile.hideTimer, TIMER_DURATION ); if ( tile.hideTimer === TIMER_DURATION ) { tile.showTimer = 0; tile.hideTimer = 0; // Release the content lock; reset synchronously so the tile is in a clean // state before any async callbacks can observe it if ( tile.loadingState !== UNLOADED ) { scope.contentCache.release( tile.x, tile.y, tile.level ); tile.loadingState = UNLOADED; } } } // Active after show delay; stays active through the hide delay so visible tiles don't flash const isTargetTile = ( targeted ? tile.showTimer === TIMER_DURATION : tile.showTimer > 0 ) || forcedTarget; // Kick off load once the show timer commits to this tile if ( isTargetTile && tile.loadingState === UNLOADED ) { tile.loadingState = LOADING; const { x, y, level } = tile; const result = scope.contentCache.lock( x, y, level ); if ( result instanceof Promise ) { result .then( res => { if ( tile.loadingState === LOADING ) { tile.loadingState = LOADED; } } ) .catch( err => { if ( tile.loadingState === LOADING ) { tile.loadingState = err.name === 'AbortError' ? UNLOADED : FAILED; } } ); } else { tile.loadingState = result !== null ? LOADED : FAILED; } } // when loadSiblings is true, a targeted child ensures all four siblings exist and forces // them to load so this tile never has to display for partial coverage const { children } = tile; if ( scope.loadSiblings ) { let anyChildTargeted = false; for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child !== null && child.target > 0 ) { anyChildTargeted = true; } } if ( anyChildTargeted && tile.childCount < 4 ) { for ( let cy = 0; cy <= 1; cy ++ ) { for ( let cx = 0; cx <= 1; cx ++ ) { scope._ensureTile( 2 * tile.x + cx, 2 * tile.y + cy, tile.level + 1 ); } } } for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child !== null ) { child.siblingForced = anyChildTargeted; } } } else { // clear any stale forcing when the sibling traversal is disabled for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child !== null ) { child.siblingForced = false; } } } for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child !== null ) { updateState( child ); } } } function updateVisibility( tile, force = false, siblingsReady = true ) { const targeted = tile.target > 0 || tile.siblingForced; const forcedTarget = tile.visible && force; // record the keep-alive state for the next frame's state pass tile.forced = force; // Active after show delay; stays active through the hide delay so visible tiles don't flash const isTargetTile = ( targeted ? tile.showTimer === TIMER_DURATION : tile.showTimer > 0 ) || forcedTarget; let setVisible = false; if ( targeted || forcedTarget ) { // the tile can only display once loaded and, when displaying sibling sets, once the // whole set is ready so the quad swaps in together. Tiles forced to stay visible // bypass the sibling gate so already-displayed content isn't hidden mid-transition. if ( tile.loadingState === LOADED && ( siblingsReady || forcedTarget ) ) { setVisible = true; force = false; } else if ( isTargetTile ) { force = true; } } // the sibling set below is ready to display once every sibling has finished loading - // failed tiles count as ready and display as missing content const { children } = tile; let childSiblingsReady = true; if ( scope.loadSiblings ) { for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child === null || ( child.loadingState !== LOADED && child.loadingState !== FAILED ) ) { childSiblingsReady = false; } } } // showTimer > 0 keeps tiles with in-flight loads from being pruned mid-hysteresis let tileRequired = tile.visible || targeted || tile.showTimer > 0; let childrenNeedCoverage = false; for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( child !== null ) { tileRequired = updateVisibility( child, force, childSiblingsReady ) || tileRequired; // failed tiles can never display, so they don't require coverage when sibling // sets are displayed - the quad shows with missing content instead const childTargeted = child.target > 0 || child.siblingForced; const childBlocked = scope.loadSiblings && child.loadingState === FAILED; childrenNeedCoverage = childrenNeedCoverage || ( childTargeted && ! child.visible && ! childBlocked ); } } if ( childrenNeedCoverage && tile.loadingState === LOADED ) { setVisible = true; } // when the full sibling set below this tile is displayed the children replace this // tile's content entirely, so it doesn't display even when targeted. Failed children // count as displayed as long as at least one sibling is actually visible. if ( scope.loadSiblings && setVisible && tile.childCount === 4 ) { let allDisplayed = true; let anyVisible = false; for ( let i = 0, l = children.length; i < l; i ++ ) { const child = children[ i ]; if ( ! child.visible && child.loadingState !== FAILED ) { allDisplayed = false; } anyVisible = anyVisible || child.visible; } if ( allDisplayed && anyVisible ) { setVisible = false; } } if ( setVisible !== tile.visible ) { tile.visible = setVisible; scope.dispatchEvent( { type: 'toggle', visible: setVisible, x: tile.x, y: tile.y, level: tile.level, } ); } if ( tile !== scope.root && ! tileRequired ) { _toPrune.add( tile ); } return tileRequired; } } getVisibleTiles() { let arr = []; for ( const key in this.cache ) { const child = this.cache[ key ]; if ( child.visible ) { arr.push( child ); } } return arr; } setTargetState( x, y, l, target ) { if ( target ) { const tile = this._ensureTile( x, y, l ); tile.target ++; } else { const tile = this.cache[ getKey( x, y, l ) ]; if ( ! tile || tile.target <= 0 ) { throw new Error( 'MVTHierarchy: target ref count went negative — mismatched calls.' ); } tile.target --; } } _deleteTile( tile ) { if ( tile === this.root ) { throw new Error(); } const { cache } = this; const { x, y, level } = tile; const key = getKey( x, y, level ); if ( ! ( key in cache ) ) { throw new Error(); } cache[ key ].remove(); delete cache[ key ]; } _ensureTile( x, y, level ) { const { cache } = this; const key = getKey( x, y, level ); if ( key in cache ) { return cache[ key ]; } const child = new MVTTile(); child.x = x; child.y = y; child.level = level; const parentX = x >> 1; const parentY = y >> 1; const parentLevel = level - 1; const parent = this._ensureTile( parentX, parentY, parentLevel ); parent.addChild( child ); cache[ child.getKey() ] = child; return child; } }