diff --git a/CHANGES.md b/CHANGES.md index 351fb7985..1b8dc924f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,19 @@ ##### Breaking Changes :mega: +- Renamed `shouldContentContinueUpdating` to `getMightHaveLatentChildren` and `setContentShouldContinueUpdating` to `setMightHaveLatentChildren` on the `Tile` class. - `LoadedRasterOverlayImage` now has a single `errorList` property instead of separate `errors` and `warnings` properties. +##### Additions :tada: + +- Added `sampleHeightMostDetailed` method to `Tileset`. +- `AxisAlignedBox` now has `constexpr` constructors. + ##### Fixes :wrench: +- Fixed a bug that prevented use of `Tileset` with a nullptr `IPrepareRendererResources`. +- Fixed a bug in `IntersectionTests::rayOBBParametric` that could cause incorrect results for some oriented bounding boxes. +- `GltfUtilities::intersectRayGltfModel` now reports a warning when given a model it can't compute the intersection with because it uses required extensions that are not supported. - Errors while loading raster overlays are now logged. Previously, they were silently ignored in many cases. - A raster overlay image failing to load will no longer completely prevent the geometry tile to which it is attached from rendering. Instead, once the raster overlay fails, the geometry tile will be shown without the raster overlay. diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/SampleHeightResult.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/SampleHeightResult.h new file mode 100644 index 000000000..ee25eacfb --- /dev/null +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/SampleHeightResult.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include + +namespace Cesium3DTilesSelection { + +/** + * @brief The result of sampling heights with + * {@link Tileset::sampleHeightMostDetailed}. + */ +struct SampleHeightResult { + /** + * @brief The positions and their sampled heights. + * + * For each resulting position, its longitude and latitude values will match + * values from its input. Its height will either be the height sampled from + * the tileset at that position, or the original input height if the sample + * was unsuccessful. To determine which, look at the value of + * {@link SampleHeightResult::sampleSuccess} at the same index. + */ + std::vector positions; + + /** + * @brief The success of each sample. + * + * Each entry specifies whether the height for the position at the + * corresponding index was successfully sampled. If true, then + * {@link SampleHeightResult::positions} has a valid height sampled from the + * tileset at this index. If false, the height could not be sampled, leaving + * the height in {@link SampleHeightResult::positions} unchanged from the + * original input height. + */ + std::vector sampleSuccess; + + /** + * @brief Any warnings that occurred while sampling heights. + */ + std::vector warnings; +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h index df900f4b6..3f22a7a58 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tile.h @@ -501,10 +501,24 @@ class CESIUM3DTILESSELECTION_API Tile final { void setState(TileLoadState state) noexcept; - bool shouldContentContinueUpdating() const noexcept; + /** + * @brief Gets a flag indicating whether this tile might have latent children. + * Latent children don't exist in the `_children` property, but can be created + * by the {@link TilesetContentLoader}. + * + * When true, this tile might have children that can be created by the + * TilesetContentLoader but aren't yet reflected in the `_children` property. + * For example, in implicit tiling, we save memory by only creating explicit + * Tile instances from implicit availability as those instances are needed. + * When this flag is true, the creation of those explicit instances hasn't + * happened yet for this tile. + * + * If this flag is false, the children have already been created, if they + * exist. The tile may still have no children because it is a leaf node. + */ + bool getMightHaveLatentChildren() const noexcept; - void - setContentShouldContinueUpdating(bool shouldContentContinueUpdating) noexcept; + void setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept; // Position in bounding-volume hierarchy. Tile* _pParent; @@ -528,7 +542,7 @@ class CESIUM3DTILESSELECTION_API Tile final { TileContent _content; TilesetContentLoader* _pLoader; TileLoadState _loadState; - bool _shouldContentContinueUpdating; + bool _mightHaveLatentChildren; // mapped raster overlay std::vector _rasterTiles; diff --git a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h index fea035987..efa32e830 100644 --- a/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h +++ b/Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h @@ -2,6 +2,7 @@ #include "Library.h" #include "RasterOverlayCollection.h" +#include "SampleHeightResult.h" #include "Tile.h" #include "TilesetContentLoader.h" #include "TilesetExternals.h" @@ -15,6 +16,7 @@ #include +#include #include #include #include @@ -23,6 +25,8 @@ namespace Cesium3DTilesSelection { class TilesetContentManager; class TilesetMetadata; +class TilesetHeightQuery; +class TilesetHeightRequest; /** * @brief A loadMetadata(); + /** + * @brief Initiates an asynchronous query for the height of this tileset at a + * list of cartographic positions (longitude and latitude). The most detailed + * available tiles are used to determine each height. + * + * The height of the input positions is ignored. The output height is + * expressed in meters above the ellipsoid (usually WGS84), which should not + * be confused with a height above mean sea level. + * + * Note that {@link Tileset::updateView} must be called periodically, or else + * the returned `Future` will never resolve. If you are not using this tileset + * for visualization, you can call `updateView` with an empty list of + * frustums. + * + * @param positions The positions for which to sample heights. + * @return A future that asynchronously resolves to the result of the height + * query. + */ + CesiumAsync::Future sampleHeightMostDetailed( + const std::vector& positions); + private: /** * @brief The result of traversing one branch of the tile hierarchy. @@ -495,6 +520,7 @@ class CESIUM3DTILESSELECTION_API Tileset final { std::vector _mainThreadLoadQueue; std::vector _workerThreadLoadQueue; + std::vector _heightQueryLoadQueue; Tile::LoadedLinkedList _loadedTiles; @@ -509,6 +535,8 @@ class CESIUM3DTILESSELECTION_API Tileset final { CesiumUtility::IntrusivePointer _pTilesetContentManager; + std::list _heightRequests; + void addTileToLoadQueue( Tile& tile, TileLoadPriorityGroup priorityGroup, diff --git a/Cesium3DTilesSelection/src/Tile.cpp b/Cesium3DTilesSelection/src/Tile.cpp index 932dd5b0d..721ba5b63 100644 --- a/Cesium3DTilesSelection/src/Tile.cpp +++ b/Cesium3DTilesSelection/src/Tile.cpp @@ -57,7 +57,7 @@ Tile::Tile( _content{std::forward(args)...}, _pLoader{pLoader}, _loadState{loadState}, - _shouldContentContinueUpdating{true} {} + _mightHaveLatentChildren{true} {} Tile::Tile(Tile&& rhs) noexcept : _pParent(rhs._pParent), @@ -74,7 +74,7 @@ Tile::Tile(Tile&& rhs) noexcept _content(std::move(rhs._content)), _pLoader{rhs._pLoader}, _loadState{rhs._loadState}, - _shouldContentContinueUpdating{rhs._shouldContentContinueUpdating} { + _mightHaveLatentChildren{rhs._mightHaveLatentChildren} { // since children of rhs will have the parent pointed to rhs, // we will reparent them to this tile as rhs will be destroyed after this for (Tile& tile : this->_children) { @@ -105,7 +105,7 @@ Tile& Tile::operator=(Tile&& rhs) noexcept { this->_content = std::move(rhs._content); this->_pLoader = rhs._pLoader; this->_loadState = rhs._loadState; - this->_shouldContentContinueUpdating = rhs._shouldContentContinueUpdating; + this->_mightHaveLatentChildren = rhs._mightHaveLatentChildren; } return *this; @@ -227,12 +227,12 @@ void Tile::setParent(Tile* pParent) noexcept { this->_pParent = pParent; } void Tile::setState(TileLoadState state) noexcept { this->_loadState = state; } -bool Tile::shouldContentContinueUpdating() const noexcept { - return this->_shouldContentContinueUpdating; +bool Tile::getMightHaveLatentChildren() const noexcept { + return this->_mightHaveLatentChildren; } -void Tile::setContentShouldContinueUpdating( - bool shouldContentContinueUpdating) noexcept { - this->_shouldContentContinueUpdating = shouldContentContinueUpdating; +void Tile::setMightHaveLatentChildren(bool mightHaveLatentChildren) noexcept { + this->_mightHaveLatentChildren = mightHaveLatentChildren; } + } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/src/Tileset.cpp b/Cesium3DTilesSelection/src/Tileset.cpp index 05954d672..ee9d6f9ca 100644 --- a/Cesium3DTilesSelection/src/Tileset.cpp +++ b/Cesium3DTilesSelection/src/Tileset.cpp @@ -1,5 +1,6 @@ #include "TileUtilities.h" #include "TilesetContentManager.h" +#include "TilesetHeightQuery.h" #include #include @@ -45,13 +46,18 @@ Tileset::Tileset( _previousFrameNumber(0), _distances(), _childOcclusionProxies(), - _pTilesetContentManager{new TilesetContentManager( - _externals, - _options, - RasterOverlayCollection{_loadedTiles, externals, options.ellipsoid}, - std::vector{}, - std::move(pCustomLoader), - std::move(pRootTile))} {} + _pTilesetContentManager{ + new TilesetContentManager( + _externals, + _options, + RasterOverlayCollection{ + _loadedTiles, + externals, + options.ellipsoid}, + std::vector{}, + std::move(pCustomLoader), + std::move(pRootTile)), + } {} Tileset::Tileset( const TilesetExternals& externals, @@ -63,11 +69,16 @@ Tileset::Tileset( _previousFrameNumber(0), _distances(), _childOcclusionProxies(), - _pTilesetContentManager{new TilesetContentManager( - _externals, - _options, - RasterOverlayCollection{_loadedTiles, externals, options.ellipsoid}, - url)} {} + _pTilesetContentManager{ + new TilesetContentManager( + _externals, + _options, + RasterOverlayCollection{ + _loadedTiles, + externals, + options.ellipsoid}, + url), + } {} Tileset::Tileset( const TilesetExternals& externals, @@ -90,6 +101,10 @@ Tileset::Tileset( ionAssetEndpointUrl)} {} Tileset::~Tileset() noexcept { + TilesetHeightRequest::failHeightRequests( + this->_heightRequests, + "Tileset is being destroyed."); + this->_pTilesetContentManager->unloadAll(); if (this->_externals.pTileOcclusionProxyPool) { this->_externals.pTileOcclusionProxyPool->destroyPool(); @@ -325,6 +340,15 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { Tile* pRootTile = this->getRootTile(); if (!pRootTile) { + // If the root tile is marked as ready, but doesn't actually exist, then + // the tileset couldn't load. Fail any outstanding height requests. + if (!this->_heightRequests.empty() && this->_pTilesetContentManager && + this->_pTilesetContentManager->getRootTileAvailableEvent().isReady()) { + TilesetHeightRequest::failHeightRequests( + this->_heightRequests, + "Height requests could not complete because the tileset failed to " + "load."); + } return result; } @@ -358,6 +382,13 @@ Tileset::updateView(const std::vector& frustums, float deltaTime) { result = ViewUpdateResult(); } + TilesetHeightRequest::processHeightRequests( + *this->_pTilesetContentManager, + this->_options, + this->_loadedTiles, + this->_heightRequests, + this->_heightQueryLoadQueue); + result.workerThreadTileLoadQueueLength = static_cast(this->_workerThreadLoadQueue.size()); result.mainThreadTileLoadQueueLength = @@ -525,6 +556,27 @@ CesiumAsync::Future Tileset::loadMetadata() { }); } +CesiumAsync::Future +Tileset::sampleHeightMostDetailed(const std::vector& positions) { + if (positions.empty()) { + return this->_asyncSystem.createResolvedFuture({}); + } + + Promise promise = this->_asyncSystem.createPromise(); + + std::vector queries; + queries.reserve(positions.size()); + + for (const CesiumGeospatial::Cartographic& position : positions) { + queries.emplace_back(position, this->_options.ellipsoid); + } + + this->_heightRequests.emplace_back( + TilesetHeightRequest{std::move(queries), promise}); + + return promise.getFuture(); +} + static void markTileNonRendered( TileSelectionState::Result lastResult, Tile& tile, @@ -1427,17 +1479,52 @@ void Tileset::_processWorkerThreadLoadQueue() { return; } - std::vector& queue = this->_workerThreadLoadQueue; - std::sort(queue.begin(), queue.end()); + std::sort( + this->_workerThreadLoadQueue.begin(), + this->_workerThreadLoadQueue.end()); + + // Select tiles alternately from the two queues. Each frame, switch which + // queue we pull the first tile from. The goal is to schedule both height + // query and visualization tile loads fairly. + auto visIt = this->_workerThreadLoadQueue.begin(); + auto queryIt = this->_heightQueryLoadQueue.begin(); + + bool nextIsVis = (this->_previousFrameNumber % 2) == 0; + + while (this->_pTilesetContentManager->getNumberOfTilesLoading() < + maximumSimultaneousTileLoads) { + // Tell tiles from the current queue to load until one of them actually + // does. Calling loadTileContent might not actually start the loading + // process + int32_t originalNumberOfTilesLoading = + this->_pTilesetContentManager->getNumberOfTilesLoading(); + if (nextIsVis) { + while (visIt != this->_workerThreadLoadQueue.end() && + originalNumberOfTilesLoading == + this->_pTilesetContentManager->getNumberOfTilesLoading()) { + this->_pTilesetContentManager->loadTileContent(*visIt->pTile, _options); + ++visIt; + } + } else { + while (queryIt != this->_heightQueryLoadQueue.end() && + originalNumberOfTilesLoading == + this->_pTilesetContentManager->getNumberOfTilesLoading()) { + this->_pTilesetContentManager->loadTileContent(**queryIt, _options); + ++queryIt; + } + } - for (TileLoadTask& task : queue) { - this->_pTilesetContentManager->loadTileContent(*task.pTile, _options); - if (this->_pTilesetContentManager->getNumberOfTilesLoading() >= - maximumSimultaneousTileLoads) { + if (visIt == this->_workerThreadLoadQueue.end() && + queryIt == this->_heightQueryLoadQueue.end()) { + // No more work in either queue break; } + + // Get the next tile from the other queue. + nextIsVis = !nextIsVis; } } + void Tileset::_processMainThreadLoadQueue() { CESIUM_TRACE("Tileset::_processMainThreadLoadQueue"); // Process deferred main-thread load tasks with a time budget. diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.cpp b/Cesium3DTilesSelection/src/TilesetContentManager.cpp index 4a12a29c2..2ee6b657a 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.cpp +++ b/Cesium3DTilesSelection/src/TilesetContentManager.cpp @@ -572,64 +572,69 @@ postProcessContentInWorkerThread( pAssetAccessor, gltfOptions, std::move(gltfResult)) - .thenInWorkerThread( - [result = std::move(result), - projections = std::move(projections), - tileLoadInfo = std::move(tileLoadInfo), - rendererOptions]( - CesiumGltfReader::GltfReaderResult&& gltfResult) mutable { - if (!gltfResult.errors.empty()) { - if (result.pCompletedRequest) { - SPDLOG_LOGGER_ERROR( - tileLoadInfo.pLogger, - "Failed resolving external glTF buffers from {}:\n- {}", - result.pCompletedRequest->url(), - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); - } else { - SPDLOG_LOGGER_ERROR( - tileLoadInfo.pLogger, - "Failed resolving external glTF buffers:\n- {}", - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); - } - } - - if (!gltfResult.warnings.empty()) { - if (result.pCompletedRequest) { - SPDLOG_LOGGER_WARN( - tileLoadInfo.pLogger, - "Warning when resolving external gltf buffers from " - "{}:\n- {}", - result.pCompletedRequest->url(), - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); - } else { - SPDLOG_LOGGER_ERROR( - tileLoadInfo.pLogger, - "Warning resolving external glTF buffers:\n- {}", - CesiumUtility::joinToString(gltfResult.errors, "\n- ")); - } - } - - if (!gltfResult.model) { - return tileLoadInfo.asyncSystem.createResolvedFuture( - TileLoadResultAndRenderResources{ - TileLoadResult::createFailedResult(nullptr), - nullptr}); - } - - result.contentKind = std::move(*gltfResult.model); - - postProcessGltfInWorkerThread( - result, - std::move(projections), - tileLoadInfo); - - // create render resources - return tileLoadInfo.pPrepareRendererResources->prepareInLoadThread( - tileLoadInfo.asyncSystem, - std::move(result), - tileLoadInfo.tileTransform, - rendererOptions); - }); + .thenInWorkerThread([result = std::move(result), + projections = std::move(projections), + tileLoadInfo = std::move(tileLoadInfo), + rendererOptions](CesiumGltfReader::GltfReaderResult&& + gltfResult) mutable { + if (!gltfResult.errors.empty()) { + if (result.pCompletedRequest) { + SPDLOG_LOGGER_ERROR( + tileLoadInfo.pLogger, + "Failed resolving external glTF buffers from {}:\n- {}", + result.pCompletedRequest->url(), + CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + } else { + SPDLOG_LOGGER_ERROR( + tileLoadInfo.pLogger, + "Failed resolving external glTF buffers:\n- {}", + CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + } + } + + if (!gltfResult.warnings.empty()) { + if (result.pCompletedRequest) { + SPDLOG_LOGGER_WARN( + tileLoadInfo.pLogger, + "Warning when resolving external gltf buffers from " + "{}:\n- {}", + result.pCompletedRequest->url(), + CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + } else { + SPDLOG_LOGGER_ERROR( + tileLoadInfo.pLogger, + "Warning resolving external glTF buffers:\n- {}", + CesiumUtility::joinToString(gltfResult.errors, "\n- ")); + } + } + + if (!gltfResult.model) { + return tileLoadInfo.asyncSystem.createResolvedFuture( + TileLoadResultAndRenderResources{ + TileLoadResult::createFailedResult(nullptr), + nullptr}); + } + + result.contentKind = std::move(*gltfResult.model); + + postProcessGltfInWorkerThread( + result, + std::move(projections), + tileLoadInfo); + + // create render resources + if (tileLoadInfo.pPrepareRendererResources) { + return tileLoadInfo.pPrepareRendererResources->prepareInLoadThread( + tileLoadInfo.asyncSystem, + std::move(result), + tileLoadInfo.tileTransform, + rendererOptions); + } else { + return tileLoadInfo.asyncSystem + .createResolvedFuture( + TileLoadResultAndRenderResources{std::move(result), nullptr}); + } + }); } } // namespace @@ -1065,16 +1070,22 @@ void TilesetContentManager::updateTileContent( updateDoneState(tile, tilesetOptions); } - if (tile.shouldContentContinueUpdating()) { + this->createLatentChildrenIfNecessary(tile, tilesetOptions); +} + +void TilesetContentManager::createLatentChildrenIfNecessary( + Tile& tile, + const TilesetOptions& tilesetOptions) { + if (tile.getChildren().empty() && tile.getMightHaveLatentChildren()) { TileChildrenResult childrenResult = this->_pLoader->createTileChildren(tile, tilesetOptions.ellipsoid); if (childrenResult.state == TileLoadResultState::Success) { tile.createChildTiles(std::move(childrenResult.children)); } - bool shouldTileContinueUpdated = + bool mightStillHaveLatentChildren = childrenResult.state == TileLoadResultState::RetryLater; - tile.setContentShouldContinueUpdating(shouldTileContinueUpdated); + tile.setMightHaveLatentChildren(mightStillHaveLatentChildren); } } @@ -1374,19 +1385,12 @@ void TilesetContentManager::updateContentLoadedState( void TilesetContentManager::updateDoneState( Tile& tile, const TilesetOptions& tilesetOptions) { - // The reason for this method to terminate early when - // Tile::shouldContentContinueUpdating() returns true is that: When a tile has - // Tile::shouldContentContinueUpdating() to be true, it means the tile's - // children need to be created by the - // TilesetContentLoader::createTileChildren() which is invoked in the - // TilesetContentManager::updateTileContent() method. In the - // updateDoneState(), RasterOverlayTiles that are mapped to the tile will - // begin updating. If there are more RasterOverlayTiles with higher LOD and - // the current tile is a leaf, more upsample children will be created for that - // tile. So to accurately determine if a tile is a leaf, it needs the tile to - // have no children and Tile::shouldContentContinueUpdating() to return false - // which means the loader has no more children for this tile. - if (tile.shouldContentContinueUpdating()) { + if (tile.getMightHaveLatentChildren()) { + // This tile might have latent children, but we don't know yet whether it + // *actually* has children. We need to know that before we can continue + // this function, which will decide whether or not to create upsampled + // children for this tile. It only makes sense to create upsampled children + // for a tile that we know for sure doesn't have real children. return; } @@ -1483,10 +1487,12 @@ void TilesetContentManager::unloadContentLoadedState(Tile& tile) { pRenderContent && "Tile must have render content to be unloaded"); void* pWorkerRenderResources = pRenderContent->getRenderResources(); - this->_externals.pPrepareRendererResources->free( - tile, - pWorkerRenderResources, - nullptr); + if (this->_externals.pPrepareRendererResources) { + this->_externals.pPrepareRendererResources->free( + tile, + pWorkerRenderResources, + nullptr); + } pRenderContent->setRenderResources(nullptr); } diff --git a/Cesium3DTilesSelection/src/TilesetContentManager.h b/Cesium3DTilesSelection/src/TilesetContentManager.h index d750b2207..43ecb94f7 100644 --- a/Cesium3DTilesSelection/src/TilesetContentManager.h +++ b/Cesium3DTilesSelection/src/TilesetContentManager.h @@ -64,6 +64,27 @@ class TilesetContentManager void updateTileContent(Tile& tile, const TilesetOptions& tilesetOptions); + /** + * @brief Creates explicit Tile instances for a tile's latent children, if + * it is necessary and possible to do so. + * + * Latent children are child tiles that can be created by + * {@link TilesetContentLoader::createChildTiles} but that are not yet + * reflected in {@link Tile::getChildren}. For example, in implicit tiling, + * we save memory by only creating explicit Tile instances from implicit + * availability as those instances are needed. Calling this method will create + * the explicit tile instances for the given tile's children. + * + * This method does nothing if the given tile already has children, or if + * {@link Tile::getMightHaveLatentChildren} returns false. + * + * @param tile The tile for which to create latent children. + * @param tilesetOptions The tileset's options. + */ + void createLatentChildrenIfNecessary( + Tile& tile, + const TilesetOptions& tilesetOptions); + bool unloadTileContent(Tile& tile); void waitUntilIdle(); diff --git a/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp b/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp new file mode 100644 index 000000000..0bed6c942 --- /dev/null +++ b/Cesium3DTilesSelection/src/TilesetHeightQuery.cpp @@ -0,0 +1,345 @@ +#include "TilesetHeightQuery.h" + +#include "TileUtilities.h" +#include "TilesetContentManager.h" + +#include +#include +#include +#include + +using namespace Cesium3DTilesSelection; +using namespace CesiumGeospatial; +using namespace CesiumGeometry; +using namespace CesiumUtility; +using namespace CesiumAsync; + +namespace { +bool boundingVolumeContainsCoordinate( + const BoundingVolume& boundingVolume, + const Ray& ray, + const Cartographic& coordinate) { + struct Operation { + const Ray& ray; + const Cartographic& coordinate; + + bool operator()(const OrientedBoundingBox& boundingBox) noexcept { + std::optional t = + IntersectionTests::rayOBBParametric(ray, boundingBox); + return t && t.value() >= 0; + } + + bool operator()(const BoundingRegion& boundingRegion) noexcept { + return boundingRegion.getRectangle().contains(coordinate); + } + + bool operator()(const BoundingSphere& boundingSphere) noexcept { + std::optional t = + IntersectionTests::raySphereParametric(ray, boundingSphere); + return t && t.value() >= 0; + } + + bool operator()( + const BoundingRegionWithLooseFittingHeights& boundingRegion) noexcept { + return boundingRegion.getBoundingRegion().getRectangle().contains( + coordinate); + } + + bool operator()(const S2CellBoundingVolume& s2Cell) noexcept { + return s2Cell.computeBoundingRegion().getRectangle().contains(coordinate); + } + }; + + return std::visit(Operation{ray, coordinate}, boundingVolume); +} + +// The ray for height queries starts at this fraction of the ellipsoid max +// radius above the ellipsoid surface. If a tileset surface is more than this +// distance above the ellipsoid, it may be missed by height queries. +// 0.007 is chosen to accomodate Olympus Mons, the tallest peak on Mars. 0.007 +// is seven-tenths of a percent, or about 44,647 meters for WGS84, well above +// the highest point on Earth. +const double rayOriginHeightFraction = 0.007; + +Ray createRay(const Cartographic& position, const Ellipsoid& ellipsoid) { + Cartographic startPosition( + position.longitude, + position.latitude, + ellipsoid.getMaximumRadius() * rayOriginHeightFraction); + + return Ray( + Ellipsoid::WGS84.cartographicToCartesian(startPosition), + -Ellipsoid::WGS84.geodeticSurfaceNormal(startPosition)); +} + +} // namespace + +TilesetHeightQuery::TilesetHeightQuery( + const Cartographic& position, + const Ellipsoid& ellipsoid) + : inputPosition(position), + ray(createRay(position, ellipsoid)), + intersection(), + additiveCandidateTiles(), + candidateTiles(), + previousCandidateTiles() {} + +void TilesetHeightQuery::intersectVisibleTile( + Tile* pTile, + std::vector& outWarnings) { + TileRenderContent* pRenderContent = pTile->getContent().getRenderContent(); + if (!pRenderContent) + return; + + auto gltfIntersectResult = + CesiumGltfContent::GltfUtilities::intersectRayGltfModel( + this->ray, + pRenderContent->getModel(), + true, + pTile->getTransform()); + + if (!gltfIntersectResult.warnings.empty()) { + outWarnings.insert( + outWarnings.end(), + std::make_move_iterator(gltfIntersectResult.warnings.begin()), + std::make_move_iterator(gltfIntersectResult.warnings.end())); + } + + // Set ray info to this hit if closer, or the first hit + if (!this->intersection.has_value()) { + this->intersection = std::move(gltfIntersectResult.hit); + } else { + double prevDistSq = this->intersection->rayToWorldPointDistanceSq; + double thisDistSq = intersection->rayToWorldPointDistanceSq; + if (thisDistSq < prevDistSq) + this->intersection = std::move(gltfIntersectResult.hit); + } +} + +namespace { + +void markTileVisited(Tile::LoadedLinkedList& loadedTiles, Tile* pTile) { + // Don't move the root tile to the tail, because this tile is used to mark the + // beginning of the tiles used in the current frame. If we move it, some tiles + // may be deemed to have most recently been used last frame, and so will be + // unloaded. + if (pTile == nullptr || pTile->getParent() == nullptr) + return; + + loadedTiles.insertAtTail(*pTile); +} + +} // namespace + +void TilesetHeightQuery::findCandidateTiles( + Tile* pTile, + Tile::LoadedLinkedList& loadedTiles, + std::vector& warnings) { + // Make sure this tile is not unloaded until we're done with it. + markTileVisited(loadedTiles, pTile); + + // If tile failed to load, this means we can't complete the intersection + if (pTile->getState() == TileLoadState::Failed) { + warnings.push_back("Tile load failed during query. Ignoring."); + return; + } + + const std::optional& contentBoundingVolume = + pTile->getContentBoundingVolume(); + + if (pTile->getChildren().empty()) { + // This is a leaf node, it's a candidate + + // If optional content bounding volume exists, test against it + if (contentBoundingVolume) { + if (boundingVolumeContainsCoordinate( + *contentBoundingVolume, + this->ray, + this->inputPosition)) + this->candidateTiles.push_back(pTile); + } else { + this->candidateTiles.push_back(pTile); + } + } else { + // We have children + + // If additive refinement, add parent to the list with children + if (pTile->getRefine() == TileRefine::Add) { + // If optional content bounding volume exists, test against it + if (contentBoundingVolume) { + if (boundingVolumeContainsCoordinate( + *contentBoundingVolume, + this->ray, + this->inputPosition)) + this->additiveCandidateTiles.push_back(pTile); + } else { + this->additiveCandidateTiles.push_back(pTile); + } + } + + // Traverse children + for (Tile& child : pTile->getChildren()) { + // if bounding volume doesn't intersect this ray, we can skip it + if (!boundingVolumeContainsCoordinate( + child.getBoundingVolume(), + this->ray, + this->inputPosition)) + continue; + + // Child is a candidate, traverse it and its children + findCandidateTiles(&child, loadedTiles, warnings); + } + } +} + +/*static*/ void TilesetHeightRequest::processHeightRequests( + TilesetContentManager& contentManager, + const TilesetOptions& options, + Tile::LoadedLinkedList& loadedTiles, + std::list& heightRequests, + std::vector& heightQueryLoadQueue) { + if (heightRequests.empty()) + return; + + // Go through all requests, either complete them, or gather the tiles they + // need for completion + std::set tileLoadSet; + for (auto it = heightRequests.begin(); it != heightRequests.end();) { + TilesetHeightRequest& request = *it; + if (!request.tryCompleteHeightRequest( + contentManager, + options, + loadedTiles, + tileLoadSet)) { + ++it; + } else { + auto deleteIt = it; + ++it; + heightRequests.erase(deleteIt); + } + } + + heightQueryLoadQueue.assign(tileLoadSet.begin(), tileLoadSet.end()); +} + +void Cesium3DTilesSelection::TilesetHeightRequest::failHeightRequests( + std::list& heightRequests, + const std::string& message) { + for (TilesetHeightRequest& request : heightRequests) { + request.promise.reject(std::runtime_error(message)); + } + + heightRequests.clear(); +} + +bool TilesetHeightRequest::tryCompleteHeightRequest( + TilesetContentManager& contentManager, + const TilesetOptions& options, + Tile::LoadedLinkedList& loadedTiles, + std::set& tileLoadSet) { + bool tileStillNeedsLoading = false; + std::vector warnings; + for (TilesetHeightQuery& query : this->queries) { + if (query.candidateTiles.empty() && query.additiveCandidateTiles.empty()) { + // Find the initial set of tiles whose bounding volume is intersected by + // the query ray. + query.findCandidateTiles( + contentManager.getRootTile(), + loadedTiles, + warnings); + } else { + // Refine the current set of candidate tiles, in case further tiles from + // implicit tiling, external tilesets, etc. having been loaded since last + // frame. + std::swap(query.candidateTiles, query.previousCandidateTiles); + + query.candidateTiles.clear(); + + for (Tile* pCandidate : query.previousCandidateTiles) { + TileLoadState loadState = pCandidate->getState(); + if (!pCandidate->getChildren().empty() && + loadState >= TileLoadState::ContentLoaded) { + query.findCandidateTiles(pCandidate, loadedTiles, warnings); + } else { + // Make sure this tile stays loaded. + markTileVisited(loadedTiles, pCandidate); + + // Check again next frame to see if this tile has children. + query.candidateTiles.emplace_back(pCandidate); + } + } + } + + auto checkTile = [&contentManager, + &options, + &tileLoadSet, + &tileStillNeedsLoading](Tile* pTile) { + contentManager.createLatentChildrenIfNecessary(*pTile, options); + + TileLoadState state = pTile->getState(); + if (state == TileLoadState::Unloading) { + // This tile is in the process of unloading, which must complete + // before we can load it again. + contentManager.unloadTileContent(*pTile); + tileStillNeedsLoading = true; + } else if (state <= TileLoadState::ContentLoading) { + tileLoadSet.insert(pTile); + tileStillNeedsLoading = true; + } + }; + + // If any candidates need loading, add to return set + for (Tile* pTile : query.additiveCandidateTiles) { + // Additive tiles are only enumerated once in findCandidateTiles, so we + // need to continue every frame to make sure they're not unloaded before + // we're done with them. + markTileVisited(loadedTiles, pTile); + + checkTile(pTile); + } + for (Tile* pTile : query.candidateTiles) { + checkTile(pTile); + } + } + + // Bail if we're waiting on tiles to load + if (tileStillNeedsLoading) + return false; + + // Do the intersect tests + for (TilesetHeightQuery& query : this->queries) { + for (Tile* pTile : query.additiveCandidateTiles) { + query.intersectVisibleTile(pTile, warnings); + } + for (Tile* pTile : query.candidateTiles) { + query.intersectVisibleTile(pTile, warnings); + } + } + + // All rays are done, create results + SampleHeightResult results; + + // Start with any warnings from tile traversal + results.warnings = std::move(warnings); + + results.positions.resize(this->queries.size(), Cartographic(0.0, 0.0, 0.0)); + results.sampleSuccess.resize(this->queries.size()); + + // Populate results with completed queries + for (size_t i = 0; i < this->queries.size(); ++i) { + const TilesetHeightQuery& query = this->queries[i]; + + bool sampleSuccess = query.intersection.has_value(); + results.sampleSuccess[i] = sampleSuccess; + results.positions[i] = query.inputPosition; + + if (sampleSuccess) { + results.positions[i].height = + options.ellipsoid.getMaximumRadius() * rayOriginHeightFraction - + glm::sqrt(query.intersection->rayToWorldPointDistanceSq); + } + } + + this->promise.resolve(std::move(results)); + return true; +} diff --git a/Cesium3DTilesSelection/src/TilesetHeightQuery.h b/Cesium3DTilesSelection/src/TilesetHeightQuery.h new file mode 100644 index 000000000..fdb2e0317 --- /dev/null +++ b/Cesium3DTilesSelection/src/TilesetHeightQuery.h @@ -0,0 +1,173 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace Cesium3DTilesSelection { + +class TilesetContentManager; +struct TilesetOptions; +struct SampleHeightResult; + +class TilesetHeightQuery { +public: + /** + * @brief Initializes a new instance. + * + * @param position The position at which to query a height. The existing + * height is ignored. + * @param ellipsoid The ellipsoid on which the position is defined. + */ + TilesetHeightQuery( + const CesiumGeospatial::Cartographic& position, + const CesiumGeospatial::Ellipsoid& ellipsoid); + + /** + * @brief The original input position for which the height is to be queried. + */ + CesiumGeospatial::Cartographic inputPosition; + + /** + * @brief A ray created from the {@link TilesetHeightQuery::inputPosition}. + * + */ + CesiumGeometry::Ray ray; + + /** + * @brief The current intersection of the ray with the tileset. If there are + * multiple intersections, this will be the one closest to the origin of the + * ray. + */ + std::optional intersection; + + /** + * @brief Non-leaf tiles with additive refinement whose bounding volumes are + * intersected by the query ray. + */ + std::vector additiveCandidateTiles; + + /** + * @brief The current set of leaf tiles whose bounding volumes are intersected + * by the query ray. + */ + std::vector candidateTiles; + + /** + * @brief The previous set of leaf tiles. Swapping `candidateTiles` and + * `previousCandidateTiles` each frame allows us to avoid a heap allocation + * for a new vector each frame. + */ + std::vector previousCandidateTiles; + + /** + * @brief Find the intersection of the ray with the given tile. If there is + * one, and if it's closer to the ray's origin than the previous best-known + * intersection, then {@link TilesetHeightQuery::intersection} will be + * updated. + * + * @param pTile The tile to test for intersection with the ray. + * @param outWarnings On return, reports any warnings that occurred while + * attempting to intersect the ray with the tile. + */ + void intersectVisibleTile(Tile* pTile, std::vector& outWarnings); + + /** + * @brief Find candidate tiles for the height query by traversing the tile + * tree, starting with the given tile. + * + * Any tile whose bounding volume intersects the ray will be added to the + * {@link TilesetHeightQuery::candidateTiles} vector. Non-leaf tiles that are + * additively-refined will be added to + * {@link TilesetHeightQuery::additiveCandidateTiles}. + * + * @param pTile The tile at which to start traversal. + * @param loadedTiles The linked list of loaded tiles, used to ensure that + * tiles loaded for height queries stay loaded just long enough to complete + * the query, and no longer. + * @param outWarnings On return, reports any warnings that occurred during + * candidate search. + */ + void findCandidateTiles( + Tile* pTile, + Tile::LoadedLinkedList& loadedTiles, + std::vector& outWarnings); +}; + +/** + * @brief A request for a batch of height queries. When all of the queries are + * complete, they will be delivered to the requestor via resolving a promise. + */ +struct TilesetHeightRequest { + /** + * @brief The individual height queries in this request. + */ + std::vector queries; + + /** + * @brief The promise to be resolved when all height queries are complete. + */ + CesiumAsync::Promise promise; + + /** + * @brief Process a given list of height requests. This is called by the {@link Tileset} + * in every call to {@link Tileset::updateView}. + * + * @param contentManager The content manager. + * @param options Options associated with the tileset. + * @param loadedTiles The linked list of loaded tiles, used to ensure that + * tiles loaded for height queries stay loaded just long enough to complete + * the query, and no longer. + * @param heightRequests The list of all height requests. Completed requests + * will be removed from this list. + * @param heightQueryLoadQueue Tiles that still need to be loaded before all + * height requests can complete are added to this vector. + */ + static void processHeightRequests( + TilesetContentManager& contentManager, + const TilesetOptions& options, + Tile::LoadedLinkedList& loadedTiles, + std::list& heightRequests, + std::vector& heightQueryLoadQueue); + + /** + * @brief Cancels all outstanding height requests and rejects the associated + * futures. This is useful when it is known that the height requests will + * never complete, such as when the tileset fails to load or when it is being + * destroyed. + * + * @param heightRequests The height requests to cancel. + * @param message The message explaining what went wrong. + */ + static void failHeightRequests( + std::list& heightRequests, + const std::string& message); + + /** + * @brief Tries to complete this height request. Returns false if further data + * still needs to be loaded and thus the request cannot yet complete. + * + * @param contentManager The content manager. + * @param options Options associated with the tileset. + * @param loadedTiles The linked list of loaded tiles, used to ensure that + * tiles loaded for height queries stay loaded just long enough to complete + * the query, and no longer. + * @param tileLoadSet Tiles that needs to be loaded before this height request + * can complete. + */ + bool tryCompleteHeightRequest( + TilesetContentManager& contentManager, + const TilesetOptions& options, + Tile::LoadedLinkedList& loadedTiles, + std::set& tileLoadSet); +}; + +} // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp index 47719fd9c..5f1afcc53 100644 --- a/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp +++ b/Cesium3DTilesSelection/test/MockTilesetContentManager.cpp @@ -10,6 +10,6 @@ void MockTilesetContentManagerTestFixture::setTileLoadState( void MockTilesetContentManagerTestFixture::setTileShouldContinueUpdating( Cesium3DTilesSelection::Tile& tile, bool shouldContinueUpdating) { - tile.setContentShouldContinueUpdating(shouldContinueUpdating); + tile.setMightHaveLatentChildren(shouldContinueUpdating); } } // namespace Cesium3DTilesSelection diff --git a/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp b/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp new file mode 100644 index 000000000..a5deaa259 --- /dev/null +++ b/Cesium3DTilesSelection/test/TestTilesetHeightQueries.cpp @@ -0,0 +1,228 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace Cesium3DTilesContent; +using namespace Cesium3DTilesSelection; +using namespace CesiumAsync; +using namespace CesiumGeospatial; +using namespace CesiumNativeTests; +using namespace CesiumUtility; + +namespace { + +std::filesystem::path testDataPath = Cesium3DTilesSelection_TEST_DATA_DIR; + +} + +TEST_CASE("Tileset height queries") { + // The coordinates and expected heights in this file were determined in Cesium + // for Unreal Engine by adding the tileset, putting a cube above the location + // of interest, adding a CesiumGlobeAnchor to it, and pressing the "End" key + // to drop it onto terrain. The coordinates were then copied out of the globe + // anchor, subtracting 0.5 from the height to account for "End" placing the + // bottom of the cube on the surface instead of its center. + + registerAllTileContentTypes(); + + std::shared_ptr pAccessor = + std::make_shared(); + AsyncSystem asyncSystem(std::make_shared()); + + TilesetExternals externals{pAccessor, nullptr, asyncSystem, nullptr}; + + SECTION("Additive-refined tileset") { + std::string url = + "file://" + Uri::nativePathToUriPath( + (testDataPath / "Tileset" / "tileset.json").u8string()); + + Tileset tileset(externals, url); + + Future future = tileset.sampleHeightMostDetailed( + // A point on geometry in "parent.b3dm", which should only be included + // because this tileset is additive-refined. + {Cartographic::fromDegrees(-75.612088, 40.042526, 0.0), + + // A point on geometry in a leaf tile. + Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + SampleHeightResult results = future.waitInMainThread(); + CHECK(results.warnings.empty()); + REQUIRE(results.positions.size() == 2); + + CHECK(results.sampleSuccess[0]); + CHECK(Math::equalsEpsilon( + results.positions[0].height, + 78.155809, + 0.0, + Math::Epsilon4)); + + CHECK(results.sampleSuccess[1]); + CHECK(Math::equalsEpsilon( + results.positions[1].height, + 7.837332, + 0.0, + Math::Epsilon4)); + } + + SECTION("Replace-refined tileset") { + std::string url = + "file://" + + Uri::nativePathToUriPath( + (testDataPath / "ReplaceTileset" / "tileset.json").u8string()); + + Tileset tileset(externals, url); + + Future future = tileset.sampleHeightMostDetailed( + // A point on geometry in "parent.b3dm", which should not be + // included because this tileset is replace-refined. + {Cartographic::fromDegrees(-75.612088, 40.042526, 0.0), + + // A point on geometry in a leaf tile. + Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + SampleHeightResult results = future.waitInMainThread(); + CHECK(results.warnings.empty()); + REQUIRE(results.positions.size() == 2); + + CHECK(!results.sampleSuccess[0]); + + CHECK(results.sampleSuccess[1]); + CHECK(Math::equalsEpsilon( + results.positions[1].height, + 7.837332, + 0.0, + Math::Epsilon4)); + } + + SECTION("External tileset") { + std::string url = + "file://" + + Uri::nativePathToUriPath( + (testDataPath / "AddTileset" / "tileset.json").u8string()); + + Tileset tileset(externals, url); + + Future future = tileset.sampleHeightMostDetailed( + // A point on geometry in "0/0/0.b3dm", which should only be + // included because this tileset is additive-refined. + {Cartographic::fromDegrees(-75.612088, 40.042526, 0.0), + + // A point on geometry in a leaf tile. + Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + SampleHeightResult results = future.waitInMainThread(); + CHECK(results.warnings.empty()); + REQUIRE(results.positions.size() == 2); + + CHECK(results.sampleSuccess[0]); + CHECK(Math::equalsEpsilon( + results.positions[0].height, + 78.155809, + 0.0, + Math::Epsilon4)); + + CHECK(results.sampleSuccess[1]); + CHECK(Math::equalsEpsilon( + results.positions[1].height, + 7.837332, + 0.0, + Math::Epsilon4)); + } + + SECTION("Implicit tileset") { + std::string url = + "file://" + + Uri::nativePathToUriPath( + (testDataPath / "ImplicitTileset" / "tileset_1.1.json").u8string()); + + Tileset tileset(externals, url); + + Future future = tileset.sampleHeightMostDetailed( + // A point on geometry in "0/0/0.b3dm", which should only be + // included because this tileset is additive-refined. + {Cartographic::fromDegrees(-75.612088, 40.042526, 0.0), + + // A point on geometry in a leaf tile. + Cartographic::fromDegrees(-75.612025, 40.041684, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + SampleHeightResult results = future.waitInMainThread(); + CHECK(results.warnings.empty()); + REQUIRE(results.positions.size() == 2); + + CHECK(results.sampleSuccess[0]); + CHECK(Math::equalsEpsilon( + results.positions[0].height, + 78.155809, + 0.0, + Math::Epsilon4)); + + CHECK(results.sampleSuccess[1]); + CHECK(Math::equalsEpsilon( + results.positions[1].height, + 7.837332, + 0.0, + Math::Epsilon4)); + } + + SECTION("Instanced model is not yet supported") { + std::string url = + "file://" + + Uri::nativePathToUriPath( + (testDataPath / "i3dm" / "InstancedWithBatchTable" / "tileset.json") + .u8string()); + + Tileset tileset(externals, url); + + Future future = tileset.sampleHeightMostDetailed( + {Cartographic::fromDegrees(-75.612559, 40.042183, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + SampleHeightResult results = future.waitInMainThread(); + REQUIRE(results.warnings.size() == 1); + REQUIRE(results.positions.size() == 1); + CHECK(!results.sampleSuccess[0]); + CHECK( + results.warnings[0].find("EXT_mesh_gpu_instancing") != + std::string::npos); + } + + SECTION("broken tileset") { + Tileset tileset(externals, "http://localhost/notgonnawork"); + + Future future = tileset.sampleHeightMostDetailed( + {Cartographic::fromDegrees(-75.612559, 40.042183, 0.0)}); + + while (!future.isReady()) { + tileset.updateView({}); + } + + REQUIRE_THROWS(future.waitInMainThread()); + } +} diff --git a/CesiumGeometry/include/CesiumGeometry/AxisAlignedBox.h b/CesiumGeometry/include/CesiumGeometry/AxisAlignedBox.h index d63ee5eb4..f4033028d 100644 --- a/CesiumGeometry/include/CesiumGeometry/AxisAlignedBox.h +++ b/CesiumGeometry/include/CesiumGeometry/AxisAlignedBox.h @@ -8,7 +8,7 @@ namespace CesiumGeometry { struct CESIUMGEOMETRY_API AxisAlignedBox final { - AxisAlignedBox() noexcept + constexpr AxisAlignedBox() noexcept : minimumX(0.0), minimumY(0.0), minimumZ(0.0), @@ -20,7 +20,7 @@ struct CESIUMGEOMETRY_API AxisAlignedBox final { lengthZ(0.0), center(0.0) {} - AxisAlignedBox( + constexpr AxisAlignedBox( double minimumX_, double minimumY_, double minimumZ_, diff --git a/CesiumGeometry/src/IntersectionTests.cpp b/CesiumGeometry/src/IntersectionTests.cpp index 1d8079a0c..7029a38a5 100644 --- a/CesiumGeometry/src/IntersectionTests.cpp +++ b/CesiumGeometry/src/IntersectionTests.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -229,23 +230,30 @@ IntersectionTests::rayOBB(const Ray& ray, const OrientedBoundingBox& obb) { std::optional IntersectionTests::rayOBBParametric( const Ray& ray, const OrientedBoundingBox& obb) { - - const glm::dmat3x3& inverseHalfAxis = obb.getInverseHalfAxes(); - glm::dmat4x4 transformation( - glm::dvec4(glm::normalize(inverseHalfAxis[0]), 0.0), - glm::dvec4(glm::normalize(inverseHalfAxis[1]), 0.0), - glm::dvec4(glm::normalize(inverseHalfAxis[2]), 0.0), - glm::dvec4(0.0, 0.0, 0.0, 1.0)); - - glm::dvec3 center = - glm::dvec3(transformation * glm::dvec4(obb.getCenter(), 1.0)); - glm::dvec3 halfLengths = obb.getLengths() / 2.0; - glm::dvec3 ll = center - halfLengths; - glm::dvec3 ur = center + halfLengths; - - return rayAABBParametric( - ray.transform(transformation), - AxisAlignedBox(ll.x, ll.y, ll.z, ur.x, ur.y, ur.z)); + // Extract the rotation from the OBB's rotatin/scale transformation and + // invert it. This code assumes that there is not a negative scale, that + // there's no skew, that there's no other funny business. Non-uniform scale + // is fine! + const glm::dmat3& halfAxes = obb.getHalfAxes(); + glm::dvec3 halfLengths = obb.getLengths() * 0.5; + glm::dmat3 rotationOnly( + halfAxes[0] / halfLengths.x, + halfAxes[1] / halfLengths.y, + halfAxes[2] / halfLengths.z); + glm::dmat3 inverseRotation = glm::transpose(rotationOnly); + + // Find the equivalent ray in the coordinate system where the OBB is not + // rotated or translated. That is, where it's an AABB at the origin. + glm::dvec3 relativeOrigin = ray.getOrigin() - obb.getCenter(); + glm::dvec3 rayOrigin(inverseRotation * relativeOrigin); + glm::dvec3 rayDirection(inverseRotation * ray.getDirection()); + + // Find the distance to the new ray's intersection with the AABB, which is + // equivalent to the distance of the original ray intersection with the OBB. + glm::dvec3 ll = -halfLengths; + glm::dvec3 ur = +halfLengths; + AxisAlignedBox aabb(ll.x, ll.y, ll.z, ur.x, ur.y, ur.z); + return rayAABBParametric(Ray(rayOrigin, rayDirection), aabb); } std::optional diff --git a/CesiumGeometry/test/TestIntersectionTests.cpp b/CesiumGeometry/test/TestIntersectionTests.cpp index 1524d9e42..61d3d49d3 100644 --- a/CesiumGeometry/test/TestIntersectionTests.cpp +++ b/CesiumGeometry/test/TestIntersectionTests.cpp @@ -7,7 +7,8 @@ #include "CesiumUtility/Math.h" #include -#include +#include +#include #include @@ -245,9 +246,7 @@ TEST_CASE("IntersectionTests::rayAABB") { TEST_CASE("IntersectionTests::rayOBB") { struct TestCase { Ray ray; - glm::dvec3 xHalf; - glm::dvec3 yHalf; - glm::dvec3 obbOrigin; + OrientedBoundingBox obb; std::optional expectedIntersectionPoint; }; @@ -255,25 +254,118 @@ TEST_CASE("IntersectionTests::rayOBB") { // 2x2x2 obb at origin that is rotated -45 degrees on the x-axis. TestCase{ Ray(glm::dvec3(0.0, 0.0, 10.0), glm::dvec3(0.0, 0.0, -1.0)), - glm::dvec3(-1.0 / glm::sqrt(2), 0.0, 1.0 / glm::sqrt(2)), - glm::dvec3(0.0, 1.0, 0.0), - glm::dvec3(0.0, 0.0, 0.0), - glm::dvec3(0.0, 0.0, 2.0 / glm::sqrt(2))}, + OrientedBoundingBox( + glm::dvec3(0.0, 0.0, 0.0), + glm::dmat3( + glm::rotate(glm::radians(-45.0), glm::dvec3(1.0, 0.0, 0.0)))), + glm::dvec3(0.0, 0.0, glm::sqrt(2.0))}, // 2x2x2 obb at (10,10,10) that is rotated -45 degrees on the x-axis. TestCase{ Ray(glm::dvec3(10.0, 10.0, 20.0), glm::dvec3(0.0, 0.0, -1.0)), - glm::dvec3(-1.0 / glm::sqrt(2), 0.0, 1.0 / glm::sqrt(2)), - glm::dvec3(0.0, 1.0, 0.0), - glm::dvec3(10.0, 10.0, 10.0), - glm::dvec3(10.0, 10.0, 10.0 + 2.0 / glm::sqrt(2))}); - std::optional intersectionPoint = IntersectionTests::rayOBB( - testCase.ray, - OrientedBoundingBox( - testCase.obbOrigin, - glm::dmat3x3( - testCase.xHalf, - testCase.yHalf, - glm::cross(testCase.xHalf, testCase.yHalf)))); + OrientedBoundingBox( + glm::dvec3(10.0, 10.0, 10.0), + glm::dmat3( + glm::rotate(glm::radians(-45.0), glm::dvec3(1.0, 0.0, 0.0)))), + glm::dvec3(10.0, 10.0, 10.0 + glm::sqrt(2.0))}, + // 2x2x2 obb at (10,20,30) that is rotated -45 degrees on the x-axis and + // hit from an angle. + TestCase{ + Ray(glm::dvec3(10.0, 20.0 + 2.0, 30.0 + 1.0 + glm::sqrt(2)), + glm::normalize(glm::dvec3(0.0, -2.0, -1.0))), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3( + glm::rotate(glm::radians(-45.0), glm::dvec3(1.0, 0.0, 0.0)))), + glm::dvec3(10.0, 20.0, 30.0 + glm::sqrt(2.0))}, + // 4x4x4 obb at (10,10,10) that is rotated -45 degrees on the x-axis. + TestCase{ + Ray(glm::dvec3(10.0, 10.0, 20.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 10.0, 10.0), + 2.0 * glm::dmat3(glm::rotate( + glm::radians(-45.0), + glm::dvec3(1.0, 0.0, 0.0)))), + glm::dvec3(10.0, 10.0, 10.0 + glm::sqrt(8.0))}, + // 4x4x4 obb at (10,20,30) that is rotated -45 degrees on the x-axis and + // hit from an angle + TestCase{ + Ray(glm::dvec3(10.0, 20.0 + 10.0, 30.0 + 20.0 + glm::sqrt(8.0)), + glm::normalize(glm::dvec3(0.0, -1.0, -2.0))), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + 2.0 * glm::dmat3(glm::rotate( + glm::radians(-45.0), + glm::dvec3(1.0, 0.0, 0.0)))), + glm::dvec3(10.0, 20.0, 30.0 + glm::sqrt(8.0))}, + // 4x4x2 obb at (10,10,10) that is not rotated. + TestCase{ + Ray(glm::dvec3(10.0, 10.0, 20.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 10.0, 10.0), + glm::dmat3(glm::scale(glm::dvec3(2.0, 2.0, 1.0)))), + glm::dvec3(10.0, 10.0, 10.0 + 1.0)}, + // 4x2x4 obb at (10,20,30) that is not rotated. + TestCase{ + Ray(glm::dvec3(10.0, 20.0, 40.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3(glm::scale(glm::dvec3(2.0, 1.0, 2.0)))), + glm::dvec3(10.0, 20.0, 30.0 + 2.0)}, + // 2x4x2 obb at (10,20,30) that is rotated 45 degrees on the Y-axis. + TestCase{ + Ray(glm::dvec3(10.0, 20.0, 40.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3(glm::scale(glm::dvec3(1.0, 2.0, 1.0))) * + glm::dmat3(glm::rotate( + glm::radians(45.0), + glm::dvec3(0.0, 1.0, 0.0)))), + glm::dvec3(10.0, 20.0, 30.0 + glm::sqrt(2.0))}, + // 2x4x2 obb at (10,20,30) that is rotated 45 degrees on the X-axis. + TestCase{ + Ray(glm::dvec3(10.0, 20.0, 40.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3( + glm::rotate(glm::radians(45.0), glm::dvec3(1.0, 0.0, 0.0))) * + glm::dmat3(glm::scale(glm::dvec3(1.0, 2.0, 1.0)))), + glm::dvec3(10.0, 20.0, 30.0 + 1.0 / glm::cos(glm::radians(45.0)))}, + // 2x4x2 obb at (10,20,30) that is rotated 225 degrees on the Y-axis. + TestCase{ + Ray(glm::dvec3(10.0, 20.0, 40.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3(glm::scale(glm::dvec3(1.0, 2.0, 1.0))) * + glm::dmat3(glm::rotate( + glm::radians(225.0), + glm::dvec3(0.0, 1.0, 0.0)))), + glm::dvec3(10.0, 20.0, 30.0 + glm::sqrt(2.0))}, + // 2x2x4 obb at (10,20,30) that is rotated 90 degrees on the X-axis and + // hit from an angle. + TestCase{ + Ray(glm::dvec3(10.0, 20.0 + 2.0, 30.0 + 1.0 + 1.0), + glm::normalize(glm::dvec3(0.0, -2.0, -1.0))), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + glm::dmat3( + glm::rotate(glm::radians(90.0), glm::dvec3(1.0, 0.0, 0.0))) * + glm::dmat3(glm::scale(glm::dvec3(1.0, 1.0, 2.0)))), + glm::dvec3(10.0, 20.0, 30.0 + 1.0)}, + // 2x2x2 obb at (10,20,30) that is rotated 45 degrees on the X- and + // Y-axis. + TestCase{ + Ray(glm::dvec3(10.0, 20.0, 40.0), glm::dvec3(0.0, 0.0, -1.0)), + OrientedBoundingBox( + glm::dvec3(10.0, 20.0, 30.0), + (glm::dmat3(glm::rotate( + glm::atan(1.0 / 2.0, glm::sqrt(2) / 2.0), + glm::dvec3(1.0, 0.0, 0.0)))) * + glm::dmat3(glm::rotate( + glm::radians(45.0), + glm::dvec3(0.0, 1.0, 0.0)))), + glm::dvec3(10.0, 20.0, 30.0 + glm::sqrt(3.0))}); + std::optional intersectionPoint = + IntersectionTests::rayOBB(testCase.ray, testCase.obb); CHECK(glm::all(glm::lessThan( glm::abs(*intersectionPoint - *testCase.expectedIntersectionPoint), glm::dvec3(CesiumUtility::Math::Epsilon6)))); diff --git a/CesiumGltfContent/src/GltfUtilities.cpp b/CesiumGltfContent/src/GltfUtilities.cpp index 2686151af..9cfabe707 100644 --- a/CesiumGltfContent/src/GltfUtilities.cpp +++ b/CesiumGltfContent/src/GltfUtilities.cpp @@ -1373,6 +1373,12 @@ std::optional intersectRayScenePrimitive( return transformedRay.pointFromDistance(tClosest); } +std::string intersectGltfUnsupportedExtensions[] = { + ExtensionKhrDracoMeshCompression::ExtensionName, + ExtensionBufferViewExtMeshoptCompression::ExtensionName, + ExtensionExtMeshGpuInstancing::ExtensionName, + "KHR_mesh_quantization"}; + } // namespace GltfUtilities::IntersectResult GltfUtilities::intersectRayGltfModel( @@ -1380,6 +1386,19 @@ GltfUtilities::IntersectResult GltfUtilities::intersectRayGltfModel( const CesiumGltf::Model& gltf, bool cullBackFaces, const glm::dmat4x4& gltfTransform) { + // We can't currently intersect a ray with a model if the model has any funny + // business with its vertex positions or if it uses instancing. + for (const std::string& unsupportedExtension : + intersectGltfUnsupportedExtensions) { + if (gltf.isExtensionRequired(unsupportedExtension)) { + return IntersectResult{ + std::nullopt, + {fmt::format( + "Cannot intersect a ray with a glTF model with the {} extension.", + unsupportedExtension)}}; + } + } + glm::dmat4x4 rootTransform = applyRtcCenter(gltf, gltfTransform); rootTransform = applyGltfUpAxisTransform(gltf, rootTransform); diff --git a/CesiumNativeTests/src/FileAccessor.cpp b/CesiumNativeTests/src/FileAccessor.cpp index cc94e8716..b59a53963 100644 --- a/CesiumNativeTests/src/FileAccessor.cpp +++ b/CesiumNativeTests/src/FileAccessor.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -8,7 +9,6 @@ namespace CesiumNativeTests { namespace { std::unique_ptr readFileUri(const std::string& uri) { - std::vector result; CesiumAsync::HttpHeaders headers; std::string contentType; @@ -23,7 +23,8 @@ std::unique_ptr readFileUri(const std::string& uri) { if (protocolPos != 0) { return response(400); } - std::string path = uri.substr(std::strlen("file://")); + std::string path = + CesiumUtility::Uri::uriPathToNativePath(CesiumUtility::Uri::getPath(uri)); std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) { return response(404);