Trying to drap large MVT data on 3D Terrain

Hi everyone,

I’m is looking for the most performant architectural approach to drape large-scale Vector data (originally MVT) over 3D Terrain in CesiumJS.

My Core Constraints:

  1. Visuals Only (No Interaction): I strictly need this for presentation purposes. Zero feature pickability or hovering is required.

  2. Maximum Performance: Client-side framerate is our top priority.

A quick background on my work so far: First, I looked into the hybrid MapLibre + Three.js + 3d-tiles-renderer architecture (as discussed in this thread: https://community.cesium.com/t/best-practices-for-hybrid-maplibre-cesium-architecture-handling-mvt-terrain-and-complex-styling-in-production/46031/6). However, manually syncing camera matrices, handling custom WebGL contexts and so on, is just too much overhead. I’m not a Three.js master, so we had to drop that approach.

I’ve also been closely following Vector data... in glTF? · Issue #825 · CesiumGS/3d-tiles · GitHub and https://github.com/CesiumGS/cesium/pull/13404 regarding MVT support in Cesium.

Given that client-side clampToGround is too heavy for our dataset, we are shifting the workload to a server-side preprocessing pipeline. We extract our MVT data into flat GeoJSON, and currently, we are torn between two architectural plans:

Plan A: Adaptive Densification via Local GeoTIFF I built a backend-native pipeline using Node.js and a local DEM (GeoTIFF).

  • The approach: Instead of blindly densifying vertices every 5m (which bloats the file), I use a recursive midpoint checking logic (Adaptive Densification) combined with Bilinear Interpolation to avoid the “stair-case” effect.

  • The problem: It’s incredibly fast offline (~110ms), but because MVT is already a lossy/simplified format, densifying these straight segments over uneven terrain still occasionally results in messy, overlapping lines or weird geometry artifacts in short depressions.

Plan B: Server-side Rasterization (ImageryLayer) To avoid the geometry/topology mess entirely, I built an alternative pipeline.

  • The approach: I use a headless browser (Puppeteer + Canvas) to render the MVT vector tiles directly into standard PNG raster tiles. I then package these into a ZIP with a tilemapresource.xml to upload to Cesium ion as an ImageryLayer. Cesium handles the draping perfectly.

  • **The problem:**I’m constantly hitting the dreaded One or more files are not georeferenced error when uploading the TMS ZIP to Cesium ion, presumably because Ion’s parser is incredibly strict about the XML SRS and BoundingBox formats.

My questions for the community:

  1. Has anyone successfully dealt with this specific “MVT-to-Terrain” scenario in production?

  2. Is there a better “best practice” I am completely missing?

  3. Between Plan A (refining the geometry math) and Plan B (pushing through the TMS upload errors), which path would you recommend investing more engineering time into?

Any insights, tips, or pointers would be massively appreciated!

Thanks in advance :grin: .

I used a customized version of the “MVT-Headless-Renderer”, which was publicly avaialble on Github to render several million of point features across the USA.

The workflow was basically:

1 Tilie the original vector data into MVT tiles(We used Carto tiler)
2 Host the generated tiles on own server(We used GCP VMs).
3 Create a custom Cesium’s ImageryProvider that fetches raw MVT tiles and renders them as images.
It seems that MVT-Headless-Renderer was has been renamed to Mapbox-vector-tiles-basic-js-renderer.
please check https://www.npmjs.com/package/mvt-basic-render.

Hi @ZhefengJin,

Thanks for sharing, I will consider it and give it a try. :grin:

New update, I’ve been experimenting using custom ImageryProvider wrappers (via mvt-basic-render). And it’s been a bit of a headache :sad_but_relieved_face:

  • Style Spec Limitations: Many lightweight MVT renderers fail on complex Mapbox GL styles. I’ve hit AssertionErrors with standard filters like !in or nested boolean logic.
  • Interface Strictness: Modern Cesium (1.100+) is very picky about ImageryProvider inheritance. Using object literals or improper class structures will quickly trigger DeveloperError.
  • Debugging is tough: i have often stuck in a loop of errors triggered by other errors, leaving me with a clean map despite 200 OK tile responses.

Hey @zanzandora, good to see you again (just replied to your imagery tile crash in #46192 as well).

Two notes on the blockers:

ImageryProvider (just in case)

If you haven’t already, make sure you’re extending the ImageryProvider base class rather than using an object literal. As of Cesium 1.100+, POJOs will throw DeveloperError immediately. The key getters you need: tileWidth, tileHeight, maximumLevel, minimumLevel, tilingScheme, rectangle, errorEvent, hasAlphaChannel, plus a requestImage(x, y, level) method returning a canvas.

For mvt-basic-render’s style spec gaps (!in, nested booleans), that library only implements a subset of the Mapbox GL spec. You’d need to simplify filters or patch the renderer for those operators.

TMS ZIP for ion

The “not georeferenced” error is usually a missing or mismatched profile attribute in tilemapresource.xml. Make sure profile="global-geodetic" for EPSG:4326 or profile="global-mercator" for EPSG:3857, and that the BoundingBox matches the SRS. Ion is strict about that.

Recommendation

Given your constraints (visuals only, max performance, zero interaction), Plan B (server-side rasterization to TMS, then drape via ion) is the stronger path. Once the upload issue is resolved, you get clean terrain-draped imagery with no client-side vector processing. PR #13404 for native MVT is still in progress, so I wouldn’t wait on it.

Cheers!

Quick comment regarding server side raster tile rendering from mvt: rather than lightweight mvt-basic-renderer, is it not what maptiler/tileserver-gl is meant to be used for? They rely on maplibre-native to render the style set applied vector tileset.