Skip to content

Adding a raster overlay to a loaded tileset reloads every loaded tile, causing a visible flicker #1422

Description

@baruchInsert-tech

Summary

Adding a RasterOverlay to a Tileset that has already loaded causes every affected tile to have its content unloaded and reloaded from scratch. In a real-time application this is very visible: the geometry disappears and reappears, i.e. the map flickers.

We reproduce this with two different overlay types, via Cesium for Unity:

  • GeoJsonDocumentRasterOverlay
  • WMTS overlays (CesiumWebMapTileServiceRasterOverlay, i.e. WebMapTileServiceRasterOverlay)

It also recurs on every add rather than being a one-time cost.

Mechanism

Cesium3DTilesSelection/src/TilesetContentManager.cpp (~line 2126):

if (status.firstIndexWithMissingProjection) {
  // The mesh doesn't have the right texture coordinates for this
  // overlay's projection, so we need to kick it back to the unloaded
  // state to fix that.
  // In the future, we could add the ability to add the required
  // texture coordinates without starting over from scratch.
  unloadTileContent(tile);
  return;
}

The "missing projection" state is produced in
Cesium3DTilesSelection/src/RasterMappedTo3DTile.cpp (~line 344):

} else {
  // We don't have a precise rectangle for this projection, which means the
  // tile was loaded before we knew we needed this projection. We'll need to
  // reload the tile (later).
  ...
}

To be clear, we are not claiming the reload itself is a defect. Having read through it, it looks like a sound tradeoff: overlay texture coordinates are per-vertex glTF attributes (_CESIUMOVERLAY_n) generated by createRasterOverlayTextureCoordinates by projecting every vertex position, along with the per-projection rectangles in RasterOverlayDetails. They are produced in the load thread from the glTF, and downstream consumers do not necessarily keep the glTF afterwards -- Cesium for Unity discards it once the model has been converted to Unity meshes, retaining only the GameObject and a primitive index map. Retrofitting a new UV set onto an already-uploaded mesh would mean reconstructing that data and re-uploading to the GPU. The comment already notes the better answer as future work.

Why it is painful in practice

It recurs indefinitely. A tile only generates texture coordinates for the projections present in the collection at the moment it loads. As the camera moves and tiles stream in and out, newly loaded tiles again lack the second projection's UVs, so the next add reloads them. There is no point at which the application has "paid" the cost.

GeoJSON overlays cannot avoid it at all. GeoJsonDocumentRasterOverlay hardcodes GeographicProjection and offers no override --
CesiumVectorOverlays/src/GeoJsonDocumentRasterOverlay.cpp (~line 690):

GeoJsonDocumentRasterOverlayTileProvider(...)
    : RasterOverlayTileProvider(
          pCreator,
          parameters,
          GeographicProjection(geoJsonOptions.ellipsoid),
          projectRectangleSimple(
              GeographicProjection(geoJsonOptions.ellipsoid),
              GlobeRectangle::MAXIMUM)),

GeoJsonDocumentRasterOverlayOptions carries only defaultStyle, ellipsoid and mipLevels. Since Web Mercator is the common case for imagery overlays, a GeoJSON overlay on such a tileset is guaranteed to introduce a new projection.

WMTS overlays can avoid it, but only by accident of configuration. They do expose a projection setting, so an application that keeps every overlay on one projection is fine. Ours mixes them (some layers are configured Geographic, others use the WebMercator default), and adding a
mismatched one produces the same flicker.

Use case

Hovering an item in a UI list draws that item's footprint as a GeoJSON overlay over the base map; separately, users toggle WMTS imagery layers on and off. Both make the base map's loaded tiles reload, so the tileset visibly blinks and the interactions feel broken.

We confirmed the diagnosis by matching projections: setting our WMTS base overlay to Geographic where the service supports EPSG:4326 removes the flicker completely. That workaround is only available when the imagery service actually serves that projection.

One thing we could not confirm

We have not isolated a case where an overlay is added with a projection that is already in use by the tileset and the reload still happens. Our reading of the code says that case should leave loaded tiles untouched. It would be useful to have that confirmed, since it is the property applications would need to rely on to work around this: is adding a raster overlay whose projection already matches an existing overlay on the same tileset guaranteed not to reload tile content?

Possible resolutions (in rough order of preference)

  1. Implement the TODO already noted in the comment: add the missing texture coordinates to an
    already-loaded tile without discarding and re-fetching its content. This is the general fix and
    covers every overlay type.

  2. Allow projections to be pre-registered on the tileset (e.g. via TilesetOptions), so an
    application that knows it will later add a Geographic overlay can have tiles generate those
    texture coordinates up front. This also covers every overlay type and may be considerably simpler
    than (1).

  3. Expose the projection through GeoJsonDocumentRasterOverlayOptions, defaulting to
    GeographicProjection so existing behaviour is unchanged. This is the smallest change, and it
    closes the one case where an application currently has no way out at all. Implementation note:
    the coverage rectangle in the tile provider constructor would need to use the selected projection
    too; loadTileImage already goes through this->getProjection() and looks projection-agnostic.

Even without (1) or (2), documenting the constraint would help a great deal -- particularly the
positive guarantee that matching projections avoids the reload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions