cesium-native/Cesium3DTilesSelection/test/TestTilesetSelectionAlgorit...

941 lines
35 KiB
C++

#include "Cesium3DTilesSelection/Tileset.h"
#include "Cesium3DTilesSelection/ViewState.h"
#include "Cesium3DTilesSelection/registerAllTileContentTypes.h"
#include "CesiumGeospatial/Ellipsoid.h"
#include "CesiumUtility/Math.h"
#include "SimpleAssetAccessor.h"
#include "SimpleAssetRequest.h"
#include "SimpleAssetResponse.h"
#include "SimplePrepareRendererResource.h"
#include "SimpleTaskProcessor.h"
#include <algorithm>
#include <catch2/catch.hpp>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <glm/mat4x4.hpp>
using namespace CesiumAsync;
using namespace Cesium3DTilesSelection;
using namespace CesiumGeospatial;
using namespace CesiumUtility;
static std::vector<std::byte> readFile(const std::filesystem::path& fileName) {
std::ifstream file(fileName, std::ios::binary | std::ios::ate);
REQUIRE(file);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<std::byte> buffer(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(buffer.data()), size);
return buffer;
}
static bool doesTileMeetSSE(
const ViewState& viewState,
const Tile& tile,
const Tileset& tileset) {
double distance = glm::sqrt(viewState.computeDistanceSquaredToBoundingVolume(
tile.getBoundingVolume()));
double sse =
viewState.computeScreenSpaceError(tile.getGeometricError(), distance);
return sse < tileset.getOptions().maximumScreenSpaceError;
}
static void initializeTileset(Tileset& tileset) {
// create a random view state so that we can able to load the tileset first
const Ellipsoid& ellipsoid = Ellipsoid::WGS84;
Cartographic viewPositionCartographic{
Math::degreesToRadians(118.0),
Math::degreesToRadians(32.0),
200.0};
Cartographic viewFocusCartographic{
viewPositionCartographic.longitude + Math::degreesToRadians(0.5),
viewPositionCartographic.latitude + Math::degreesToRadians(0.5),
0.0};
glm::dvec3 viewPosition =
ellipsoid.cartographicToCartesian(viewPositionCartographic);
glm::dvec3 viewFocus =
ellipsoid.cartographicToCartesian(viewFocusCartographic);
glm::dvec3 viewUp{0.0, 0.0, 1.0};
glm::dvec2 viewPortSize{500.0, 500.0};
double aspectRatio = viewPortSize.x / viewPortSize.y;
double horizontalFieldOfView = Math::degreesToRadians(60.0);
double verticalFieldOfView =
std::atan(std::tan(horizontalFieldOfView * 0.5) / aspectRatio) * 2.0;
ViewState viewState = ViewState::create(
viewPosition,
glm::normalize(viewFocus - viewPosition),
viewUp,
viewPortSize,
horizontalFieldOfView,
verticalFieldOfView);
tileset.updateView({viewState});
}
static ViewState zoomToTile(const Tile& tile) {
const BoundingRegion* region =
std::get_if<BoundingRegion>(&tile.getBoundingVolume());
REQUIRE(region != nullptr);
const GlobeRectangle& rectangle = region->getRectangle();
double maxHeight = region->getMaximumHeight();
Cartographic center = rectangle.computeCenter();
Cartographic corner = rectangle.getNorthwest();
corner.height = maxHeight;
const Ellipsoid& ellipsoid = Ellipsoid::WGS84;
glm::dvec3 viewPosition = ellipsoid.cartographicToCartesian(corner);
glm::dvec3 viewFocus = ellipsoid.cartographicToCartesian(center);
glm::dvec3 viewUp{0.0, 0.0, 1.0};
glm::dvec2 viewPortSize{500.0, 500.0};
double aspectRatio = viewPortSize.x / viewPortSize.y;
double horizontalFieldOfView = Math::degreesToRadians(60.0);
double verticalFieldOfView =
std::atan(std::tan(horizontalFieldOfView * 0.5) / aspectRatio) * 2.0;
return ViewState::create(
viewPosition,
glm::normalize(viewFocus - viewPosition),
viewUp,
viewPortSize,
horizontalFieldOfView,
verticalFieldOfView);
}
static ViewState zoomToTileset(const Tileset& tileset) {
const Tile* root = tileset.getRootTile();
REQUIRE(root != nullptr);
return zoomToTile(*root);
}
TEST_CASE("Test replace refinement for render") {
Cesium3DTilesSelection::registerAllTileContentTypes();
// initialize REPLACE tileset
//
// parent.b3dm
//
// ll.b3dm lr.b3dm ul.b3dm ur.b3dm
//
// ll_ll.b3dm
//
std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR;
testDataPath = testDataPath / "ReplaceTileset";
std::vector<std::string> files{
"tileset.json",
"parent.b3dm",
"ll.b3dm",
"lr.b3dm",
"ul.b3dm",
"ur.b3dm",
"ll_ll.b3dm",
};
std::map<std::string, std::shared_ptr<SimpleAssetRequest>>
mockCompletedRequests;
for (const auto& file : files) {
std::unique_ptr<SimpleAssetResponse> mockCompletedResponse =
std::make_unique<SimpleAssetResponse>(
static_cast<uint16_t>(200),
"doesn't matter",
CesiumAsync::HttpHeaders{},
readFile(testDataPath / file));
mockCompletedRequests.insert(
{file,
std::make_shared<SimpleAssetRequest>(
"GET",
file,
CesiumAsync::HttpHeaders{},
std::move(mockCompletedResponse))});
}
std::shared_ptr<SimpleAssetAccessor> mockAssetAccessor =
std::make_shared<SimpleAssetAccessor>(std::move(mockCompletedRequests));
TilesetExternals tilesetExternals{
mockAssetAccessor,
std::make_shared<SimplePrepareRendererResource>(),
AsyncSystem(std::make_shared<SimpleTaskProcessor>()),
nullptr};
// create tileset and call updateView() to give it a chance to load
Tileset tileset(tilesetExternals, "tileset.json");
initializeTileset(tileset);
// check the tiles status
const Tile* root = tileset.getRootTile();
REQUIRE(root->getState() == Tile::LoadState::ContentLoading);
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Unloaded);
}
SECTION("No refinement happen when tile meet SSE") {
// Zoom to tileset. Expect the root will not meet sse in this configure
ViewState viewState = zoomToTileset(tileset);
// Zoom out from the tileset a little bit to make sure the root meet sse
glm::dvec3 zoomOutPosition =
viewState.getPosition() - viewState.getDirection() * 2500.0;
ViewState zoomOutViewState = ViewState::create(
zoomOutPosition,
viewState.getDirection(),
viewState.getUp(),
viewState.getViewportSize(),
viewState.getHorizontalFieldOfView(),
viewState.getVerticalFieldOfView());
// Check 1st and 2nd frame. Root should meet sse and render. No transitions
// are expected here
for (int frame = 0; frame < 2; ++frame) {
ViewUpdateResult result = tileset.updateView({zoomOutViewState});
// Check tile state. Ensure root meet sse
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(doesTileMeetSSE(zoomOutViewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Unloaded);
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 1);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
// check children state are still unloaded
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Unloaded);
}
}
}
SECTION("Root doesn't meet sse but has to be rendered because children "
"cannot be rendered") {
// we should forbid hole first to let the checks below happen
tileset.getOptions().forbidHoles = true;
SECTION("Children cannot be rendered because of no response") {
// remove one of children completed response to mock network error
mockAssetAccessor->mockCompletedRequests["ll.b3dm"]->pResponse = nullptr;
mockAssetAccessor->mockCompletedRequests["lr.b3dm"]->pResponse = nullptr;
mockAssetAccessor->mockCompletedRequests["ul.b3dm"]->pResponse = nullptr;
mockAssetAccessor->mockCompletedRequests["ur.b3dm"]->pResponse = nullptr;
}
SECTION("Children cannot be rendered because response has an failed status "
"code") {
// remove one of children completed response to mock network error
mockAssetAccessor->mockCompletedRequests["ll.b3dm"]
->pResponse->mockStatusCode = 404;
mockAssetAccessor->mockCompletedRequests["lr.b3dm"]
->pResponse->mockStatusCode = 404;
mockAssetAccessor->mockCompletedRequests["ul.b3dm"]
->pResponse->mockStatusCode = 404;
mockAssetAccessor->mockCompletedRequests["ur.b3dm"]
->pResponse->mockStatusCode = 404;
}
ViewState viewState = zoomToTileset(tileset);
// 1st frame. Root doesn't meet sse, so it goes to children. But because
// children haven't started loading, root should be rendered.
{
ViewUpdateResult result = tileset.updateView({viewState});
// Check tile state. Ensure root doesn't meet sse, but children does.
// Children begin loading as well
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::ContentLoading);
REQUIRE(doesTileMeetSSE(viewState, child, tileset));
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 1);
REQUIRE(result.tilesLoadingMediumPriority == 4);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 2nd frame. Because children receive failed response, so they can't be
// rendered. Root should be rendered instead. Children should have failed
// load states
{
ViewUpdateResult result = tileset.updateView({viewState});
// Check tile state. Ensure root doesn't meet sse, but children does
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::FailedTemporarily);
REQUIRE(doesTileMeetSSE(viewState, child, tileset));
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 1);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
}
SECTION("Parent meets sse but not renderable") {
// Zoom to tileset. Expect the root will not meet sse in this configure
ViewState viewState = zoomToTileset(tileset);
glm::dvec3 zoomInPosition =
viewState.getPosition() + viewState.getDirection() * 200.0;
ViewState zoomInViewState = ViewState::create(
zoomInPosition,
viewState.getDirection(),
viewState.getUp(),
viewState.getViewportSize(),
viewState.getHorizontalFieldOfView(),
viewState.getVerticalFieldOfView());
// remove the ll.b3dm (one of the root's children) request to replicate
// network failure
mockAssetAccessor->mockCompletedRequests["ll.b3dm"]->pResponse = nullptr;
// 1st frame. Root doesn't meet sse, but none of the children finish
// loading. So we will render root
{
ViewUpdateResult result = tileset.updateView({zoomInViewState});
// check tiles status. All the children should have loading status
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(zoomInViewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::ContentLoading);
}
const Tile& ll = root->getChildren().front();
REQUIRE(!doesTileMeetSSE(zoomInViewState, ll, tileset));
const Tile& ll_ll = ll.getChildren().front();
REQUIRE(ll_ll.getState() == Tile::LoadState::ContentLoading);
REQUIRE(doesTileMeetSSE(zoomInViewState, ll_ll, tileset));
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 6);
REQUIRE(result.tilesLoadingLowPriority == 1);
REQUIRE(result.tilesLoadingMediumPriority == 4);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 2nd frame. All the children finish loading, so they are ready to be
// rendered (except ll.b3dm tile since it doesn't meet sse)
{
ViewUpdateResult result = tileset.updateView({zoomInViewState});
// check tiles status. All the children should have loading status
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(zoomInViewState, *root, tileset));
// the first child of root isn't rendered because it doesn't meet sse. It
// will be refined to its child which is ready to be rendered
const Tile& ll = root->getChildren().front();
REQUIRE(ll.getState() == Tile::LoadState::Failed);
REQUIRE(!doesTileMeetSSE(zoomInViewState, ll, tileset));
const Tile& ll_ll = ll.getChildren().front();
REQUIRE(ll_ll.getState() == Tile::LoadState::Done);
REQUIRE(doesTileMeetSSE(zoomInViewState, ll_ll, tileset));
// the rest of the root's children are ready to be rendered since they all
// meet sse
for (size_t i = 1; i < root->getChildren().size(); ++i) {
const Tile& child = root->getChildren()[i];
REQUIRE(child.getState() == Tile::LoadState::Done);
REQUIRE(doesTileMeetSSE(zoomInViewState, child, tileset));
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 4);
REQUIRE(result.tilesToRenderThisFrame[0] == &ll_ll);
REQUIRE(result.tilesToRenderThisFrame[1] == &root->getChildren()[1]);
REQUIRE(result.tilesToRenderThisFrame[2] == &root->getChildren()[2]);
REQUIRE(result.tilesToRenderThisFrame[3] == &root->getChildren()[3]);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 1);
REQUIRE(result.tilesToNoLongerRenderThisFrame.front() == root);
REQUIRE(result.tilesVisited == 6);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 3d frame. Zoom out the camera so that ll.b3dm tile will meet sse.
// However, since its content is failed to load and in the last frame it is
// refined, its child will be rendered instead to prevent loss of detail
{
glm::dvec3 zoomOutPosition =
viewState.getPosition() - viewState.getDirection() * 100.0;
ViewState zoomOutViewState = ViewState::create(
zoomOutPosition,
viewState.getDirection(),
viewState.getUp(),
viewState.getViewportSize(),
viewState.getHorizontalFieldOfView(),
viewState.getVerticalFieldOfView());
ViewUpdateResult result = tileset.updateView({zoomOutViewState});
// check tiles status. All the children should have loading status
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(zoomOutViewState, *root, tileset));
// The first child of root isn't rendered because it is failed to load
// even though it meets sse. Use its child instead since the child is
// rendered last frame
const Tile& ll = root->getChildren().front();
REQUIRE(ll.getState() == Tile::LoadState::Failed);
REQUIRE(doesTileMeetSSE(zoomOutViewState, ll, tileset));
const Tile& ll_ll = ll.getChildren().front();
REQUIRE(ll_ll.getState() == Tile::LoadState::Done);
// the rest of the root's children are ready to be rendered since they all
// meet sse
for (size_t i = 1; i < root->getChildren().size(); ++i) {
const Tile& child = root->getChildren()[i];
REQUIRE(child.getState() == Tile::LoadState::Done);
REQUIRE(doesTileMeetSSE(zoomOutViewState, child, tileset));
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 4);
REQUIRE(result.tilesToRenderThisFrame[0] == &ll_ll);
REQUIRE(result.tilesToRenderThisFrame[1] == &root->getChildren()[1]);
REQUIRE(result.tilesToRenderThisFrame[2] == &root->getChildren()[2]);
REQUIRE(result.tilesToRenderThisFrame[3] == &root->getChildren()[3]);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 6);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
}
SECTION("Child should be chosen when parent doesn't meet SSE") {
ViewState viewState = zoomToTileset(tileset);
// 1st frame. Root doesn't meet sse and children does. However, because
// none of the children are loaded, root will be rendered instead and
// children transition from unloaded to loading in the mean time
{
ViewUpdateResult result = tileset.updateView({viewState});
// Check tile state. Ensure root doesn't meet sse, but children does
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::ContentLoading);
REQUIRE(doesTileMeetSSE(viewState, child, tileset));
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 5);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 4);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 2nd frame. Children are finished loading and ready to be rendered. Root
// shouldn't be rendered in this frame
{
ViewUpdateResult result = tileset.updateView({viewState});
// check tile states
REQUIRE(root->getState() == Tile::LoadState::Done);
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Done);
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 4);
for (const auto& child : root->getChildren()) {
REQUIRE(
std::find(
result.tilesToRenderThisFrame.begin(),
result.tilesToRenderThisFrame.end(),
&child) != result.tilesToRenderThisFrame.end());
}
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 1);
REQUIRE(result.tilesToNoLongerRenderThisFrame.front() == root);
REQUIRE(result.tilesVisited == 5);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
}
}
TEST_CASE("Test additive refinement") {
Cesium3DTilesSelection::registerAllTileContentTypes();
std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR;
testDataPath = testDataPath / "AddTileset";
std::vector<std::string> files{
"tileset.json",
"tileset2.json",
"parent.b3dm",
"lr.b3dm",
"ul.b3dm",
"ur.b3dm",
"tileset3/tileset3.json",
"tileset3/ll.b3dm"};
std::map<std::string, std::shared_ptr<SimpleAssetRequest>>
mockCompletedRequests;
for (const auto& file : files) {
std::unique_ptr<SimpleAssetResponse> mockCompletedResponse =
std::make_unique<SimpleAssetResponse>(
static_cast<uint16_t>(200),
"doesn't matter",
CesiumAsync::HttpHeaders{},
readFile(testDataPath / file));
mockCompletedRequests.insert(
{file,
std::make_shared<SimpleAssetRequest>(
"GET",
file,
CesiumAsync::HttpHeaders{},
std::move(mockCompletedResponse))});
}
std::shared_ptr<SimpleAssetAccessor> mockAssetAccessor =
std::make_shared<SimpleAssetAccessor>(std::move(mockCompletedRequests));
TilesetExternals tilesetExternals{
mockAssetAccessor,
std::make_shared<SimplePrepareRendererResource>(),
AsyncSystem(std::make_shared<SimpleTaskProcessor>()),
nullptr};
// create tileset and call updateView() to give it a chance to load
Tileset tileset(tilesetExternals, "tileset.json");
initializeTileset(tileset);
// root is external tileset. Since its content is loading, we won't know if it
// has children or not
const Tile* root = tileset.getRootTile();
REQUIRE(root->getState() == Tile::LoadState::ContentLoading);
REQUIRE(root->getChildren().size() == 0);
SECTION("Load external tilesets") {
ViewState viewState = zoomToTileset(tileset);
// 1st frame. Root will get rendered first and 5 of its children are loading
// since they meet sse
{
ViewUpdateResult result = tileset.updateView({viewState});
// root is rendered first
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
REQUIRE(root->getChildren().size() == 1);
// root's children don't have content loading right now, so only root get
// rendered
const Tile& parentB3DM = root->getChildren().front();
REQUIRE(parentB3DM.getState() == Tile::LoadState::ContentLoading);
REQUIRE(!doesTileMeetSSE(viewState, parentB3DM, tileset));
REQUIRE(parentB3DM.getChildren().size() == 4);
for (const Tile& child : parentB3DM.getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::ContentLoading);
REQUIRE(doesTileMeetSSE(viewState, child, tileset));
}
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 6);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 5);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 2nd frame
{
ViewUpdateResult result = tileset.updateView({viewState});
// root is rendered first
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
REQUIRE(root->getChildren().size() == 1);
// root's children don't have content loading right now, so only root get
// rendered
const Tile& parentB3DM = root->getChildren().front();
REQUIRE(parentB3DM.getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, parentB3DM, tileset));
REQUIRE(parentB3DM.getChildren().size() == 4);
for (const Tile& child : parentB3DM.getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Done);
if (*std::get_if<std::string>(&child.getTileID()) !=
"tileset3/tileset3.json") {
REQUIRE(doesTileMeetSSE(viewState, child, tileset));
} else {
// external tileset has always geometric error over 999999, so it
// won't meet sse
REQUIRE(!doesTileMeetSSE(viewState, child, tileset));
// expect the children to meet sse and begin loading the content
REQUIRE(child.getChildren().size() == 1);
REQUIRE(
doesTileMeetSSE(viewState, child.getChildren().front(), tileset));
REQUIRE(
child.getChildren().front().getState() ==
Tile::LoadState::ContentLoading);
}
}
REQUIRE(result.tilesToRenderThisFrame.size() == 2);
REQUIRE(result.tilesToRenderThisFrame[0] == root);
REQUIRE(result.tilesToRenderThisFrame[1] == &parentB3DM);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 7);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 1);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 3rd frame. All the children finish loading. All should be rendered now
{
ViewUpdateResult result = tileset.updateView({viewState});
REQUIRE(result.tilesToRenderThisFrame.size() == 7);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 7);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
}
}
TEST_CASE("Render any tiles even when one of children can't be rendered for "
"additive refinement") {
Cesium3DTilesSelection::registerAllTileContentTypes();
std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR;
testDataPath = testDataPath / "ErrorChildrenAddTileset";
std::vector<std::string> files{
"tileset.json",
"parent.b3dm",
"error_lr.b3dm",
"ul.b3dm",
"ur.b3dm",
};
std::map<std::string, std::shared_ptr<SimpleAssetRequest>>
mockCompletedRequests;
for (const auto& file : files) {
std::unique_ptr<SimpleAssetResponse> mockCompletedResponse =
std::make_unique<SimpleAssetResponse>(
static_cast<uint16_t>(200),
"doesn't matter",
CesiumAsync::HttpHeaders{},
readFile(testDataPath / file));
mockCompletedRequests.insert(
{file,
std::make_shared<SimpleAssetRequest>(
"GET",
file,
CesiumAsync::HttpHeaders{},
std::move(mockCompletedResponse))});
}
std::shared_ptr<SimpleAssetAccessor> mockAssetAccessor =
std::make_shared<SimpleAssetAccessor>(std::move(mockCompletedRequests));
TilesetExternals tilesetExternals{
mockAssetAccessor,
std::make_shared<SimplePrepareRendererResource>(),
AsyncSystem(std::make_shared<SimpleTaskProcessor>()),
nullptr};
// create tileset and call updateView() to give it a chance to load
Tileset tileset(tilesetExternals, "tileset.json");
initializeTileset(tileset);
ViewState viewState = zoomToTileset(tileset);
Tile* root = tileset.getRootTile();
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
REQUIRE(root->getState() == Tile::LoadState::ContentLoading);
REQUIRE(root->getChildren().size() == 3);
// 1st frame. Root doesn't meet sse, so load children. But they are
// non-renderable, so render root only
{
ViewUpdateResult result = tileset.updateView({viewState});
for (const Tile& child : root->getChildren()) {
CHECK(child.getState() == Tile::LoadState::ContentLoading);
}
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 4);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 3);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
// 2nd frame. Root doesn't meet sse, so load children. But they are
// non-renderable, so render root only
{
ViewUpdateResult result = tileset.updateView({viewState});
REQUIRE(root->isRenderable());
for (const Tile& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Done);
REQUIRE(child.isRenderable());
}
REQUIRE(result.tilesToRenderThisFrame.size() == 4);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 0);
REQUIRE(result.tilesVisited == 4);
REQUIRE(result.tilesLoadingLowPriority == 0);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesLoadingHighPriority == 0);
REQUIRE(result.tilesCulled == 0);
REQUIRE(result.culledTilesVisited == 0);
}
}
TEST_CASE("Test multiple frustums") {
Cesium3DTilesSelection::registerAllTileContentTypes();
std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR;
testDataPath = testDataPath / "ReplaceTileset";
std::vector<std::string> files{
"tileset.json",
"parent.b3dm",
"ll.b3dm",
"lr.b3dm",
"ul.b3dm",
"ur.b3dm",
"ll_ll.b3dm",
};
std::map<std::string, std::shared_ptr<SimpleAssetRequest>>
mockCompletedRequests;
for (const auto& file : files) {
std::unique_ptr<SimpleAssetResponse> mockCompletedResponse =
std::make_unique<SimpleAssetResponse>(
static_cast<uint16_t>(200),
"doesn't matter",
CesiumAsync::HttpHeaders{},
readFile(testDataPath / file));
mockCompletedRequests.insert(
{file,
std::make_shared<SimpleAssetRequest>(
"GET",
file,
CesiumAsync::HttpHeaders{},
std::move(mockCompletedResponse))});
}
std::shared_ptr<SimpleAssetAccessor> mockAssetAccessor =
std::make_shared<SimpleAssetAccessor>(std::move(mockCompletedRequests));
TilesetExternals tilesetExternals{
mockAssetAccessor,
std::make_shared<SimplePrepareRendererResource>(),
AsyncSystem(std::make_shared<SimpleTaskProcessor>()),
nullptr};
// create tileset and call updateView() to give it a chance to load
Tileset tileset(tilesetExternals, "tileset.json");
initializeTileset(tileset);
// check the tiles status
const Tile* root = tileset.getRootTile();
REQUIRE(root->getState() == Tile::LoadState::ContentLoading);
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Unloaded);
}
// Zoom to tileset. Expect the root will not meet sse in this configure
ViewState viewState = zoomToTileset(tileset);
// Zoom out from the tileset a little bit to make sure the root meets sse
glm::dvec3 zoomOutPosition =
viewState.getPosition() - viewState.getDirection() * 2500.0;
ViewState zoomOutViewState = ViewState::create(
zoomOutPosition,
viewState.getDirection(),
viewState.getUp(),
viewState.getViewportSize(),
viewState.getHorizontalFieldOfView(),
viewState.getVerticalFieldOfView());
SECTION("The frustum with the highest SSE should be used for deciding to "
"refine") {
// frame 1
{
ViewUpdateResult result =
tileset.updateView({viewState, zoomOutViewState});
// Check tile state. Ensure root meets sse for only the zoomed out
// ViewState
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
REQUIRE(doesTileMeetSSE(zoomOutViewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::ContentLoading);
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 1);
REQUIRE(result.tilesLoadingMediumPriority == 4);
REQUIRE(result.tilesToRenderThisFrame.front() == root);
}
// frame 2
{
ViewUpdateResult result =
tileset.updateView({viewState, zoomOutViewState});
// Check tile state. Ensure root meets sse for only the zoomed out
// ViewState
REQUIRE(root->getState() == Tile::LoadState::Done);
REQUIRE(!doesTileMeetSSE(viewState, *root, tileset));
REQUIRE(doesTileMeetSSE(zoomOutViewState, *root, tileset));
for (const auto& child : root->getChildren()) {
REQUIRE(child.getState() == Tile::LoadState::Done);
}
// check result
REQUIRE(result.tilesToRenderThisFrame.size() == 4);
REQUIRE(result.tilesToNoLongerRenderThisFrame.size() == 1);
REQUIRE(result.tilesToNoLongerRenderThisFrame.front() == root);
REQUIRE(result.tilesVisited == 5);
REQUIRE(result.tilesLoadingMediumPriority == 0);
REQUIRE(result.tilesCulled == 0);
}
}
SECTION("Tiles should be culled if all the cameras agree") {
REQUIRE(root->getChildren().size() == 4);
const Tile& firstChild = root->getChildren()[0];
const Tile& secondChild = root->getChildren()[1];
REQUIRE(firstChild.getChildren().size() == 1);
const Tile& grandChild = firstChild.getChildren()[0];
ViewState zoomToTileViewState = zoomToTile(firstChild);
// Expected to only contain the grand child
// (child of the first child of the root).
glm::dvec3 zoomInPosition = zoomToTileViewState.getPosition() +
zoomToTileViewState.getDirection() * 250.0;
ViewState zoomInViewState1 = ViewState::create(
zoomInPosition,
zoomToTileViewState.getDirection(),
zoomToTileViewState.getUp(),
zoomToTileViewState.getViewportSize(),
0.5 * zoomToTileViewState.getHorizontalFieldOfView(),
0.5 * zoomToTileViewState.getVerticalFieldOfView());
zoomInPosition = zoomToTileViewState.getPosition() +
glm::dvec3(15.0, 0, 0) +
zoomToTileViewState.getDirection() * 243.0;
ViewState zoomInViewState2 = ViewState::create(
zoomInPosition,
zoomToTileViewState.getDirection(),
zoomToTileViewState.getUp(),
zoomToTileViewState.getViewportSize(),
0.5 * zoomToTileViewState.getHorizontalFieldOfView(),
0.5 * zoomToTileViewState.getVerticalFieldOfView());
// frame 3 & 4
{
tileset.updateView({zoomInViewState1, zoomInViewState2});
ViewUpdateResult result =
tileset.updateView({zoomInViewState1, zoomInViewState2});
// check result
// The grand child and the second child are the only ones rendered.
// The third and fourth children of the root are culled.
REQUIRE(result.tilesToRenderThisFrame.size() == 2);
REQUIRE(result.tilesVisited == 4);
REQUIRE(
std::find(
result.tilesToRenderThisFrame.begin(),
result.tilesToRenderThisFrame.end(),
&grandChild) != result.tilesToRenderThisFrame.end());
REQUIRE(
std::find(
result.tilesToRenderThisFrame.begin(),
result.tilesToRenderThisFrame.end(),
&secondChild) != result.tilesToRenderThisFrame.end());
REQUIRE(result.tilesCulled == 2);
}
}
}