Skip to content

Add VectorTilesRasterOverlay#1365

Draft
azrogers wants to merge 22 commits into
mainfrom
vector-tiles-overlay
Draft

Add VectorTilesRasterOverlay#1365
azrogers wants to merge 22 commits into
mainfrom
vector-tiles-overlay

Conversation

@azrogers
Copy link
Copy Markdown
Contributor

@azrogers azrogers commented May 11, 2026

This PR adds support for rendering vector tilesets as a raster overlay. It uses a rasterization approach similar to the existing GeoJsonDocumentRasterOverlay, but instead of working from a single document, it works from a 3D Tiles tileset.

There are many things that need to get done before this PR is final:

  • There will be a number of extensions related to vector 3D Tiles tilesets that we need to add support for.
  • Polygons should be supported. ✅Should work as long as they have EXT_mesh_polygon.
  • We need to find a home for VectorTilesRasterOverlay that isn't in Cesium3DTilesSelection. It can't be in CesiumRasterOverlays as it currently uses the internal TilesetContentManager in Cesium3DTilesSelection.
  • We want to implement a "partial selection" optimization that takes advantage of the fact that selection will almost always only happen by going "up" or "down" from previously selected tiles in predictable ways. Doing a full selection every time a tile is rasterized is a waste.
  • Need to add tests for points similar to those that exist for polylines and polygons.

Also for the future:

  • How do we handle styling here? A single default style for the entire tileset only gets us so far. In the GeoJsonDocumentRasterOverlay we got away with letting users customize the styles on elements directly in the document before the raster overlay was loaded, so users can accomplish any kind of metadata styling or other sort of styling they're looking for. But we'll need a different approach for a 3D Tiles tileset. Is it finally time to handle style expressions? (Support for 3D Tiles Styling language #1208)

@azrogers azrogers marked this pull request as draft May 11, 2026 16:43
@j9liu j9liu requested review from Copilot and kring and removed request for Copilot May 12, 2026 15:11
@j9liu
Copy link
Copy Markdown
Contributor

j9liu commented May 12, 2026

Hey @kring! Are you able to take a glance from an architectural level for this PR?

@kring
Copy link
Copy Markdown
Member

kring commented May 18, 2026

If I can try to summarize how this works...

Every time we need a raster image for a terrain tile, we call findAndLoadTiles. It returns a future that eventually resolves to the vector tiles that need to be rendered, once all such tiles have been completely loaded.

findAndLoadTiles does a full traversal of the vector tile tree from the root. If any selected tiles still need to be loaded, then it calls itself recursively after the future returned by the first invocation resolves.

The futures returned by findAndLoadTiles are resolved by the tick method. This is necessary because we don't currently have a future that is resolved when a tile finishes loading. (In the normal SSE-based selection, we essentially poll for completion instead.)

The tick method is called every frame. It loops over all tiles that have a load in flight, and for each that has completed, it loops over all the "requests" (one per raster tile that is loading) to see if that tile uses that now-completed tile. Once a raster tile has no vector tile loads remaining, the request's promise is resolved. That will trigger another findAndLoadTiles (a full traversal) to make sure that there aren't now new tiles that we need to wait for.

This is potentially a lot of traversals. For vector tilesets with a small number of tiles, it probably isn't too bad. But I think it'll get pretty expensive for bigger ones.

Another more subtle problem is that a single call to loadTileImage can, in a tileset that uses lots of external tilesets or implicit subtrees, trigger a significant amount of loading. This loading is required in order to render the tile, so that's fine. Except that this loading process, once set in motion, will continue to completion even if the viewer moved away and this raster tile isn't even needed anymore.

I don't have any easy solution to this. Probably the ideal approach is more polling-based and incremental. That is, perhaps tick could a) check which (relevant) raster tiles are still in flight and advanced them by loading additional vector tiles if necessary, and b) do incremental traversals starting from a known point instead of traversing the entire vector tile tree from the root each time. It's not simple, though. (A) works against our raster overlay system, unfortunately. There's no way to "complete" a raster image load other than by providing the final image or failing it forever.

So I'd say it's reasonable to move forward with the current approach, and just keep an eye on how much time we're spending in this process as users start to work with larger vector tilesets.

Copy link
Copy Markdown
Member

@kring kring left a comment

Choose a reason for hiding this comment

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

Some smaller comments below.

*/
size_t size() const noexcept;

void tick() noexcept;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't appear to be used. The Tileset just calls ActivateRasterOverlay::tick directly.


void tick();

bool isTickable() const noexcept;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not used, other than by the ticking system in RasterOverlayCollection, which is itself unused.

// part 1 - selecting from scratch
if (this->_pTilesetContentManager->getRootTile() == nullptr) {
return this->_pTilesetContentManager->getRootTileAvailableEvent()
.thenInWorkerThread([this,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two problems here:

  1. I think the root tile available event can resolve while the root tile is still nullptr, e.g., if there's an error. If it does, this will be an endless promise cycle.
  2. It's not valid to call loadTileImageFromTileset in a worker thread, because it accesses the TilesetContentManager.

Comment thread CesiumVectorOverlays/include/CesiumVectorOverlays/VectorTilesRasterOverlay.h Outdated
nullptr};

this->_pTilesetContentManager =
TilesetContentManager::createFromUrl(externals, TilesetOptions{}, url);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if we had a single tileset that had both normal meshes and vector data? For example, what if Google Photorealistic 3D Tiles also contained vector roads or points of interest?
I think in that case it would be really important to have just a single TilesetContentManager. Otherwise, we'd end up loading every tile twice.

// external content and now we need to check their children.
return this
->addLoadRequest(this->getAsyncSystem(), loadInfo.tileLoadTasks)
.thenImmediately([this, tileRectangle, textureSize]() mutable {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can't call findAndLoadTiles anywhere but the main thread, so this should be thenInMainThread.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is an interesting possibility here... rather than strictly running the selection algorithm in the main thread, it is theoretically possible to run it in a worker thread instead. However, in that case, we need to be very careful that it's always called from the same worker thread. We could do that with a thread pool with one thread, as we do with the request cache. Also, this will break down if a VectorTilesRasterOverlay ever uses a Tileset that is also used for normal rendering.

CesiumAsync::Future<void> future = request.promise.getFuture();

std::scoped_lock<std::mutex> lock(
this->_pSharedTileSelectionState->loadRequestsMutex);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I can't see any reason to use a mutex. All accesses of loadRequests are (and must be) on the main thread.

*/
VectorTilesRasterOverlay(
const std::string& name,
const TilesetSource& source,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd suggest an overload rather than a variant here.


#include "RasterOverlayUpsampler.h"

#include <Cesium3DTilesSelection/CesiumIonTilesetContentLoaderFactory.h>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer not to make TilesetContentManager part of the public API. Almost everything you need it for can also be achieved through Tileset. Expand and refactor that class, if necessary.

I guess it's worth considering what is the actual separation of concerns here. Originally, there was no TilesetContentManager; everything was in Tileset. When Bao introduced the TilesetContentManager, he described it like this:

Previously Tileset and Tile are used to set the state machine of Tile. Now only TilesetContentManager will take care of that. Its responsibilities consists of loading tile content (by using TilesetContentLoader), managing tile loading state machine, and unloading tile. No ones except the TilesetContentManager can set the state of the tile now

So the content manager handled tile content state transitions (loading/unloading, basically) and the Tileset handled the rest. Where "the rest" was mostly the selection algorithm and the public entry points to the content manager.

Now with #1342, we're talking about moving the selection algorith out of Tileset, too.

What's left? Not much I guess, but I think it's still serving a useful purpose as the public API representation of a Tileset, even if almost everything it does is delegated. By exposing TilesetContentManager, we a) need to give a lot more thought to its public API, because very little has gone into it, and b) are committing to maintaining it (yes, our deprecation policy doesn't strictly require we avoid breakage, but we still should avoid it wherever possibe).


struct LoadRequest {
CesiumAsync::Promise<void> promise;
std::set<Tile*> requestedTiles;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it might be better to use a sorted std::vector<Tile*> here instead of a std::set. Similar time complexity, far fewer memory allocations and better cache locality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants