Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

### ? - ?

##### Additions :tada:

- Added `Tileset::sampleHeightCurrentDetail` to synchonously sample heights from the currently loaded tiles.

### v0.59.0 - 2026-03-31

##### Breaking Changes :mega:
Expand Down
34 changes: 34 additions & 0 deletions Cesium3DTilesSelection/include/Cesium3DTilesSelection/Tileset.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include <list>
#include <memory>
#include <optional>
#include <string>
#include <vector>

Expand Down Expand Up @@ -365,6 +366,39 @@ class CESIUM3DTILESSELECTION_API Tileset final {
CesiumAsync::Future<SampleHeightResult> sampleHeightMostDetailed(
const std::vector<CesiumGeospatial::Cartographic>& positions);

/**
* @brief Samples the height of this tileset at a list of cartographic
* positions using only currently-loaded tile content.
*
* Unlike {@link sampleHeightMostDetailed}, this method does not trigger any
* tile loads and returns immediately. It uses whatever is the most detailed
* LOD that is currently loaded in memory, which may not be the most detailed
* LOD available in the tileset.
*
* 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.
*
* @param positions The positions for which to sample heights.
* @return The result of the height query.
*/
SampleHeightResult sampleHeightCurrentDetail(
const std::vector<CesiumGeospatial::Cartographic>& positions) const;

/**
* @brief Samples the height of this tileset at a single cartographic
* position using only currently-loaded tile content.
*
* This is a convenience overload of
* {@link sampleHeightCurrentDetail(const std::vector<CesiumGeospatial::Cartographic>&) const}.
*
* @param position The position for which to sample height.
* @return The sampled height in meters above the ellipsoid, or std::nullopt
* if no loaded tile covers the position.
*/
std::optional<double> sampleHeightCurrentDetail(
const CesiumGeospatial::Cartographic& position) const;

/**
* @brief Gets the default view group that is used when calling
* {@link updateView}.
Expand Down
51 changes: 51 additions & 0 deletions Cesium3DTilesSelection/src/Tileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,57 @@ Tileset::sampleHeightMostDetailed(const std::vector<Cartographic>& positions) {
return promise.getFuture();
}

SampleHeightResult Tileset::sampleHeightCurrentDetail(
const std::vector<Cartographic>& positions) const {
SampleHeightResult results;
if (positions.empty()) {
return results;
}

results.positions.resize(positions.size(), Cartographic(0.0, 0.0, 0.0));
results.sampleSuccess.resize(positions.size(), false);

const Tile* pRootTile = this->_pTilesetContentManager->getRootTile();
if (!pRootTile) {
results.warnings.emplace_back(
"Height sampling could not complete because the tileset root tile is "
"not available.");
for (size_t i = 0; i < positions.size(); ++i) {
results.positions[i] = positions[i];
}
return results;
}

const Ellipsoid& ellipsoid = this->_options.ellipsoid;

for (size_t i = 0; i < positions.size(); ++i) {
TilesetHeightQuery query(positions[i], ellipsoid);
query.findLoadedCandidateTiles(
const_cast<Tile*>(pRootTile),
results.warnings);
query.intersectCandidateTiles(results.warnings);

results.positions[i] = positions[i];
std::optional<double> height = query.getHeightFromIntersection();
results.sampleSuccess[i] = height.has_value();
if (height.has_value()) {
results.positions[i].height = *height;
}
}

return results;
}

std::optional<double>
Tileset::sampleHeightCurrentDetail(const Cartographic& position) const {
SampleHeightResult result =
this->sampleHeightCurrentDetail(std::vector<Cartographic>{position});
if (!result.sampleSuccess.empty() && result.sampleSuccess[0]) {
return result.positions[0].height;
}
return std::nullopt;
}

TilesetViewGroup& Tileset::getDefaultViewGroup() {
return this->_defaultViewGroup;
}
Expand Down
119 changes: 106 additions & 13 deletions Cesium3DTilesSelection/src/TilesetHeightQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,107 @@ void TilesetHeightQuery::findCandidateTiles(
}
}

namespace {
bool tileHasRenderContent(const Tile& tile) {
return tile.getState() >= TileLoadState::ContentLoaded &&
tile.getContent().getRenderContent() != nullptr;
}
} // namespace

void TilesetHeightQuery::findLoadedCandidateTiles(
Tile* pTile,
std::vector<std::string>& warnings) {
if (pTile->getState() == TileLoadState::Failed) {
warnings.emplace_back("Tile load failed during query. Ignoring.");
return;
}

const std::optional<BoundingVolume>& contentBoundingVolume =
pTile->getContentBoundingVolume();

// Recurse into children whose bounding volumes intersect the ray,
// tracking whether any descendant became a candidate. We recurse even
// into children that are not yet loaded, because deeper descendants
// may be loaded.
bool anyDescendantCandidate = false;
if (!pTile->getChildren().empty()) {
for (Tile& child : pTile->getChildren()) {
if (!boundingVolumeContainsCoordinate(
child.getBoundingVolume(),
this->ray,
this->inputPosition,
this->ellipsoid))
continue;

size_t prevCount =
this->candidateTiles.size() + this->additiveCandidateTiles.size();
findLoadedCandidateTiles(&child, warnings);
if (this->candidateTiles.size() + this->additiveCandidateTiles.size() >
prevCount) {
anyDescendantCandidate = true;
}
}
}

bool isLeaf = pTile->getChildren().empty();

// For additive refinement, this tile is always a candidate alongside
// children.
if (!isLeaf && pTile->getRefine() == TileRefine::Add &&
tileHasRenderContent(*pTile)) {
if (contentBoundingVolume) {
if (boundingVolumeContainsCoordinate(
*contentBoundingVolume,
this->ray,
this->inputPosition,
this->ellipsoid)) {
this->additiveCandidateTiles.emplace_back(pTile);
}
} else {
this->additiveCandidateTiles.emplace_back(pTile);
}
}

// Use this tile as a leaf candidate if:
// - It is actually a leaf, OR
// - No descendant was found as a candidate AND this is not an
// additively-refined tile (those are already in additiveCandidateTiles)
// In either case, the tile must have renderable content.
if ((isLeaf ||
(!anyDescendantCandidate && pTile->getRefine() != TileRefine::Add)) &&
tileHasRenderContent(*pTile)) {
if (contentBoundingVolume) {
if (boundingVolumeContainsCoordinate(
*contentBoundingVolume,
this->ray,
this->inputPosition,
this->ellipsoid)) {
this->candidateTiles.emplace_back(pTile);
}
} else {
this->candidateTiles.emplace_back(pTile);
}
}
}

void TilesetHeightQuery::intersectCandidateTiles(
std::vector<std::string>& outWarnings) {
for (const Tile::Pointer& pTile : this->additiveCandidateTiles) {
this->intersectVisibleTile(pTile.get(), outWarnings);
}
for (const Tile::Pointer& pTile : this->candidateTiles) {
this->intersectVisibleTile(pTile.get(), outWarnings);
}
}

std::optional<double> TilesetHeightQuery::getHeightFromIntersection() const {
if (!this->intersection.has_value()) {
return std::nullopt;
}
return this->ellipsoid.getMaximumRadius() * rayOriginHeightFraction -
glm::sqrt(this->intersection->rayToWorldPointDistanceSq);
}

TilesetHeightRequest::TilesetHeightRequest(
std::vector<TilesetHeightQuery>&& queries_,
const CesiumAsync::Promise<SampleHeightResult>& promise_) noexcept
Expand Down Expand Up @@ -381,12 +482,7 @@ bool TilesetHeightRequest::tryCompleteHeightRequest(

// Do the intersect tests
for (TilesetHeightQuery& query : this->queries) {
for (const Tile::Pointer& pTile : query.additiveCandidateTiles) {
query.intersectVisibleTile(pTile.get(), warnings);
}
for (const Tile::Pointer& pTile : query.candidateTiles) {
query.intersectVisibleTile(pTile.get(), warnings);
}
query.intersectCandidateTiles(warnings);
}

// All rays are done, create results
Expand All @@ -402,14 +498,11 @@ bool TilesetHeightRequest::tryCompleteHeightRequest(
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);
std::optional<double> height = query.getHeightFromIntersection();
results.sampleSuccess[i] = height.has_value();
if (height.has_value()) {
results.positions[i].height = *height;
}
}

Expand Down
33 changes: 33 additions & 0 deletions Cesium3DTilesSelection/src/TilesetHeightQuery.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <CesiumUtility/IntrusivePointer.h>

#include <list>
#include <optional>
#include <set>
#include <string>
#include <vector>
Expand Down Expand Up @@ -103,6 +104,38 @@ class TilesetHeightQuery {
* candidate search.
*/
void findCandidateTiles(Tile* pTile, std::vector<std::string>& outWarnings);

/**
* @brief Find candidate tiles using only currently-loaded tiles.
*
* Like {@link findCandidateTiles}, but only considers tiles that already have
* renderable content loaded. If a tile's children are not loaded, the tile
* itself is used as a candidate (if it has renderable content), rather than
* waiting for children to load.
*
* @param pTile The tile at which to start traversal.
* @param outWarnings On return, reports any warnings that occurred during
* candidate search.
*/
void
findLoadedCandidateTiles(Tile* pTile, std::vector<std::string>& outWarnings);

/**
* @brief Intersect the ray with all current candidate tiles (both additive
* and regular).
*
* @param outWarnings On return, reports any warnings that occurred during
* intersection testing.
*/
void intersectCandidateTiles(std::vector<std::string>& outWarnings);

/**
* @brief Compute the sampled height from the current intersection, if any.
*
* @return The height above the ellipsoid, or std::nullopt if no intersection
* exists.
*/
std::optional<double> getHeightFromIntersection() const;
};

/**
Expand Down
Loading
Loading