UNPKG

3d-tiles-renderer

Version:

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

631 lines (386 loc) 16.7 kB
import { LOADED, FAILED } from '../constants.js'; const viewErrorTarget = { inView: false, error: Infinity, distanceFromCamera: Infinity, }; function isDownloadFinished( value ) { return value === LOADED || value === FAILED; } // Checks whether this tile was last used on the given frame. function isUsedThisFrame( tile, frameCount ) { return isProcessed( tile ) && tile.traversal.lastFrameVisited === frameCount && tile.traversal.used; } function isProcessed( tile ) { // TODO: here "traversal" is used to determine whether a tile has been initialized. We should // move these to tiles utils return Boolean( tile.traversal ); } // Checks whether all children have been processed and are ready to traverse function areChildrenProcessed( tile ) { const { children } = tile; const childrenReady = children.length === 0 || isProcessed( children[ children.length - 1 ] ); const contentReady = ! tile.internal.hasUnrenderableContent || isDownloadFinished( tile.internal.loadingState ); return childrenReady && contentReady; } // Checks whether we can stop at this tile for rendering or not function canUnconditionallyRefine( tile ) { return tile.traversal.unconditionallyRefine; } // Resets the frame information for the given tile function resetFrameState( tile, renderer ) { if ( ! isProcessed( tile ) ) { return; } renderer.ensureChildrenArePreprocessed( tile ); if ( tile.traversal.lastFrameVisited !== renderer.frameCount ) { tile.traversal.wasInFrustum = tile.traversal.inFrustum; tile.traversal.wasSetActive = tile.traversal.active; tile.traversal.wasSetVisible = tile.traversal.visible; tile.traversal.usedLastFrame = tile.traversal.used; tile.traversal.lastFrameVisited = renderer.frameCount; tile.traversal.used = false; tile.traversal.inFrustum = false; tile.traversal.isLeaf = false; tile.traversal.visible = false; tile.traversal.active = false; tile.traversal.error = Infinity; tile.traversal.distanceFromCamera = Infinity; tile.traversal.allChildrenReady = false; tile.traversal.allChildrenLoaded = false; tile.traversal.kicked = false; tile.traversal.allUsedChildrenProcessed = false; // update tile frustum and error state renderer.calculateTileViewErrorWithPlugin( tile, viewErrorTarget ); tile.traversal.inFrustum = viewErrorTarget.inView; tile.traversal.error = viewErrorTarget.error; tile.traversal.distanceFromCamera = viewErrorTarget.distanceFromCamera; // update whether this tile can be unconditionally refined tile.traversal.unconditionallyRefine = tile.internal.hasUnrenderableContent; if ( ! tile.traversal.unconditionallyRefine ) { // See NASA-AMMOS/3DTilesRendererJS#1304#issuecomment-3301374873 // A tile can unconditionally refine if it is an internal tileset or the geometric error is // higher than that of the nearest non-unconditionally refine-able ancestor. let nearestConditionalParent = tile.parent; while ( nearestConditionalParent && nearestConditionalParent.traversal.unconditionallyRefine ) { nearestConditionalParent = nearestConditionalParent.parent; } if ( nearestConditionalParent && nearestConditionalParent.geometricError <= tile.geometricError ) { tile.traversal.unconditionallyRefine = true; } } } } // Recursively mark tiles used down to the next layer, skipping external tilesets function recursivelyMarkUsed( tile, renderer, cacheOnly = false ) { resetFrameState( tile, renderer ); if ( cacheOnly ) { renderer.markTileUsed( tile ); } else { markUsed( tile ); } // 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 ) ) { const children = tile.children; for ( let i = 0, l = children.length; i < l; i ++ ) { recursivelyMarkUsed( children[ i ], renderer, cacheOnly ); } } } // Recursively mark tiles used down to the next layer, skipping external tilesets function recursivelyMarkPreviouslyUsed( tile, renderer ) { resetFrameState( tile, renderer ); if ( tile.traversal.usedLastFrame ) { markUsed( tile ); if ( tile.traversal.wasSetActive ) { tile.traversal.active = true; } if ( ! tile.traversal.active || canUnconditionallyRefine( tile ) ) { // 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 ( areChildrenProcessed( tile ) ) { const children = tile.children; for ( let i = 0, l = children.length; i < l; i ++ ) { recursivelyMarkPreviouslyUsed( children[ i ], renderer ); } } } } } // Mark a tile as being used by current view function markUsed( tile ) { tile.traversal.used = true; } // Returns whether the tile can be traversed to the next layer of children by checking the tile metrics function canTraverse( tile, renderer ) { // If we've met the error requirements then don't load further - if an external tileset is encountered, // though, then continue to refine. if ( tile.traversal.error <= renderer.errorTarget && ! canUnconditionallyRefine( tile ) ) { return false; } // Early out if we've reached the maximum allowed depth. if ( renderer.maxDepth > 0 && tile.internal.depth + 1 >= renderer.maxDepth ) { return false; } // Early out if the children haven't been processed, yet if ( ! areChildrenProcessed( tile ) ) { return false; } return true; } // Marks "active" children as "kicked" so they are still loaded but not rendered yet function kickActiveChildren( tile, renderer ) { const { frameCount } = renderer; const { children } = tile; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; if ( isUsedThisFrame( c, frameCount ) ) { if ( c.traversal.active ) { c.traversal.kicked = true; c.traversal.active = false; } kickActiveChildren( c, renderer ); } } } // Checks whether this tile is ready to be stopped at for rendering function isChildReady( tile ) { return ! canUnconditionallyRefine( tile ) && ( ! tile.internal.hasContent || isDownloadFinished( tile.internal.loadingState ) ); } // 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. resetFrameState( tile, renderer ); if ( ! tile.traversal.inFrustum ) { return; } if ( ! canTraverse( tile, renderer ) ) { markUsed( tile ); return; } // Traverse children and see if any children are in view. let anyChildrenUsed = false; let anyChildrenInFrustum = false; const children = tile.children; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; markUsedTiles( c, renderer ); anyChildrenUsed = anyChildrenUsed || isUsedThisFrame( c, renderer.frameCount ); anyChildrenInFrustum = anyChildrenInFrustum || c.traversal.inFrustum; } // If none of the children are visible in the frustum then there should be no reason to display this tile. We still mark // this tile and all children as "used" only in the cache (but not loaded) so they are not disposed, causing an oscillation // / flicker in the content. if ( tile.refine === 'REPLACE' && ! anyChildrenInFrustum && children.length !== 0 ) { tile.traversal.inFrustum = false; renderer.markTileUsed( tile ); for ( let i = 0, l = children.length; i < l; i ++ ) { recursivelyMarkUsed( children[ i ], renderer, true ); } return; } // wait until after the above condition to mark the traversed tile as used or not // and then mark any of the sibling child tiles as used markUsed( tile ); if ( tile.refine === 'REPLACE' && anyChildrenUsed && ( renderer.loadSiblings || renderer.loadAncestors ) ) { for ( let i = 0, l = children.length; i < l; i ++ ) { recursivelyMarkUsed( children[ i ], renderer ); } } } // Traverse and mark the tiles that are at the leaf nodes of the "used" tree. function markUsedSetLeaves( tile, renderer ) { const frameCount = renderer.frameCount; if ( ! isUsedThisFrame( tile, frameCount ) ) { return; } // This tile is a leaf if none of the children had been used. const children = tile.children; let anyChildrenUsed = false; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; anyChildrenUsed = anyChildrenUsed || isUsedThisFrame( c, frameCount ); } // Traversal if ( ! anyChildrenUsed ) { tile.traversal.isLeaf = true; } else { for ( let i = 0, l = children.length; i < l; i ++ ) { markUsedSetLeaves( children[ i ], renderer ); } // compute content-readiness so markVisibleTiles can stop traversal at this tile when // loadAncestors is enabled and children haven't finished loading their content yet. // only computed when anyChildrenUsed — leaf tiles retain allChildrenLoaded = false // (from resetFrameState) so parents correctly see them as not yet loaded. let allChildrenLoaded = true; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; if ( isUsedThisFrame( c, frameCount ) ) { const childCanDisplay = ! canUnconditionallyRefine( c ); const childContentReady = ! c.internal.hasContent || isDownloadFinished( c.internal.loadingState ); const childIsReady = ( childCanDisplay && childContentReady ) || c.traversal.allChildrenLoaded; if ( ! childIsReady ) { allChildrenLoaded = false; } } } tile.traversal.allChildrenLoaded = allChildrenLoaded; } // save whether any children are processed let allUsedChildrenProcessed = true; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; if ( isUsedThisFrame( c, renderer.frameCount ) && ! c.traversal.allUsedChildrenProcessed ) { allUsedChildrenProcessed = false; } } tile.traversal.allUsedChildrenProcessed = allUsedChildrenProcessed && areChildrenProcessed( tile ); } // TODO: revisit implementation // Skip past tiles we consider unrenderable because they are outside the error threshold. function markVisibleTiles( tile, renderer ) { if ( ! isUsedThisFrame( tile, renderer.frameCount ) ) { return; } const children = tile.children; // When loading parent tiles as fallbacks: if children aren't content-ready yet, mark this tile // as a leaf so it is displayed as a placeholder while children load (mirrors legacy behavior). // allChildrenLoaded was computed bottom-up in markUsedSetLeaves so it can be checked before recursing. if ( renderer.loadAncestors && ! tile.traversal.allChildrenLoaded && ! canUnconditionallyRefine( tile ) ) { tile.traversal.isLeaf = true; } if ( tile.traversal.isLeaf ) { // if we're allowed to stop at this tile then mark it as active and allow any previously active tiles to // continue to be displayed in case this tiles content hasn't downloaded. if ( ! canUnconditionallyRefine( tile ) ) { tile.traversal.active = true; if ( areChildrenProcessed( tile ) && tile.internal.hasContent && ! isDownloadFinished( tile.internal.loadingState ) ) { for ( let i = 0, l = children.length; i < l; i ++ ) { recursivelyMarkPreviouslyUsed( children[ i ], renderer ); } } } return; } // Don't wait for all children tiles to load if this tileset has empty tiles at the root in order // to match Cesium's behavior let allChildrenReady = children.length > 0; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; markVisibleTiles( c, renderer ); if ( isUsedThisFrame( c, renderer.frameCount ) ) { const childIsReady = c.traversal.active && isChildReady( c ); if ( ! childIsReady && ! c.traversal.allChildrenReady ) { allChildrenReady = false; } } } tile.traversal.allChildrenReady = allChildrenReady; // If we find that the subsequent children are not ready such that this tile gap can be filled then // mark all lower tiles as non active and prepare this one to be displayed if possible if ( ! allChildrenReady && tile.traversal.wasSetActive && isChildReady( tile ) ) { tile.traversal.active = true; kickActiveChildren( tile, renderer ); } } // Final traverse to toggle tile visibility. function toggleTiles( tile, renderer ) { resetFrameState( tile, renderer ); const isUsed = isUsedThisFrame( tile, renderer.frameCount ); if ( isUsed ) { // any internal tileset loaded and marked as being used so we don't unload them if ( tile.internal.hasUnrenderableContent ) { renderer.markTileUsed( tile ); renderer.queueTileForDownload( tile ); } // ADD tiles are part of the display frontier alongside their children if ( tile.internal.hasRenderableContent && tile.refine === 'ADD' ) { tile.traversal.active = true; } // queue any tiles to load that we need to, and unmark any unloaded or non visible tiles as "active" if ( ( tile.traversal.active || tile.traversal.kicked ) && tile.internal.hasContent ) { renderer.markTileUsed( tile ); if ( tile.traversal.allUsedChildrenProcessed ) { renderer.queueTileForDownload( tile ); } if ( tile.internal.loadingState !== LOADED ) { tile.traversal.active = false; } } // when loading parent tiles as fallbacks, keep all used tiles downloaded // regardless of active state so they are available to display while children load if ( renderer.loadAncestors && tile.internal.hasContent ) { renderer.markTileUsed( tile ); renderer.queueTileForDownload( tile ); } // keep tiles with virtual children retained in the LRU cache so the content is // available to regenerate virtual children if the overlay configuration changes. if ( tile.internal.virtualChildCount > 0 && tile.internal.hasContent ) { renderer.markTileUsed( tile ); } // if the tile is loaded and in frustum we can mark it as visible tile.traversal.visible = tile.internal.hasRenderableContent && tile.traversal.active && tile.traversal.inFrustum && tile.internal.loadingState === LOADED; renderer.stats.used ++; if ( tile.traversal.inFrustum ) { renderer.stats.inFrustum ++; } } if ( isUsed || isProcessed( tile ) && tile.traversal.usedLastFrame ) { let setActive = false; let setVisible = false; if ( isUsed ) { // enable visibility if active due to shadows setActive = tile.traversal.active; if ( renderer.displayActiveTiles ) { setVisible = tile.traversal.active || tile.traversal.visible; } else { setVisible = tile.traversal.visible; } } else { // if the tile was used last frame but not this one then there's potential for the tile // to not have been visited during the traversal, meaning it hasn't been reset and has // stale values. This ensures the values are not stale. resetFrameState( tile, renderer ); } // If the active or visible state changed then call the functions. if ( tile.internal.hasRenderableContent && tile.internal.loadingState === LOADED ) { if ( setActive ) { renderer.stats.active ++; } if ( setVisible ) { renderer.stats.visible ++; } if ( tile.traversal.wasSetActive !== setActive ) { renderer.invokeOnePlugin( plugin => plugin.setTileActive && plugin.setTileActive( tile, setActive ) ); } if ( tile.traversal.wasSetVisible !== setVisible ) { renderer.invokeOnePlugin( plugin => plugin.setTileVisible && plugin.setTileVisible( tile, setVisible ) ); } } else if ( ! tile.internal.hasRenderableContent ) { // For non-renderable tiles, notify plugins when the tile becomes or stops being a traversal leaf so we // can display "empty" tiles in plugins like the DebugTilesPlugin. setVisible = tile.traversal.isLeaf; if ( tile.traversal.wasSetVisible !== setVisible ) { renderer.invokeOnePlugin( plugin => plugin.setEmptyTileVisible && plugin.setEmptyTileVisible( tile, setVisible ) ); } } // TODO: clean this up since "traversal.active" and "traversal.visible" fields are // overloaded and overused above. // ensure the visible and active fields are set consistently tile.traversal.visible = setVisible; tile.traversal.active = setActive; const children = tile.children; for ( let i = 0, l = children.length; i < l; i ++ ) { const c = children[ i ]; toggleTiles( c, renderer ); } } } export function runTraversal( tile, renderer ) { markUsedTiles( tile, renderer ); markUsedSetLeaves( tile, renderer ); markVisibleTiles( tile, renderer ); toggleTiles( tile, renderer ); }