own-tiles-renderer2
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
729 lines (453 loc) • 13.7 kB
JavaScript
import { getUrlExtension } from '../utilities/urlExtension.js';
import { LRUCache } from '../utilities/LRUCache.js';
import { PriorityQueue } from '../utilities/PriorityQueue.js';
import { determineFrustumSet, toggleTiles, skipTraversal, markUsedSetLeaves, traverseSet } from './traverseFunctions.js';
import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';
/**
* Function for provided to sort all tiles for prioritizing loading/unloading.
*
* @param {Tile} a
* @param {Tile} b
* @returns number
*/
const priorityCallback = ( a, b ) => {
if ( a.__depth !== b.__depth ) {
// load shallower tiles first
return a.__depth > b.__depth ? - 1 : 1;
} else if ( a.__inFrustum !== b.__inFrustum ) {
// load tiles that are in the frustum at the current depth
return a.__inFrustum ? 1 : - 1;
} else if ( a.__used !== b.__used ) {
// load tiles that have been used
return a.__used ? 1 : - 1;
} else if ( a.__error !== b.__error ) {
// load the tile with the higher error
return a.__error > b.__error ? 1 : - 1;
} else if ( a.__distanceFromCamera !== b.__distanceFromCamera ) {
// and finally visible tiles which have equal error (ex: if geometricError === 0)
// should prioritize based on distance.
return a.__distanceFromCamera > b.__distanceFromCamera ? - 1 : 1;
}
return 0;
};
/**
* Function for sorting the evicted LRU items. We should evict the shallowest depth first.
* @param {Tile} tile
* @returns number
*/
const lruPriorityCallback = ( tile ) => 1 / ( tile.__depthFromRenderedParent + 1 );
export class TilesRendererBase {
get rootTileSet() {
const tileSet = this.tileSets[ this.rootURL ];
if ( ! tileSet || tileSet instanceof Promise ) {
return null;
} else {
return tileSet;
}
}
get root() {
const tileSet = this.rootTileSet;
return tileSet ? tileSet.root : null;
}
constructor( url ) {
// state
this.tileSets = {};
this.rootURL = url;
this.fetchOptions = {};
this.preprocessURL = null;
const lruCache = new LRUCache();
lruCache.unloadPriorityCallback = lruPriorityCallback;
// 使用shadre callback
lruCache.unloadTile = this.getUnloadTileCallBack();
const downloadQueue = new PriorityQueue();
downloadQueue.maxJobs = 4;
downloadQueue.priorityCallback = priorityCallback;
const parseQueue = new PriorityQueue();
parseQueue.maxJobs = 1;
parseQueue.priorityCallback = priorityCallback;
this.lruCache = lruCache;
this.downloadQueue = downloadQueue;
this.parseQueue = parseQueue;
this.stats = {
parsing: 0,
downloading: 0,
failed: 0,
inFrustum: 0,
used: 0,
active: 0,
visible: 0,
};
this.frameCount = 0;
// options
this.errorTarget = 6.0;
this.errorThreshold = Infinity;
this.loadSiblings = true;
this.displayActiveTiles = false;
this.maxDepth = Infinity;
this.stopAtEmptyTiles = true;
}
getUnloadTileCallBack( ) {
return ( tile ) => {
const isExternalTileSet = tile.__externalTileSet;
if ( tile.__loadingState === LOADING ) {
tile.__loadAbort.abort();
tile.__loadAbort = null;
} else if ( isExternalTileSet ) {
tile.children.length = 0;
} else {
this.disposeTile( tile );
}
// Decrement stats
if ( tile.__loadingState === LOADING ) {
this.stats.downloading --;
} else if ( tile.__loadingState === PARSING ) {
this.stats.parsing --;
}
tile.__loadingState = UNLOADED;
tile.__loadIndex ++;
const byteLength = tile.byteLength || 0;
console.log( 'bytelength', byteLength );
this.stats.cacheBytes -= byteLength;
this.parseQueue.remove( tile );
this.downloadQueue.remove( tile );
};
}
traverse( beforecb, aftercb ) {
const tileSets = this.tileSets;
const rootTileSet = tileSets[ this.rootURL ];
if ( ! rootTileSet || ! rootTileSet.root ) return;
traverseSet( rootTileSet.root, beforecb, aftercb );
}
// Public API
update() {
const stats = this.stats;
const lruCache = this.lruCache;
const tileSets = this.tileSets;
const rootTileSet = tileSets[ this.rootURL ];
if ( ! ( this.rootURL in tileSets ) ) {
this.loadRootTileSet( this.rootURL );
return;
} else if ( ! rootTileSet || ! rootTileSet.root ) {
return;
}
const root = rootTileSet.root;
this.updateDynamicScreenSpaceError( root );
stats.inFrustum = 0,
stats.used = 0,
stats.active = 0,
stats.visible = 0,
this.frameCount ++;
determineFrustumSet( root, this );
markUsedSetLeaves( root, this );
skipTraversal( root, this );
toggleTiles( root, this );
lruCache.scheduleUnload();
}
// Overrideable
parseTile( buffer, tile, extension ) {
return null;
}
disposeTile( tile ) {
}
preprocessNode( tile, parentTile, tileSetDir ) {
if ( tile.content ) {
// Fix old file formats
if ( ! ( 'uri' in tile.content ) && 'url' in tile.content ) {
tile.content.uri = tile.content.url;
delete tile.content.url;
}
if ( tile.content.uri ) {
// tile content uri has to be interpreted relative to the tileset.json
tile.content.uri = new URL( tile.content.uri, tileSetDir + '/' ).toString();
}
// NOTE: fix for some cases where tilesets provide the bounding volume
// but volumes are not present.
if (
tile.content.boundingVolume &&
! (
'box' in tile.content.boundingVolume ||
'sphere' in tile.content.boundingVolume ||
'region' in tile.content.boundingVolume
)
) {
delete tile.content.boundingVolume;
}
}
tile.parent = parentTile;
tile.children = tile.children || [];
const uri = tile.content && tile.content.uri;
if ( uri ) {
// "content" should only indicate loadable meshes, not external tile sets
const extension = getUrlExtension( tile.content.uri );
const isExternalTileSet = Boolean( extension && extension.toLowerCase() === 'json' );
tile.__externalTileSet = isExternalTileSet;
tile.__contentEmpty = isExternalTileSet;
} else {
tile.__externalTileSet = false;
tile.__contentEmpty = true;
}
// Expected to be set during calculateError()
tile.__distanceFromCamera = Infinity;
tile.__error = Infinity;
tile.__inFrustum = false;
tile.__isLeaf = false;
tile.__usedLastFrame = false;
tile.__used = false;
tile.__wasSetVisible = false;
tile.__visible = false;
tile.__childrenWereVisible = false;
tile.__allChildrenLoaded = false;
tile.__wasSetActive = false;
tile.__active = false;
tile.__loadingState = UNLOADED;
tile.__loadIndex = 0;
tile.__loadAbort = null;
tile.__depthFromRenderedParent = - 1;
if ( parentTile === null ) {
tile.__depth = 0;
tile.refine = tile.refine || 'REPLACE';
} else {
tile.__depth = parentTile.__depth + 1;
tile.refine = tile.refine || parentTile.refine;
}
}
isOnScreenLongEnough() {
return true;
}
isStopppedMovingLongEnough() {
return true;
}
updateDynamicScreenSpaceError() {
return;
}
setTileActive( tile, state ) {
}
setTileVisible( tile, state ) {
}
calculateError( tile ) {
return 0;
}
tileInView( tile ) {
return true;
}
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 ) {
return fetch( url, fetchOptions )
.then( res => {
if ( res.ok ) {
return res.json();
} else {
throw new Error( `TilesRenderer: Failed to load tileset "${ url }" with status ${ res.status } : ${ res.statusText }` );
}
} )
.then( json => {
const version = json.asset.version;
console.assert(
version === '1.0' || version === '0.0',
'asset.version is expected to be a string of "1.0" or "0.0"'
);
// remove trailing slash and last path-segment from the URL
let basePath = url.replace( /\/[^\/]*\/?$/, '' );
basePath = new URL( basePath, window.location.href ).toString();
traverseSet(
json.root,
( node, parent ) => this.preprocessNode( node, parent, basePath ),
null,
parent,
parent ? parent.__depth : 0,
);
return json;
} );
}
loadRootTileSet( url ) {
const tileSets = this.tileSets;
if ( ! ( url in tileSets ) ) {
const pr = this
.fetchTileSet( this.preprocessURL ? this.preprocessURL( url ) : url, this.fetchOptions )
.then( json => {
tileSets[ url ] = json;
} );
pr.catch( err => {
console.error( err );
tileSets[ url ] = err;
} );
tileSets[ url ] = pr;
return pr;
} else if ( tileSets[ url ] instanceof Error ) {
return Promise.reject( tileSets[ url ] );
} else {
return Promise.resolve( tileSets[ url ] );
}
}
requestTileContents( tile ) {
// If the tile is already being loaded then don't
// start it again.
if ( tile.__loadingState !== UNLOADED ) {
return;
}
if ( ! this.isOnScreenLongEnough( tile ) ) {
return;
}
if ( ! this.isStopppedMovingLongEnough( tile ) ) {
return;
}
const stats = this.stats;
const lruCache = this.lruCache;
const downloadQueue = this.downloadQueue;
const parseQueue = this.parseQueue;
const isExternalTileSet = tile.__externalTileSet;
lruCache.add( tile, t => {
// Stop the load if it's started
if ( t.__loadingState === LOADING ) {
t.__loadAbort.abort();
t.__loadAbort = null;
} else if ( isExternalTileSet ) {
t.children.length = 0;
} else {
this.disposeTile( t );
}
// Decrement stats
if ( t.__loadingState === LOADING ) {
stats.downloading --;
} else if ( t.__loadingState === PARSING ) {
stats.parsing --;
}
t.__loadingState = UNLOADED;
t.__loadIndex ++;
parseQueue.remove( t );
downloadQueue.remove( t );
} );
// Track a new load index so we avoid the condition where this load is stopped and
// another begins soon after so we don't parse twice.
tile.__loadIndex ++;
const loadIndex = tile.__loadIndex;
const controller = new AbortController();
const signal = controller.signal;
stats.downloading ++;
tile.__loadAbort = controller;
tile.__loadingState = LOADING;
const errorCallback = e => {
// if it has been unloaded then the tile has been disposed
if ( tile.__loadIndex !== loadIndex ) {
return;
}
if ( e.name !== 'AbortError' ) {
parseQueue.remove( tile );
downloadQueue.remove( tile );
if ( tile.__loadingState === PARSING ) {
stats.parsing --;
} else if ( tile.__loadingState === LOADING ) {
stats.downloading --;
}
stats.failed ++;
console.error( `TilesRenderer : Failed to load tile at url "${ tile.content.uri }".` );
console.error( e );
tile.__loadingState = FAILED;
} else {
lruCache.remove( tile );
}
};
if ( isExternalTileSet ) {
downloadQueue.add( tile, tileCb => {
// if it has been unloaded then the tile has been disposed
if ( tileCb.__loadIndex !== loadIndex ) {
return Promise.resolve();
}
const uri = this.preprocessURL ? this.preprocessURL( tileCb.content.uri ) : tileCb.content.uri;
return this.fetchTileSet( uri, Object.assign( { signal }, this.fetchOptions ), tileCb );
} )
.then( json => {
// if it has been unloaded then the tile has been disposed
if ( tile.__loadIndex !== loadIndex ) {
return;
}
stats.downloading --;
tile.__loadAbort = null;
tile.__loadingState = LOADED;
tile.children.push( json.root );
} )
.catch( errorCallback );
} else {
downloadQueue.add( tile, downloadTile => {
if ( downloadTile.__loadIndex !== loadIndex ) {
return Promise.resolve();
}
const uri = this.preprocessURL ? this.preprocessURL( downloadTile.content.uri ) : downloadTile.content.uri;
return fetch( uri, Object.assign( { signal }, this.fetchOptions ) );
} )
.then( res => {
if ( tile.__loadIndex !== loadIndex ) {
return;
}
if ( res.ok ) {
return res.arrayBuffer();
} else {
throw new Error( `Failed to load model with error code ${res.status}` );
}
} )
.then( buffer => {
// if it has been unloaded then the tile has been disposed
if ( tile.__loadIndex !== loadIndex ) {
return;
}
stats.downloading --;
stats.parsing ++;
tile.__loadAbort = null;
tile.__loadingState = PARSING;
return parseQueue.add( tile, parseTile => {
// if it has been unloaded then the tile has been disposed
if ( parseTile.__loadIndex !== loadIndex ) {
return Promise.resolve();
}
const uri = parseTile.content.uri;
const extension = getUrlExtension( uri );
return this.parseTile( buffer, parseTile, extension );
} );
} )
.then( () => {
// if it has been unloaded then the tile has been disposed
if ( tile.__loadIndex !== loadIndex ) {
return;
}
stats.parsing --;
tile.__loadingState = LOADED;
if ( tile.__wasSetVisible ) {
this.setTileVisible( tile, true );
}
if ( tile.__wasSetActive ) {
this.setTileActive( tile, true );
}
} )
.catch( errorCallback );
}
}
dispose() {
const lruCache = this.lruCache;
this.traverse( tile => {
lruCache.remove( tile );
} );
this.stats = {
parsing: 0,
downloading: 0,
failed: 0,
inFrustum: 0,
used: 0,
active: 0,
visible: 0,
};
this.frameCount = 0;
}
}
TilesRendererBase.prototype.unloadTile = unloadTile;