Skip to content

Commit

Permalink
Merge pull request #1039 from CesiumGS/doc-tile-selection-algorithm
Browse files Browse the repository at this point in the history
Document tile selection algorithm
  • Loading branch information
azrogers authored Dec 17, 2024
2 parents ccb918a + 54f9d4b commit cc0d6c5
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 63 deletions.
125 changes: 66 additions & 59 deletions Cesium3DTilesSelection/src/Tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1014,37 +1014,38 @@ Tileset::TraversalDetails Tileset::_renderLeaf(
lastFrameSelectionState);
}

namespace {

/**
* We can render it if _any_ of the following are true:
* 1. We rendered it (or kicked it) last frame.
* 2. This tile was culled last frame, or it wasn't even visited because an
* ancestor was culled.
* 3. The tile is done loading and ready to render.
* Note that even if we decide to render a tile here, it may later get "kicked"
* in favor of an ancestor.
* @brief Determines if we must refine this tile so that we can continue
* rendering the deeper descendant tiles of this tile.
*
* If this tile was refined last frame, and is not yet renderable, then we
* should REFINE past this tile in order to continue rendering the deeper tiles
* that we rendered last frame, until such time as this tile is loaded and we
* can render it instead. This is necessary to avoid detail vanishing when
* the camera zooms out and lower-detail tiles are not yet loaded.
*
* @param tile The tile to check, which is assumed to meet the SSE for
* rendering.
* @param lastFrameSelectionState The selection state of this tile last frame.
* @param lastFrameNumber The previous frame number.
* @return True if this tile must be refined instead of rendered, so that we can
* continue rendering deeper tiles.
*/
static bool shouldRenderThisTile(
bool mustContinueRefiningToDeeperTiles(
const Tile& tile,
const TileSelectionState& lastFrameSelectionState,
int32_t lastFrameNumber) noexcept {
const TileSelectionState::Result originalResult =
lastFrameSelectionState.getOriginalResult(lastFrameNumber);
if (originalResult == TileSelectionState::Result::Rendered) {
return true;
}
if (originalResult == TileSelectionState::Result::Culled ||
originalResult == TileSelectionState::Result::None) {
return true;
}

// Tile::isRenderable is actually a pretty complex operation, so only do
// it when absolutely necessary
if (tile.isRenderable()) {
return true;
}
return false;
return originalResult == TileSelectionState::Result::Refined &&
!tile.isRenderable();
}

} // namespace

Tileset::TraversalDetails Tileset::_renderInnerTile(
const FrameState& frameState,
Tile& tile,
Expand Down Expand Up @@ -1270,6 +1271,12 @@ Tileset::_checkOcclusion(const Tile& tile, const FrameState& frameState) {
return TileOcclusionState::NotOccluded;
}

namespace {

enum class VisitTileAction { Render, Refine };

}

// Visits a tile for possible rendering. When we call this function with a tile:
// * The tile has previously been determined to be visible.
// * Its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true,
Expand All @@ -1295,7 +1302,14 @@ Tileset::TraversalDetails Tileset::_visitTile(

const bool unconditionallyRefine = tile.getUnconditionallyRefine();

bool wantToRefine = unconditionallyRefine || (!meetsSse && !ancestorMeetsSse);
// Determine whether to REFINE or RENDER. Note that even if this tile is
// initially marked for RENDER here, it may later switch to REFINE as a
// result of `mustContinueRefiningToDeeperTiles`.
VisitTileAction action = VisitTileAction::Render;
if (unconditionallyRefine)
action = VisitTileAction::Refine;
else if (!meetsSse && !ancestorMeetsSse)
action = VisitTileAction::Refine;

const TileSelectionState lastFrameSelectionState =
tile.getLastSelectionState();
Expand Down Expand Up @@ -1324,14 +1338,15 @@ Tileset::TraversalDetails Tileset::_visitTile(
// If this tile and a child were both refined last frame, this tile does not
// need occlusion results.
bool shouldCheckOcclusion = this->_options.enableOcclusionCulling &&
wantToRefine && !unconditionallyRefine &&
action == VisitTileAction::Refine &&
!unconditionallyRefine &&
(!tileLastRefined || !childLastRefined);

if (shouldCheckOcclusion) {
TileOcclusionState occlusion = this->_checkOcclusion(tile, frameState);
if (occlusion == TileOcclusionState::Occluded) {
++result.tilesOccluded;
wantToRefine = false;
action = VisitTileAction::Render;
meetsSse = true;
} else if (
occlusion == TileOcclusionState::OcclusionUnavailable &&
Expand All @@ -1340,55 +1355,47 @@ Tileset::TraversalDetails Tileset::_visitTile(
frameState.lastFrameNumber) !=
TileSelectionState::Result::Refined) {
++result.tilesWaitingForOcclusionResults;
wantToRefine = false;
action = VisitTileAction::Render;
meetsSse = true;
}
}

bool queuedForLoad = false;

if (!wantToRefine) {
// This tile (or an ancestor) is the one we want to render this frame, but
// we'll do different things depending on the state of this tile and on what
// we did _last_ frame.

// We can render it if _any_ of the following are true:
// 1. We rendered it (or kicked it) last frame.
// 2. This tile was culled last frame, or it wasn't even visited because an
// ancestor was culled.
// 3. The tile is done loading and ready to render.
//
// Note that even if we decide to render a tile here, it may later get
// "kicked" in favor of an ancestor.
const bool renderThisTile = shouldRenderThisTile(
if (action == VisitTileAction::Render) {
// This tile meets the screen-space error requirement, so we'd like to
// render it, if we can.
bool mustRefine = mustContinueRefiningToDeeperTiles(
tile,
lastFrameSelectionState,
frameState.lastFrameNumber);
if (renderThisTile) {
if (mustRefine) {
// // We must refine even though this tile meets the SSE.
action = VisitTileAction::Refine;

// Loading this tile is very important, because a number of deeper,
// higher-detail tiles are being rendered in its stead, so we want to load
// it with high priority. However, if `ancestorMeetsSse` is set, then our
// parent tile is in the exact same situation, and loading this tile with
// high priority would compete with that one. We should prefer the parent
// because it is closest to the actual desired LOD and because up the tree
// there can only be fewer tiles that need loading.
if (!ancestorMeetsSse) {
addTileToLoadQueue(tile, TileLoadPriorityGroup::Urgent, tilePriority);
queuedForLoad = true;
}

// Fall through to REFINE, but mark this tile as already meeting the
// required SSE.
ancestorMeetsSse = true;
} else {
// Render this tile and return without visiting children.
// Only load this tile if it (not just an ancestor) meets the SSE.
if (meetsSse && !ancestorMeetsSse) {
if (!ancestorMeetsSse) {
addTileToLoadQueue(tile, TileLoadPriorityGroup::Normal, tilePriority);
}
return _renderInnerTile(frameState, tile, result);
}

// Otherwise, we can't render this tile (or blank space where it would be)
// because doing so would cause detail to disappear that was visible last
// frame. Instead, keep rendering any still-visible descendants that were
// rendered last frame and render nothing for newly-visible descendants.
// E.g. if we were rendering level 15 last frame but this frame we want
// level 14 and the closest renderable level <= 14 is 0, rendering level
// zero would be pretty jarring so instead we keep rendering level 15 even
// though its SSE is better than required. So fall through to continue
// traversal...
ancestorMeetsSse = true;

// Load this blocker tile with high priority, but only if this tile (not
// just an ancestor) meets the SSE.
if (meetsSse) {
addTileToLoadQueue(tile, TileLoadPriorityGroup::Urgent, tilePriority);
queuedForLoad = true;
}
}

// Refine!
Expand Down
5 changes: 4 additions & 1 deletion doc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ if(DOXYGEN_FOUND)
set(DOXYGEN_BUILTIN_STL_SUPPORT YES)
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "../README.md")
set(DOXYGEN_EXCLUDE_PATTERNS "*/node_modules/*")
set(DOXYGEN_IMAGE_PATH "${CMAKE_CURRENT_LIST_DIR}/img")

list(APPEND DOXYGEN_IMAGE_PATH "${CMAKE_CURRENT_LIST_DIR}/")
list(APPEND DOXYGEN_IMAGE_PATH "${CMAKE_CURRENT_LIST_DIR}/img")

set(DOXYGEN_MARKDOWN_ID_STYLE GITHUB)
set(DOXYGEN_INTERACTIVE_SVG YES)
# Tag files can be used by other Doxygen projects to link to our docs
Expand Down
21 changes: 21 additions & 0 deletions doc/diagrams/tileset-culling-flowchart.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
flowchart LR
Start@{ shape: sm-circ, label: "Start" }
ITileExcluder{Excluded by any ITileExcluder?}
CullAgainstViews[Cull Against Views]
FrustumCull{Is Inside View Frustum or Frustum Culling Disabled?}
FogCull{Is Visible in Fog or Fog Culling Disabled?}
MoreViews{More Views?}
Visit[Visit]
NoVisit[Do Not Visit]

Start-->ITileExcluder
ITileExcluder-->|Yes|NoVisit
ITileExcluder-->|No|CullAgainstViews
CullAgainstViews-->|First View|FrustumCull

FrustumCull-->|Yes|FogCull
FrustumCull-->|No|MoreViews
MoreViews-->|Yes - Next View|FrustumCull
MoreViews-->|No|NoVisit
FogCull-->|No|MoreViews
FogCull-->|Yes|Visit
24 changes: 24 additions & 0 deletions doc/diagrams/tileset-traversal-flowchart.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
flowchart LR
Start@{ shape: sm-circ, label: "Start" }
VisitTileIfNeeded[_visitTileIfNeeded]
ShouldVisit{Should Visit?}
VisitTile[_visitTile]
MeetsSSE{Meets SSE?}
Done
Render
Refine
VisitVisibleChildrenNearToFar[_visitVisibleChildrenNearToFar]
Recurse[Recurse on Children]

Start-->|Root Tile|VisitTileIfNeeded
VisitTileIfNeeded-->ShouldVisit
ShouldVisit-->|Yes|VisitTile
ShouldVisit-->|No|Done
VisitTile-->MeetsSSE
MeetsSSE-->|Yes|Render
MeetsSSE-->|No|Refine
Refine-->VisitVisibleChildrenNearToFar
VisitVisibleChildrenNearToFar-->Recurse
Recurse-->|Child Tile 1|VisitTileIfNeeded
Recurse-->|Child Tile ...|VisitTileIfNeeded
Recurse-->|Child Tile n|VisitTileIfNeeded
Binary file added doc/img/with-ancestor-meets-sse.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/img/without-ancestor-meets-sse.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion doc/topics/rendering-3d-tiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ In order to understand the pieces that you will need to implement in order to co

In Frame 1, your application calls [updateView](@ref Cesium3DTilesSelection::Tileset::updateView) on the `Tileset`. It passes in all of the [ViewStates](@ref Cesium3DTilesSelection::ViewState) from which the tileset is currently being viewed. A `ViewState` includes a position, a look direction, a camera "up" direction, a viewport width and height in pixels, and horizontal and vertical field-of-view angles. This is all the information that Cesium Native needs in order to decide which subset of the model is visible, and what level-of-detail is needed for each part. You'll likely create a `ViewState` from each camera in your scene.

In our example, based on the `ViewStates`, Cesium Native selects tiles A and B as being needed for rendering. The details of this process are described in [The 3D Tiles Selection Algorithm](@ref selection-algorithm-details), but aren't important for now. In Frame 1, no tiles are loaded yet, so `Tileset` calls [IAssetAccessor::get](@ref CesiumAsync::IAssetAccessor::get) to initiate the download of these two tiles. These downloads happen asynchronously via the [AsyncSystem](#async-system); Cesium Native doesn't wait for them to complete before continuing.
In our example, based on the `ViewStates`, Cesium Native selects tiles A and B as being needed for rendering. The details of this process are described in the [3D Tiles Selection Algorithm](@ref selection-algorithm-details), but aren't important for now. In Frame 1, no tiles are loaded yet, so `Tileset` calls [IAssetAccessor::get](@ref CesiumAsync::IAssetAccessor::get) to initiate the download of these two tiles. These downloads happen asynchronously via the [AsyncSystem](#async-system); Cesium Native doesn't wait for them to complete before continuing.

@mermaid{tileset-sequence-diagram-frame2}

Expand Down
Loading

0 comments on commit cc0d6c5

Please sign in to comment.