diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 13d14f069a9a..dd59a9425ff1 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -23,8 +23,7 @@ define([ './Empty3DTileContentProvider', './PerInstanceColorAppearance', './Primitive', - './TileBoundingBox', - './Tileset3DTileContentProvider' + './TileBoundingBox' ], function( BoxOutlineGeometry, Cartesian3, @@ -49,8 +48,7 @@ define([ Empty3DTileContentProvider, PerInstanceColorAppearance, Primitive, - TileBoundingBox, - Tileset3DTileContentProvider) { + TileBoundingBox) { "use strict"; /** @@ -123,6 +121,8 @@ define([ */ this.numberOfChildrenWithoutContent = defined(header.children) ? header.children.length : 0; + this._numberOfUnrefinableChildren = this.numberOfChildrenWithoutContent; + /** * DOC_TBA * @@ -130,6 +130,13 @@ define([ */ this.hasTilesetContent = false; + /** + * DOC_TBA + * + * @readonly + */ + this.hasContent = true; + /** * DOC_TBA * @@ -147,17 +154,35 @@ define([ if (type === 'json') { this.hasTilesetContent = true; - content = new Tileset3DTileContentProvider(); - } else if (defined(contentFactory)) { + this.hasContent = false; + this._numberOfUnrefinableChildren = 1; + } + + if (defined(contentFactory)) { content = contentFactory(tileset, this, url); } else { throw new DeveloperError('Unknown tile content type, ' + type + ', for ' + url); } } else { content = new Empty3DTileContentProvider(); + this.hasContent = false; } this._content = content; + function setRefinable(tile) { + var parent = tile.parent; + if (defined(parent) && (tile.hasContent || tile.isRefinable())) { + // When a tile with content is loaded, its parent can safely refine to it without any gaps in rendering + // Since an empty tile doesn't have content of its own, its descendants with content need to be loaded + // before the parent is able to refine to it. + --parent._numberOfUnrefinableChildren; + // If the parent is empty, traverse up the tree to update ancestor tiles. + if (!parent.hasContent) { + setRefinable(parent); + } + } + } + var that = this; // Content enters the READY state @@ -166,6 +191,8 @@ define([ --that.parent.numberOfChildrenWithoutContent; } + setRefinable(that); + that.readyPromise.resolve(that); }).otherwise(function(error) { that.readyPromise.reject(error); @@ -237,6 +264,13 @@ define([ return this._content.state === Cesium3DTileContentState.READY; }; + /** + * DOC_TBA + */ + Cesium3DTile.prototype.isRefinable = function() { + return this._numberOfUnrefinableChildren === 0; + }; + /** * DOC_TBA */ diff --git a/Source/Scene/Cesium3DTileContentProviderFactory.js b/Source/Scene/Cesium3DTileContentProviderFactory.js index b3a7876919ff..3cd080c8e18d 100644 --- a/Source/Scene/Cesium3DTileContentProviderFactory.js +++ b/Source/Scene/Cesium3DTileContentProviderFactory.js @@ -4,13 +4,15 @@ define([ './Composite3DTileContentProvider', './Empty3DTileContentProvider', './Instanced3DModel3DTileContentProvider', - './Points3DTileContentProvider' + './Points3DTileContentProvider', + './Tileset3DTileContentProvider' ], function( Batched3DModel3DTileContentProvider, Composite3DTileContentProvider, Empty3DTileContentProvider, Instanced3DModel3DTileContentProvider, - Points3DTileContentProvider) { + Points3DTileContentProvider, + Tileset3DTileContentProvider) { "use strict"; /** @@ -29,6 +31,9 @@ define([ cmpt : function(tileset, tile, url) { // Send in the factory in order to avoid a cyclical dependency return new Composite3DTileContentProvider(tileset, tile, url, Cesium3DTileContentProviderFactory); + }, + json : function(tileset, tile, url) { + return new Tileset3DTileContentProvider(tileset, tile, url); } }; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 4a1948d9eb5d..92358205ef76 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -13,7 +13,6 @@ define([ '../ThirdParty/when', './Cesium3DTile', './Cesium3DTileRefine', - './Cesium3DTileContentState', './CullingVolume', './SceneMode' ], function( @@ -30,7 +29,6 @@ define([ when, Cesium3DTile, Cesium3DTileRefine, - Cesium3DTileContentState, CullingVolume, SceneMode) { "use strict"; @@ -151,60 +149,16 @@ define([ var that = this; var tilesJson = url + 'tiles.json'; - loadTilesJson(this, tilesJson, undefined, function(data) { + this.loadTilesJson(tilesJson, undefined).then(function(data) { var tree = data.tree; that._properties = tree.properties; that._geometricError = tree.geometricError; that._root = data.root; that._readyPromise.resolve(that); - }); - }; - - function loadTilesJson(tileset, tilesJson, parentTile, done) { - loadJson(tilesJson).then(function(tree) { - var baseUrl = tileset.url; - var rootTile = new Cesium3DTile(tileset, baseUrl, tree.root, parentTile); - - // If there is a parentTile, add the root of the currently loading - // tileset to parentTile's children, and increment its numberOfChildrenWithoutContent - // with 1 - if (defined(parentTile)) { - parentTile.children.push(rootTile); - ++parentTile.numberOfChildrenWithoutContent; - } - - var stack = []; - stack.push({ - header : tree.root, - cesium3DTile : rootTile - }); - - while (stack.length > 0) { - var t = stack.pop(); - var children = t.header.children; - if (defined(children)) { - var length = children.length; - for (var k = 0; k < length; ++k) { - var childHeader = children[k]; - var childTile = new Cesium3DTile(tileset, baseUrl, childHeader, t.cesium3DTile); - t.cesium3DTile.children.push(childTile); - - stack.push({ - header : childHeader, - cesium3DTile : childTile - }); - } - } - } - - done({ - tree : tree, - root : rootTile - }); }).otherwise(function(error) { - tileset._readyPromise.reject(error); + that._readyPromise.reject(error); }); - } + }; defineProperties(Cesium3DTileset.prototype, { /** @@ -272,6 +226,53 @@ define([ } }); + /** + * @private + */ + Cesium3DTileset.prototype.loadTilesJson = function(tilesJson, parentTile) { + var tileset = this; + return loadJson(tilesJson).then(function(tree) { + var baseUrl = tileset.url; + var rootTile = new Cesium3DTile(tileset, baseUrl, tree.root, parentTile); + + // If there is a parentTile, add the root of the currently loading tileset + // to parentTile's children, and increment its numberOfChildrenWithoutContent + if (defined(parentTile)) { + parentTile.children.push(rootTile); + ++parentTile.numberOfChildrenWithoutContent; + } + + var stack = []; + stack.push({ + header : tree.root, + cesium3DTile : rootTile + }); + + while (stack.length > 0) { + var t = stack.pop(); + var children = t.header.children; + if (defined(children)) { + var length = children.length; + for (var k = 0; k < length; ++k) { + var childHeader = children[k]; + var childTile = new Cesium3DTile(tileset, baseUrl, childHeader, t.cesium3DTile); + t.cesium3DTile.children.push(childTile); + + stack.push({ + header : childHeader, + cesium3DTile : childTile + }); + } + } + } + + return { + tree : tree, + root : rootTile + }; + }); + }; + function getScreenSpaceError(geometricError, tile, frameState) { // TODO: screenSpaceError2D like QuadtreePrimitive.js if (geometricError === 0.0) { @@ -316,7 +317,10 @@ define([ var requestScheduler = new RequestScheduler(); /////////////////////////////////////////////////////////////////////////// - function requestContent(tiles3D, tile) { + function requestContent(tiles3D, tile, outOfCore) { + if (!outOfCore) { + return; + } if (!requestScheduler.hasAvailableRequests()) { return; } @@ -332,47 +336,6 @@ define([ when(tile.readyPromise).then(removeFunction).otherwise(removeFunction); } - function selectTileWithTilesetContent(tiles3D, selectedTiles, tile, fullyVisible, frameState, additiveRefinment) { - // 1) If its children are not loaded, load the subtree it points to and then select its root child - // 2) If its children are already loaded, select its (root) child since the geometric error of it is - // same as this tile's - - // If the subtree has already been added and child - // content requested, select the child (= the root) and continue - if ((tile.isReady()) && - (tile.numberOfChildrenWithoutContent === 0)) { - // A tiles.json must specify exactly one tile, ie a root - var root = tile.children[0]; - if (root.hasTilesetContent) { - selectTileWithTilesetContent(tiles3D, selectedTiles, root, fullyVisible, frameState, additiveRefinment); - } else { - if (root.isContentUnloaded()) { - requestContent(tiles3D, root); - } else if (root.isReady()){ - selectTile(selectedTiles, root, fullyVisible, frameState); - } - } - return; - } else if (!additiveRefinment) { - // Otherwise, for replacement refinment, select the parent tile to avoid - // showing an empty space while waiting for tile to load - if (defined(tile.parent)) { - selectTile(selectedTiles, tile.parent, fullyVisible, frameState); - } - } - - // Request the tile's tileset if it's unloaded. - // In the case of tileset content tiles, their state must be explicitly set - if (tile.isContentUnloaded()) { - tile.content.state = Cesium3DTileContentState.LOADING; - var contentUrl = tile._header.content.url; - var tilesUrl = (new Uri(contentUrl).isAbsolute()) ? contentUrl : tiles3D._url + contentUrl; - loadTilesJson(tiles3D, tilesUrl, tile, function() { - tile.content.state = Cesium3DTileContentState.READY; - }); - } - } - function selectTile(selectedTiles, tile, fullyVisible, frameState) { // There may also be a tight box around just the tile's contents, e.g., for a city, we may be // zoomed into a neighborhood and can cull the skyscrapers in the root node. @@ -405,13 +368,7 @@ define([ } if (root.isContentUnloaded()) { - if (outOfCore) { - if (root.hasTilesetContent) { - selectTileWithTilesetContent(tiles3D, selectedTiles, root, fullyVisible, frameState, true); - } else { - requestContent(tiles3D, root); - } - } + requestContent(tiles3D, root, outOfCore); return; } @@ -440,28 +397,31 @@ define([ var childrenLength = children.length; var child; var k; - var additiveRefinment = (t.refine === Cesium3DTileRefine.ADD); + var additiveRefinement = (t.refine === Cesium3DTileRefine.ADD); + + if (t.hasTilesetContent) { + // If tile has tileset content, skip it and process its child instead (the tileset root) + // No need to check visibility or sse of the child because its bounding volume + // and geometric error are equal to its parent. + if (t.isReady()) { + child = t.children[0]; + if (child.isContentUnloaded()) { + requestContent(tiles3D, child, outOfCore); + } else { + stack.push(child); + } + } + continue; + } - if (additiveRefinment) { + if (additiveRefinement) { // With additive refinement, the tile is rendered // regardless of if its SSE is sufficient. - - if (!t.hasTilesetContent) { - selectTile(selectedTiles, t, fullyVisible, frameState); - } else { - // Check if the tile contains another tileset. If so: - // 1) If its children are not loaded, load the tileset it points to - // and concatenate with this tileset. - // 2) If its children are already loaded, select its (root) child - // since the geometric error of it is same as this tile's. - if (sse <= maximumScreenSpaceError) { - selectTileWithTilesetContent(tiles3D, selectedTiles, t, fullyVisible, frameState, additiveRefinment); - } - } + selectTile(selectedTiles, t, fullyVisible, frameState); // TODO: experiment with prefetching children if (sse > maximumScreenSpaceError) { - // Tile does not meet SSE. Refine to them in front-to-back order. + // Tile does not meet SSE. Refine them in front-to-back order. // Only sort and refine (render or request children) if any // children are loaded or request slots are available. @@ -484,8 +444,10 @@ define([ // Use parent's geometric error with child's box to see if we already meet the SSE if (getScreenSpaceError(t.geometricError, child, frameState) > maximumScreenSpaceError) { - if (child.isContentUnloaded() && (child.visibility(cullingVolume) !== CullingVolume.MASK_OUTSIDE) && outOfCore) { - requestContent(tiles3D, child); + if (child.isContentUnloaded()) { + if (child.visibility(cullingVolume) !== CullingVolume.MASK_OUTSIDE) { + requestContent(tiles3D, child, outOfCore); + } } else { stack.push(child); } @@ -502,15 +464,8 @@ define([ if ((sse <= maximumScreenSpaceError) || (childrenLength === 0)) { // This tile meets the SSE so add its commands. - // - // Select tile if it's a leaf (childrenLength === 0) and - // does not have tileset content. - // If the tile has tileset content, handle that tile separately. - if (!t.hasTilesetContent) { - selectTile(selectedTiles, t, fullyVisible, frameState); - } else { - selectTileWithTilesetContent(tiles3D, selectedTiles, t, fullyVisible, frameState, additiveRefinment); - } + // Select tile if it's a leaf (childrenLength === 0) + selectTile(selectedTiles, t, fullyVisible, frameState); } else { // Tile does not meet SSE. @@ -528,23 +483,10 @@ define([ // TODO: same TODO as above. } - if (!allChildrenLoaded) { + if (!t.isRefinable()) { // Tile does not meet SSE. Add its commands since it is the best we have and request its children. - if (!t.hasTilesetContent) { - selectTile(selectedTiles, t, fullyVisible, frameState); - } else { - selectTileWithTilesetContent(tiles3D, selectedTiles, t, fullyVisible, frameState, additiveRefinment); - } - - if (outOfCore) { - for (k = 0; (k < childrenLength) && requestScheduler.hasAvailableRequests(); ++k) { - child = children[k]; -// TODO: we could spin a bit less CPU here and probably above by keeping separate lists for unloaded/ready children. - if (child.isContentUnloaded()) { - requestContent(tiles3D, child); - } - } - } + selectTile(selectedTiles, t, fullyVisible, frameState); + makeRefinable(tiles3D, frameState, outOfCore, t); } else { // Tile does not meet SEE and its children are loaded. Refine to them in front-to-back order. for (k = 0; k < childrenLength; ++k) { @@ -559,6 +501,31 @@ define([ } } + var refineStack = []; + function makeRefinable(tiles3D, frameState, outOfCore, tile) { + var maximumScreenSpaceError = tiles3D.maximumScreenSpaceError; + var stack = refineStack; + stack.push(tile); + while (stack.length > 0) { + var t = stack.pop(); + var sse = getScreenSpaceError(t.geometricError, t, frameState); + if (sse > maximumScreenSpaceError) { + var children = t.children; + var childrenLength = children.length; + for (var k = 0; (k < childrenLength) && requestScheduler.hasAvailableRequests(); ++k) { +// TODO: we could spin a bit less CPU here and probably above by keeping separate lists for unloaded/ready children. + var child = children[k]; + if (child.isContentUnloaded()) { + requestContent(tiles3D, child, outOfCore); + } else if (!child.hasContent && !child.isRefinable()) { + // If the child is empty, we need to load all its descendants with content before it can refine + stack.push(child); + } + } + } + } + } + /////////////////////////////////////////////////////////////////////////// function addToProcessingQueue(tiles3D, tile) { diff --git a/Source/Scene/Tileset3DTileContentProvider.js b/Source/Scene/Tileset3DTileContentProvider.js index 8974a700dbf3..a30d0250318f 100644 --- a/Source/Scene/Tileset3DTileContentProvider.js +++ b/Source/Scene/Tileset3DTileContentProvider.js @@ -12,11 +12,41 @@ define([ /** * @private */ - var Tileset3DTileContentProvider = function() { + var Tileset3DTileContentProvider = function(tileset, tile, url) { + this._tileset = tileset; + this._tile = tile; + this._url = url; + + /** + * @readonly + */ this.state = Cesium3DTileContentState.UNLOADED; + + /** + * @type {Promise} + */ + this.processingPromise = when.defer(); + + /** + * @type {Promise} + */ + this.readyPromise = when.defer(); }; Tileset3DTileContentProvider.prototype.request = function() { + var that = this; + + this.state = Cesium3DTileContentState.LOADING; + + this._tileset.loadTilesJson(this._url, this._tile).then(function() { + that.state = Cesium3DTileContentState.PROCESSING; + that.processingPromise.resolve(that); + that.state = Cesium3DTileContentState.READY; + that.readyPromise.resolve(that); + }).otherwise(function(error) { + that.state = Cesium3DTileContentState.FAILED; + that.readyPromise.reject(error); + }); }; Tileset3DTileContentProvider.prototype.update = function(owner, frameState) {