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
 #### 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 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 }`;
 	let str = `Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.group.children.length - 2 }`;
 
 
 	if ( params.enableCacheDisplay ) {
 	if ( params.enableCacheDisplay ) {

+ 158 - 86
src/base/TilesRendererBase.js

@@ -93,7 +93,7 @@ export class TilesRendererBase {
 		const rootTileSet = tileSets[ this.rootURL ];
 		const rootTileSet = tileSets[ this.rootURL ];
 		if ( ! ( this.rootURL in tileSets ) ) {
 		if ( ! ( this.rootURL in tileSets ) ) {
 
 
-			this.loadTileSet( this.rootURL );
+			this.loadRootTileSet( this.rootURL );
 			return;
 			return;
 
 
 		} else if ( ! rootTileSet || ! rootTileSet.root ) {
 		} else if ( ! rootTileSet || ! rootTileSet.root ) {
@@ -167,7 +167,21 @@ export class TilesRendererBase {
 
 
 		tile.parent = parentTile;
 		tile.parent = parentTile;
 		tile.children = tile.children || [];
 		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.__error = 0.0;
 		tile.__inFrustum = false;
 		tile.__inFrustum = false;
@@ -225,41 +239,58 @@ export class TilesRendererBase {
 	}
 	}
 
 
 	// Private Functions
 	// 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 => {
 			pr.catch( err => {
 
 
@@ -298,6 +329,7 @@ export class TilesRendererBase {
 		const lruCache = this.lruCache;
 		const lruCache = this.lruCache;
 		const downloadQueue = this.downloadQueue;
 		const downloadQueue = this.downloadQueue;
 		const parseQueue = this.parseQueue;
 		const parseQueue = this.parseQueue;
+		const isExternalTileSet = tile.__externalTileSet;
 		lruCache.add( tile, t => {
 		lruCache.add( tile, t => {
 
 
 			// Stop the load if it's started
 			// Stop the load if it's started
@@ -306,6 +338,10 @@ export class TilesRendererBase {
 				t.__loadAbort.abort();
 				t.__loadAbort.abort();
 				t.__loadAbort = null;
 				t.__loadAbort = null;
 
 
+			} else if ( isExternalTileSet ) {
+
+				t.children.length = 0;
+
 			} else {
 			} else {
 
 
 				this.disposeTile( t );
 				this.disposeTile( t );
@@ -323,7 +359,6 @@ export class TilesRendererBase {
 
 
 			}
 			}
 
 
-			t.__buffer = null;
 			t.__loadingState = UNLOADED;
 			t.__loadingState = UNLOADED;
 			t.__loadIndex ++;
 			t.__loadIndex ++;
 
 
@@ -342,131 +377,168 @@ export class TilesRendererBase {
 		stats.downloading ++;
 		stats.downloading ++;
 		tile.__loadAbort = controller;
 		tile.__loadAbort = controller;
 		tile.__loadingState = LOADING;
 		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 ) {
 			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 it has been unloaded then the tile has been disposed
 				if ( tile.__loadIndex !== loadIndex ) {
 				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 it has been unloaded then the tile has been disposed
 					if ( tile.__loadIndex !== loadIndex ) {
 					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 ) {
 				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 ) {
 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;
 		const children = tile.children;
 		for ( let i = 0, l = children.length; i < l; i ++ ) {
 		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.
 // traversal will end early.
 export function traverseSet( tile, beforeCb = null, afterCb = null, parent = null, depth = 0 ) {
 export function traverseSet( tile, beforeCb = null, afterCb = null, parent = null, depth = 0 ) {
 
 
@@ -135,8 +141,9 @@ export function determineFrustumSet( tile, renderer ) {
 	tile.__inFrustum = true;
 	tile.__inFrustum = true;
 	stats.inFrustum ++;
 	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 );
 		const error = renderer.calculateError( tile );
 		tile.__error = error;
 		tile.__error = error;
@@ -226,7 +233,10 @@ export function markUsedSetLeaves( tile, renderer ) {
 
 
 			if ( isUsedThisFrame( c, frameCount ) ) {
 			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;
 				allChildrenLoaded = allChildrenLoaded && childLoaded;
 
 
 			}
 			}
@@ -235,6 +245,7 @@ export function markUsedSetLeaves( tile, renderer ) {
 		tile.__childrenWereVisible = childrenWereVisible;
 		tile.__childrenWereVisible = childrenWereVisible;
 		tile.__allChildrenLoaded = allChildrenLoaded;
 		tile.__allChildrenLoaded = allChildrenLoaded;
 
 
+
 	}
 	}
 
 
 }
 }
@@ -271,7 +282,7 @@ export function skipTraversal( tile, renderer ) {
 			tile.__active = true;
 			tile.__active = true;
 			stats.active ++;
 			stats.active ++;
 
 
-		} else if ( ! lruCache.isFull() && ! tile.__contentEmpty ) {
+		} else if ( ! lruCache.isFull() && ( ! tile.__contentEmpty || tile.__externalTileSet ) ) {
 
 
 			renderer.requestTileContents( tile );
 			renderer.requestTileContents( tile );
 
 
@@ -284,7 +295,8 @@ export function skipTraversal( tile, renderer ) {
 	const errorRequirement = ( renderer.errorTarget + 1 ) * renderer.errorThreshold;
 	const errorRequirement = ( renderer.errorTarget + 1 ) * renderer.errorThreshold;
 	const meetsSSE = tile.__error <= errorRequirement;
 	const meetsSSE = tile.__error <= errorRequirement;
 	const includeTile = meetsSSE || tile.refine === 'ADD';
 	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 loadedContent = isDownloadFinished( tile.__loadingState ) && hasContent;
 	const childrenWereVisible = tile.__childrenWereVisible;
 	const childrenWereVisible = tile.__childrenWereVisible;
 	const children = tile.children;
 	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
 	// Increment the relative depth of the node to the nearest rendered parent if it has content
 	// and is being rendered.
 	// and is being rendered.
-	if ( includeTile && hasContent ) {
+	if ( includeTile && hasModel ) {
 
 
 		tile.__depthFromRenderedParent ++;
 		tile.__depthFromRenderedParent ++;
 
 

+ 11 - 4
src/three/TilesRenderer.js

@@ -230,14 +230,21 @@ export class TilesRenderer extends TilesRendererBase {
 	}
 	}
 
 
 	/* Overriden */
 	/* 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 ) {
 			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 );
+
+				} );
 
 
 			}
 			}