Browse Source

Merge pull request #135 from NASA-AMMOS/external-tilesets

Add External Tile Sets
Garrett Johnson 4 years ago
parent
commit
df507e2277

+ 40 - 0
TESTCASES.md

@@ -330,3 +330,43 @@ The tileset renders and loads correctly.
 #### expected
 
 Verify that all tiles render on top of each other including the root and that setting the error target and threshold to 0 does not change this. Ensure that raising the error target to the max value will cause deeper tiles to disappear.
+
+## Verify an external tileset loads correctly
+
+#### steps
+
+1. Load `tileset-external.json` in the kitchen sink example.
+1. Zoom in and out.
+1. Set target error to `0`.
+
+#### expected
+
+Verify the tileset loads correctly and there are no missing chunks or errors in the console.
+
+## Verify the rest of the tileset renders correctly if an external tileset fails to load
+
+#### steps
+
+1. Load `tileset-external-broken.json` in the kitchen sink example.
+1. Zoom in and out.
+1. Set target error to `0`.
+
+#### expected
+
+Verify that the external tileset does not load but the rest of the tileset continues to work as expected with a gap.
+
+## Verify an external tileset can unload from the cache
+
+#### steps
+
+1. Load `tileset-external.json` in the kitchen sink example.
+1. Zoom in and out.
+1. Set target error to `0`.
+1. Ensure external tileset has loaded by running `tiles.root.children[0].children[0].children` in the console and verifying it's not empty.
+1. Set the max depth to `0`, and set min and max cache size to `1` before resetting them to force everything to unload. Check the cache display to ensure it's unloaded.
+1. Ensure external tileset is unloaded by `tiles.root.children[0].children[0].children` in the console and verifying it's empty.
+1. Raise the max depth again and verify that the external tileset loads once again.
+
+#### expected
+
+Verify all steps happen as written.

File diff suppressed because it is too large
+ 1 - 0
example/data/tileset-external-broken.json


File diff suppressed because it is too large
+ 1 - 0
example/data/tileset-external-child.json


File diff suppressed because it is too large
+ 1 - 0
example/data/tileset-external.json


+ 1 - 1
example/index.js

@@ -609,7 +609,7 @@ function render() {
 
 	}
 
-	const cacheFullness = tiles.lruCache.itemList.length / tiles.lruCache.minSize;
+	const cacheFullness = tiles.lruCache.itemList.length / tiles.lruCache.maxSize;
 	let str = `Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.group.children.length - 2 }`;
 
 	if ( params.enableCacheDisplay ) {

+ 158 - 86
src/base/TilesRendererBase.js

@@ -93,7 +93,7 @@ export class TilesRendererBase {
 		const rootTileSet = tileSets[ this.rootURL ];
 		if ( ! ( this.rootURL in tileSets ) ) {
 
-			this.loadTileSet( this.rootURL );
+			this.loadRootTileSet( this.rootURL );
 			return;
 
 		} else if ( ! rootTileSet || ! rootTileSet.root ) {
@@ -167,7 +167,21 @@ export class TilesRendererBase {
 
 		tile.parent = parentTile;
 		tile.children = tile.children || [];
-		tile.__contentEmpty = ! tile.content || ! tile.content.uri;
+
+		const uri = tile.content && tile.content.uri;
+		if ( uri ) {
+
+			// "content" should only indicate loadable meshes, not external tile sets
+			const isExternalTileSet = /\.json$/i.test( tile.content.uri );
+			tile.__externalTileSet = isExternalTileSet;
+			tile.__contentEmpty = isExternalTileSet;
+
+		} else {
+
+			tile.__externalTileSet = false;
+			tile.__contentEmpty = true;
+
+		}
 
 		tile.__error = 0.0;
 		tile.__inFrustum = false;
@@ -225,41 +239,58 @@ export class TilesRendererBase {
 	}
 
 	// Private Functions
-	loadTileSet( url ) {
+	fetchTileSet( url, fetchOptions, parent = null ) {
 
-		const tileSets = this.tileSets;
-		if ( ! ( url in tileSets ) ) {
+		return fetch( url, fetchOptions )
+			.then( res => {
+
+				if ( res.ok ) {
 
-			const pr =
-				fetch( url, this.fetchOptions )
-					.then( res => {
+					return res.json();
 
-						if ( res.ok ) {
+				} else {
 
-							return res.json();
+					throw new Error( `TilesRenderer: Failed to load tileset "${ url }" with status ${ res.status } : ${ res.statusText }` );
 
-						} 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"'
+				);
 
-					} )
-					.then( json => {
+				const basePath = path.dirname( url );
 
-						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"'
-						);
+				traverseSet(
+					json.root,
+					( node, parent ) => this.preprocessNode( node, parent, basePath ),
+					null,
+					parent,
+					parent ? parent.__depth : 0,
+				);
 
-						const basePath = path.dirname( url );
+				return json;
 
-						traverseSet( json.root, ( node, parent ) => this.preprocessNode( node, parent, basePath ) );
+			} );
 
-						tileSets[ url ] = json;
+	}
 
-					} );
+	loadRootTileSet( url ) {
+
+		const tileSets = this.tileSets;
+		if ( ! ( url in tileSets ) ) {
+
+			const pr = this
+				.fetchTileSet( url, this.fetchOptions )
+				.then( json => {
+
+					tileSets[ url ] = json;
+
+				} );
 
 			pr.catch( err => {
 
@@ -298,6 +329,7 @@ export class TilesRendererBase {
 		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
@@ -306,6 +338,10 @@ export class TilesRendererBase {
 				t.__loadAbort.abort();
 				t.__loadAbort = null;
 
+			} else if ( isExternalTileSet ) {
+
+				t.children.length = 0;
+
 			} else {
 
 				this.disposeTile( t );
@@ -323,7 +359,6 @@ export class TilesRendererBase {
 
 			}
 
-			t.__buffer = null;
 			t.__loadingState = UNLOADED;
 			t.__loadIndex ++;
 
@@ -342,131 +377,168 @@ export class TilesRendererBase {
 		stats.downloading ++;
 		tile.__loadAbort = controller;
 		tile.__loadingState = LOADING;
-		downloadQueue.add( tile, tile => {
 
+		const errorCallback = e => {
+
+			// if it has been unloaded then the tile has been disposed
 			if ( tile.__loadIndex !== loadIndex ) {
 
-				return Promise.resolve();
+				return;
 
 			}
 
-			return fetch( tile.content.uri, Object.assign( { signal }, this.fetchOptions ) );
+			if ( e.name !== 'AbortError' ) {
 
-		} )
-			.then( res => {
+				parseQueue.remove( tile );
+				downloadQueue.remove( tile );
 
-				if ( tile.__loadIndex !== loadIndex ) {
+				if ( tile.__loadingState === PARSING ) {
+
+					stats.parsing --;
+
+				} else if ( tile.__loadingState === LOADING ) {
 
-					return;
+					stats.downloading --;
 
 				}
 
-				if ( res.ok ) {
+				stats.failed ++;
 
-					return res.arrayBuffer();
+				console.error( 'TilesRenderer : Failed to load tile.' );
+				console.error( e );
+				tile.__loadingState = FAILED;
 
-				} else {
+			} else {
 
-					throw new Error( `Failed to load model with error code ${res.status}` );
+				lruCache.remove( tile );
 
-				}
+			}
 
-			} )
-			.then( buffer => {
+		};
+
+		if ( isExternalTileSet ) {
+
+			downloadQueue.add( tile, tile => {
 
 				// if it has been unloaded then the tile has been disposed
 				if ( tile.__loadIndex !== loadIndex ) {
 
-					return;
+					return Promise.resolve();
 
 				}
 
-				stats.downloading --;
-				stats.parsing ++;
-				tile.__loadAbort = null;
-				tile.__loadingState = PARSING;
-				tile.__buffer = buffer;
+				return this.fetchTileSet( tile.content.uri, Object.assign( { signal }, this.fetchOptions ), tile );
 
-				return parseQueue.add( tile, tile => {
+			} )
+				.then( json => {
 
 					// if it has been unloaded then the tile has been disposed
 					if ( tile.__loadIndex !== loadIndex ) {
 
-						return Promise.resolve();
+						return;
 
 					}
 
-					const uri = tile.content.uri;
-					const extension = uri.split( /\./g ).pop();
-					const buffer = tile.__buffer;
-					tile.__buffer = null;
+					stats.downloading --;
+					tile.__loadAbort = null;
+					tile.__loadingState = LOADED;
 
-					return this.parseTile( buffer, tile, extension );
+					tile.children.push( json.root );
 
-				} );
+				} )
+				.catch( errorCallback );
 
-			} )
-			.then( () => {
+		} else {
+
+			downloadQueue.add( tile, tile => {
 
-				// if it has been unloaded then the tile has been disposed
 				if ( tile.__loadIndex !== loadIndex ) {
 
-					return;
+					return Promise.resolve();
 
 				}
 
-				stats.parsing --;
-				tile.__loadingState = LOADED;
-				if ( tile.__wasSetVisible ) {
+				return fetch( tile.content.uri, Object.assign( { signal }, this.fetchOptions ) );
 
-					this.setTileVisible( tile, true );
+			} )
+				.then( res => {
 
-				}
+					if ( tile.__loadIndex !== loadIndex ) {
 
-				if ( tile.__wasSetActive ) {
+						return;
 
-					this.setTileActive( tile, true );
+					}
 
-				}
+					if ( res.ok ) {
 
-			} )
-			.catch( e => {
+						return res.arrayBuffer();
 
-				// if it has been unloaded then the tile has been disposed
-				if ( tile.__loadIndex !== loadIndex ) {
+					} else {
 
-					return;
+						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 ) {
 
-				if ( e.name !== 'AbortError' ) {
+						return;
 
-					parseQueue.remove( tile );
-					downloadQueue.remove( tile );
+					}
+
+					stats.downloading --;
+					stats.parsing ++;
+					tile.__loadAbort = null;
+					tile.__loadingState = PARSING;
+
+					return parseQueue.add( tile, tile => {
 
-					if ( tile.__loadingState === PARSING ) {
+						// if it has been unloaded then the tile has been disposed
+						if ( tile.__loadIndex !== loadIndex ) {
+
+							return Promise.resolve();
+
+						}
 
-						stats.parsing --;
+						const uri = tile.content.uri;
+						const extension = uri.split( /\./g ).pop();
 
-					} else if ( tile.__loadingState === LOADING ) {
+						return this.parseTile( buffer, tile, extension );
 
-						stats.downloading --;
+					} );
+
+				} )
+				.then( () => {
+
+					// if it has been unloaded then the tile has been disposed
+					if ( tile.__loadIndex !== loadIndex ) {
+
+						return;
 
 					}
 
-					stats.failed ++;
+					stats.parsing --;
+					tile.__loadingState = LOADED;
 
-					console.error( 'TilesRenderer : Failed to load tile.' );
-					console.error( e );
-					tile.__loadingState = FAILED;
+					if ( tile.__wasSetVisible ) {
 
-				} else {
+						this.setTileVisible( tile, true );
 
-					lruCache.remove( tile );
+					}
 
-				}
+					if ( tile.__wasSetActive ) {
 
-			} );
+						this.setTileActive( tile, true );
+
+					}
+
+				} )
+				.catch( errorCallback );
+
+		}
 
 	}
 

+ 20 - 8
src/base/traverseFunctions.js

@@ -54,7 +54,13 @@ function recursivelyMarkUsed( tile, frameCount, lruCache ) {
 
 function recursivelyLoadTiles( tile, depthFromRenderedParent, renderer ) {
 
-	if ( tile.__contentEmpty ) {
+	// Try to load any external tile set children if the external tile set has loaded.
+	const doTraverse =
+		tile.__contentEmpty && (
+			! tile.__externalTileSet ||
+			isDownloadFinished( tile.__loadingState )
+		);
+	if ( doTraverse ) {
 
 		const children = tile.children;
 		for ( let i = 0, l = children.length; i < l; i ++ ) {
@@ -76,7 +82,7 @@ function recursivelyLoadTiles( tile, depthFromRenderedParent, renderer ) {
 
 }
 
-// Helper function for recursively traversing a tileset. If `beforeCb` returns `true` then the
+// Helper function for recursively traversing a tile set. If `beforeCb` returns `true` then the
 // traversal will end early.
 export function traverseSet( tile, beforeCb = null, afterCb = null, parent = null, depth = 0 ) {
 
@@ -135,8 +141,9 @@ export function determineFrustumSet( tile, renderer ) {
 	tile.__inFrustum = true;
 	stats.inFrustum ++;
 
-	// Early out if this tile has less error than we're targeting.
-	if ( stopAtEmptyTiles || ! tile.__contentEmpty ) {
+	// Early out if this tile has less error than we're targeting but don't stop
+	// at an external tile set.
+	if ( ( stopAtEmptyTiles || ! tile.__contentEmpty ) && ! tile.__externalTileSet ) {
 
 		const error = renderer.calculateError( tile );
 		tile.__error = error;
@@ -226,7 +233,10 @@ export function markUsedSetLeaves( tile, renderer ) {
 
 			if ( isUsedThisFrame( c, frameCount ) ) {
 
-				const childLoaded = ( ! c.__contentEmpty && isDownloadFinished( c.__loadingState ) ) || c.__allChildrenLoaded;
+				const childLoaded =
+					c.__allChildrenLoaded ||
+					( ! c.__contentEmpty && isDownloadFinished( c.__loadingState ) ) ||
+					( c.__externalTileSet && c.__loadingState === FAILED );
 				allChildrenLoaded = allChildrenLoaded && childLoaded;
 
 			}
@@ -235,6 +245,7 @@ export function markUsedSetLeaves( tile, renderer ) {
 		tile.__childrenWereVisible = childrenWereVisible;
 		tile.__allChildrenLoaded = allChildrenLoaded;
 
+
 	}
 
 }
@@ -271,7 +282,7 @@ export function skipTraversal( tile, renderer ) {
 			tile.__active = true;
 			stats.active ++;
 
-		} else if ( ! lruCache.isFull() && ! tile.__contentEmpty ) {
+		} else if ( ! lruCache.isFull() && ( ! tile.__contentEmpty || tile.__externalTileSet ) ) {
 
 			renderer.requestTileContents( tile );
 
@@ -284,7 +295,8 @@ export function skipTraversal( tile, renderer ) {
 	const errorRequirement = ( renderer.errorTarget + 1 ) * renderer.errorThreshold;
 	const meetsSSE = tile.__error <= errorRequirement;
 	const includeTile = meetsSSE || tile.refine === 'ADD';
-	const hasContent = ! tile.__contentEmpty;
+	const hasModel = ! tile.__contentEmpty;
+	const hasContent = hasModel || tile.__externalTileSet;
 	const loadedContent = isDownloadFinished( tile.__loadingState ) && hasContent;
 	const childrenWereVisible = tile.__childrenWereVisible;
 	const children = tile.children;
@@ -292,7 +304,7 @@ export function skipTraversal( tile, renderer ) {
 
 	// Increment the relative depth of the node to the nearest rendered parent if it has content
 	// and is being rendered.
-	if ( includeTile && hasContent ) {
+	if ( includeTile && hasModel ) {
 
 		tile.__depthFromRenderedParent ++;
 

+ 11 - 4
src/three/TilesRenderer.js

@@ -230,14 +230,21 @@ export class TilesRenderer extends TilesRendererBase {
 	}
 
 	/* Overriden */
-	loadTileSet( url ) {
+	fetchTileSet( url, ...rest ) {
 
-		const pr = super.loadTileSet( url );
-		pr.then( () => {
+		const pr = super.fetchTileSet( url, ...rest );
+		pr.then( json => {
 
 			if ( this.onLoadTileSet ) {
 
-				this.onLoadTileSet( this.tileSets[ url ] );
+				// Push this onto the end of the event stack to ensure this runs
+				// after the base renderer has placed the provided json where it
+				// needs to be placed and is ready for an update.
+				Promise.resolve().then( () => {
+
+					this.onLoadTileSet( json, url );
+
+				} );
 
 			}