Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix replacement refinement with empty tiles #3456

Merged
merged 10 commits into from
Jan 26, 2016
48 changes: 19 additions & 29 deletions Source/Scene/Cesium3DTile.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@ define([
*/
this.children = [];

/**
* Descendant tiles that need to be visible before this tile can refine. For example, if
* a child is empty (such as for accelerating culling), its descendants with content would
* be added here. This array is generated during runtime in {@link Cesium3DTileset#loadTileset}.
* If a tiles's children all have content, this is left undefined.
*
* @type {Array}
* @readonly
*/
this.descendantsWithContent = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps mention in the comment that this is computed at runtime, not loaded from tileset.json.

Also, as we discussed before, but now that you have more context, would it be useful for tileset.json to have any metadata for this, e.g., number of descendants with content?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any metadata would help right now, unless it could link to those descendants directly, which doesn't seem possible.


/**
* DOC_TBA
*
Expand All @@ -135,10 +146,6 @@ define([
*/
this.numberOfChildrenWithoutContent = defined(header.children) ? header.children.length : 0;

this._numberOfUnrefinableChildren = this.numberOfChildrenWithoutContent;

this.refining = false;

this.hasContent = true;

/**
Expand Down Expand Up @@ -168,7 +175,6 @@ define([
if (type === 'json') {
this.hasTilesetContent = true;
this.hasContent = false;
this._numberOfUnrefinableChildren = 1;
}

//>>includeStart('debug', pragmas.debug);
Expand All @@ -185,20 +191,6 @@ define([
this._content = content;
this._requestServer = requestServer;

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
Expand All @@ -207,15 +199,13 @@ define([
--that.parent.numberOfChildrenWithoutContent;
}

setRefinable(that);

that.readyPromise.resolve(that);
}).otherwise(function(error) {
that.readyPromise.reject(error);
//TODO: that.parent.numberOfChildrenWithoutContent will never reach zero and therefore that.parent will never refine
});

// Members that are updated every frame for rendering optimizations:
// Members that are updated every frame for tree traversal and rendering optimizations:

/**
* @private
Expand All @@ -230,6 +220,13 @@ define([
*/
this.parentPlaneMask = 0;

/**
* Marks if the tile is selected this frame.
*
* @type {Boolean}
*/
this.selected = false;

this._debugBoundingVolume = undefined;
this._debugContentBoundingVolume = undefined;
}
Expand Down Expand Up @@ -290,13 +287,6 @@ define([
return this._content.state === Cesium3DTileContentState.READY;
};

/**
* DOC_TBA
*/
Cesium3DTile.prototype.isRefinable = function() {
return this._numberOfUnrefinableChildren === 0;
};

/**
* DOC_TBA
*/
Expand Down
127 changes: 99 additions & 28 deletions Source/Scene/Cesium3DTileset.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ define([
++parentTile.numberOfChildrenWithoutContent;
}

var refiningTiles = [];

var stack = [];
stack.push({
header : tilesetJson.root,
Expand All @@ -311,29 +313,63 @@ define([

while (stack.length > 0) {
var t = stack.pop();
var tile3D = t.cesium3DTile;
var children = t.header.children;
var hasEmptyChild = false;
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);

var childTile = new Cesium3DTile(tileset, baseUrl, childHeader, tile3D);
tile3D.children.push(childTile);
stack.push({
header : childHeader,
cesium3DTile : childTile
});
if (!childTile.hasContent) {
hasEmptyChild = true;
}
}
}
if (tile3D.hasContent && hasEmptyChild && (tile3D.refine === Cesium3DTileRefine.REPLACE)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to have a comment here explaining why all this is being checked.

// Tiles that use replacement refinement and have empty child tiles need to keep track of
// descendants with content in order to refine correctly.
refiningTiles.push(tile3D);
}
}

prepareRefiningTiles(refiningTiles);

return {
tilesetJson : tilesetJson,
root : rootTile
};
});
};

function prepareRefiningTiles(refiningTiles) {
var stack = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to generalize the tree traversal, and then pass a function to it here and in destroy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not yet. While some code is duplicated, I think its easier to understand this way.

var length = refiningTiles.length;
for (var i = 0; i < length; ++i) {
var refiningTile = refiningTiles[i];
refiningTile.descendantsWithContent = [];
stack.push(refiningTile);
while (stack.length > 0) {
var tile = stack.pop();
var children = tile.children;
var childrenLength = children.length;
for (var k = 0; k < childrenLength; ++k) {
var childTile = children[k];
if (childTile.hasContent) {
refiningTile.descendantsWithContent.push(childTile);
} else {
stack.push(childTile);
}
}
}
}
}

function getScreenSpaceError(geometricError, tile, frameState) {
// TODO: screenSpaceError2D like QuadtreePrimitive.js
if (geometricError === 0.0) {
Expand Down Expand Up @@ -389,15 +425,14 @@ define([
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.
//
// Don't select if the tile is being loaded to refine another tile
if (tile.isReady() && !tile.refining &&
(fullyVisible || (tile.contentsVisibility(frameState.cullingVolume) !== Intersect.OUTSIDE))) {
if (tile.isReady() && (fullyVisible || (tile.contentsVisibility(frameState.cullingVolume) !== Intersect.OUTSIDE))) {
selectedTiles.push(tile);
tile.selected = true;
}
}

var scratchStack = [];
var scratchRefiningTiles = [];

function selectTiles(tiles3D, frameState, outOfCore) {
if (tiles3D.debugFreezeFrame) {
Expand All @@ -410,6 +445,8 @@ define([
var selectedTiles = tiles3D._selectedTiles;
selectedTiles.length = 0;

scratchRefiningTiles.length = 0;

var root = tiles3D._root;
root.distanceToCamera = root.distanceToTile(frameState);
root.parentPlaneMask = CullingVolume.MASK_INDETERMINATE;
Expand All @@ -431,6 +468,7 @@ define([
while (stack.length > 0) {
// Depth first. We want the high detail tiles first.
var t = stack.pop();
t.selected = false;
++stats.visited;

var planeMask = t.visibility(cullingVolume);
Expand Down Expand Up @@ -459,7 +497,6 @@ define([
child = t.children[0];
child.parentPlaneMask = t.parentPlaneMask;
child.distanceToCamera = t.distanceToCamera;
child.refining = t.refining;
if (child.isContentUnloaded()) {
requestContent(tiles3D, child, outOfCore);
} else {
Expand All @@ -468,6 +505,7 @@ define([
}
continue;
}

if (additiveRefinement) {
// With additive refinement, the tile is rendered
// regardless of if its SSE is sufficient.
Expand All @@ -493,8 +531,6 @@ define([
// to replacement refinement where we need all children.
for (k = 0; k < childrenLength; ++k) {
child = children[k];
// Pass along whether the child is being loaded for refinement
child.refining = t.refining;
// Store the plane mask so that the child can optimize based on its parent's returned mask
child.parentPlaneMask = planeMask;

Expand Down Expand Up @@ -529,6 +565,7 @@ define([
// or slots are available to request them. If we are just rendering the
// tile (and can't make child requests because no slots are available)
// then the children do not need to be sorted.

var allChildrenLoaded = t.numberOfChildrenWithoutContent === 0;
if (allChildrenLoaded || t.canRequestContent()) {
// Distance is used for sorting now and for computing SSE when the tile comes off the stack.
Expand All @@ -539,37 +576,69 @@ define([
// TODO: same TODO as above.
}

if (!t.isRefinable() || t.refining) {
// Tile does not meet SSE. Add its commands since it is the best we have until it becomes refinable.
// If all its children have content, it will became refinable once they are loaded.
// If a child is empty (such as for accelerating culling), its descendants with content must be loaded first.
if (!allChildrenLoaded) {
// Tile does not meet SSE. Add its commands since it is the best we have and request its children.
selectTile(selectedTiles, t, fullyVisible, frameState);

for (k = 0; k < childrenLength; ++k) {
child = children[k];
if (child.isContentUnloaded()) {
requestContent(tiles3D, child, outOfCore);
} else if (!child.hasContent && !child.isRefinable()){
// If the child is empty, start loading its descendants. Mark as refining so they aren't selected.
child.refining = true;
// Store the plane mask so that the child can optimize based on its parent's returned mask
child.parentPlaneMask = planeMask;
stack.push(child);
if (outOfCore) {
for (k = 0; (k < childrenLength) && t.canRequestContent(); ++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, outOfCore);
}
}
}
} else {
// Tile does not meet SEE and it is refinable. Refine to children in front-to-back order.
// Tile does not meet SEE and its children are loaded. Refine to them in front-to-back order.
for (k = 0; k < childrenLength; ++k) {
child = children[k];
child.refining = false;
// Store the plane mask so that the child can optimize based on its parent's returned mask
child.parentPlaneMask = planeMask;
stack.push(child);
}

if (defined(t.descendantsWithContent)) {
scratchRefiningTiles.push(t);
}
}
}
}
}

checkRefiningTiles(scratchRefiningTiles, tiles3D, frameState);
}

function checkRefiningTiles(refiningTiles, tiles3D, frameState) {
// In the common case, a tile that uses replacement refinement is refinable once all its
// children are loaded. However if it has an empty child, refining to its children would
// show a visible gap. In this case, the empty child's children (or further descendants)
// would need to be selected before the original tile is refinable. It is hard to determine
// this easily during the traversal, so this fixes the situation retroactively.
var descendant;
var refiningTilesLength = refiningTiles.length;
for (var i = 0; i < refiningTilesLength; ++i) {
var j;
var refinable = true;
var refiningTile = refiningTiles[i];
var descendantsLength = refiningTile.descendantsWithContent.length;
for (j = 0; j < descendantsLength; ++j) {
descendant = refiningTile.descendantsWithContent[j];
if (!descendant.selected) {
// TODO: also check that its visible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to do this TODO now or later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since a lot of the work is in #3449 this is just a reminder for that PR.

refinable = false;
break;
}
}
if (!refinable) {
var fullyVisible = refiningTile.visibility(frameState.cullingVolume) === CullingVolume.MASK_INSIDE;
selectTile(tiles3D._selectedTiles, refiningTile, fullyVisible, frameState);
for (j = 0; j < descendantsLength; ++j) {
descendant = refiningTile.descendantsWithContent[j];
descendant.selected = false;
}
}
}
}

///////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -663,8 +732,10 @@ define([
var tileVisible = tiles3D.tileVisible;
for (var i = 0; i < length; ++i) {
var tile = selectedTiles[i];
tileVisible.raiseEvent(tile);
tile.update(tiles3D, frameState);
if (tile.selected) {
tileVisible.raiseEvent(tile);
tile.update(tiles3D, frameState);
}
}

tiles3D._statistics.numberOfCommands = (commandList.length - numberOfCommands);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
},
"geometricError": 70,
"refine": "replace",
"content": {
"url": "parent.b3dm"
},
"children": [
{
"boundingVolume": {
Expand Down
Loading