Best Practices for Hybrid MapLibre/Cesium Architecture: Handling MVT, Terrain, and Complex Styling in Production

Hi,

I am currently working on a hybrid mapping application (MapLibre GL JS for 2D, CesiumJS for 3D) for a large-scale Villa & Resort project. Our goal is to maintain visual consistency when users switch between the 2D and 3D views.

Our Data & Setup:

  • We use custom Vector Tiles (MVT) served from our backend.

  • Our Mapbox style JSON is quite complex, utilizing match, case, and interpolate expressions for base layers (roads, water, sidewalks, boundaries).

  • We use custom Sprite atlases for POIs, trees, and labels.

  • In Cesium, we have 3D Terrain enabled.

The Problem: Initially, we wanted to reuse the MVT and Mapbox style directly in Cesium. We tried using cesium-vector-provider (which uses MapLibre under the hood to render to a canvas and drape it over Cesium as an ImageryProvider). However, this approach is not viable for our production environment due to several critical issues:

  1. Performance Bottleneck: Running MapLibre in the background to rasterize vector tiles causes severe VRAM overhead and frame drops.

  2. Distortion on Terrain: When draped over 3D terrain, text labels and POI icons get baked into the raster image. They stretch and distort along the slopes instead of behaving like proper Billboards facing the camera.

  3. Texture Stretching: At low pitch angles, the draped imagery becomes blurry/pixelated unless we generate excessively high-resolution canvases.

Our Proposed Architecture (Seeking Validation): To solve this, we are considering completely decoupling the data pipeline for the 3D mode:

  1. Base Map (Roads, Water, Grass): Move rasterization to the Server-Side. Use a tile server to consume our MVT + Style and serve standard XYZ Raster Tiles (.png / .webp) to Cesium’s UrlTemplateImageryProvider.

  2. 3D Buildings: Convert building footprints to 3D Tiles (.b3dm or .glb) via our backend pipeline.

  3. POIs & Labels: Fetch POI data via a separate JSON/GeoJSON API. Load our sprite atlas (sprite.png & sprite.json) and render them natively in Cesium using BillboardCollection (imageSubRegion) and LabelCollection so they orient correctly in 3D space.

My Questions:

  1. Is this decoupled approach (Server-side raster for base + 3D Tiles + Native Billboards) considered the industry standard for this kind of Hybrid MVT/Cesium architecture?

  2. Are there any recommended tools or pipelines for server-side rasterization that perfectly replicate Mapbox GL JS styling (especially complex expressions)?

  3. Is there any native feature on the Cesium roadmap that will handle Mapbox Vector Tiles and their styling more elegantly in the future?

Any insights, advice, or alternative approaches would be greatly appreciated. Thank you!

Hi @zanzandora!

Support for MVT is CesiumJS is indeed on the roadmap, and you can find initial work in progress here:

Additionally, we’re working to allow including vector features directly in 3D Tiles, similar to MVT:

However, support for the Mapbox Style Specification is not in progress currently, and I can’t offer an ETA on that. Cesium 3D Tiles styling will be supported on both 3D Tiles and MVT tilesets in CesiumJS, but is currently more limited in vector-specific features today, and may not cover every need in your Mapbox style JSON. We’d welcome feedback on your needs and any issues you run into, as issues on the CesiumJS repo or on 3D Tiles issue #825 above.

I would be curious which styling features (fill-color, fill-opacity, circle-radius, …) are most important to you? The logical expressions are important but to some extent that can be worked around with custom JavaScript code, where missing styling features cannot.

We’re aware that draping on terrain will be an important use case for vector data in CesiumJS, and intend to have a solution (for both MVT and 3D Tiles) that avoids pixelation/stretching on slopes. This may affect billboard and label rendering in the longer term, but for now I would just use BillboardCollection and LabelCollection as you suggest.

I don’t think I can suggest much on your proposed architecture, other than mentioning the CesiumJS roadmap goals above. If you do move rasterization server-side, I assume that reduces performance bottlenecks but does not address the distortion/stretching, correct?

Hi @donmccurdy,

Thank you so much for the detailed roadmap insights. It really helps define the technical boundaries for our project.

We have decided to completely strip out POIs, labels, and icons in our 3D environment. Therefore, billboard/label distortion on terrain is no longer a blocker for us.

I’d like to share some real-world feedback from our extensive prototyping with current workarounds, keeping in mind our strict infrastructure constraint: I only have access to the Frontend and a local Middleware pipeline. I have zero access to the Database or Backend, and can only consume the existing MVT fetch API ( I could discuss it with the PM, but that’s not very feasible… )

1. Regarding the MVT → GeoJSON → Cesium (or Ion) approach: I successfully built a Node.js pipeline to fetch our MVTs, parse them into GeoJSON, and load them into Cesium. However, it proved to be unviable for a production product for three reasons:

  • Performance Killer: The parsed GeoJSON is flat by default. Applying clampToGround: true over 3D Terrain for this massive amount of vector data caused severe performance hits and unplayable lag.

  • Style Data Loss: The transformation from MVT to GeoJSON strips away crucial properties linked to our Mapbox style (colors, complex categorical rules), making restyling natively in Cesium incredibly difficult.

  • Cesium ion Bottleneck: Uploading massive GeoJSON files to Ion introduces quota/paid tier constraints that are not optimal for our current product model.

2. Regarding the Hybrid (Client-side Canvas Draping) approach: We experimented with the client-side hybrid method (using MapLibre GL JS to render vectors to a canvas and drape it over Cesium). It was highly unsuccessful and extremely laggy, even before we loaded any 3D building models. Managing dual WebGL contexts client-side is simply too heavy.

My Proposed Direction & Seeking Your Advice: Given these severe constraints (No DB/BE access, massive lag with clampToGround GeoJSON, unplayable lag with Hybrid canvas draping), Im currently leaning toward building a Server-Side Node.js Tile Proxy. This middleware would fetch the MVT, rasterize it into PNG tiles on the fly, and serve them to Cesium’s UrlTemplateImageryProvider.

However, before we commit our resources to building and maintaining this server-side rasterization pipeline, I wanted to leave this open for your expertise.

  • Is there any other Cesium-native workaround or optimization we might have missed for draping massive flat vectors over terrain without completely tanking the framerate?

  • Or does the Tile Proxy approach sound like the only realistic path forward until native MVT support arrives?

We are incredibly excited about the native MVT support and will be keeping a close eye on PR #13404!

Thanks again for your time and guidance.

Is there any other Cesium-native workaround or optimization we might have missed for draping massive flat vectors over terrain without completely tanking the framerate?

Not today, no. Draping vectors over terrain is currently very expensive, and does not yet scale well to large datasets. We’re actively working to improve this performance in the coming months. For progress, you can track this PR:

This still leaves the Mapbox style question, of course; you would need to evaluate the feasibility of implementing your specific style needs in available APIs like Cesium3DTileStyle.


This middleware would fetch the MVT, rasterize it into PNG tiles on the fly, and serve them to Cesium’s UrlTemplateImageryProvider

I would just caution that typical server hardware is not necessarily much stronger than the client devices it’s serving to, and (unless you’re paying significantly more) may not have access to a GPU as the client device would. Server-side graphics rendering (as opposed to HTML rendering) can become very expensive, depending on the amount of traffic/users you expect. The more that you can cache the results the better, of course.

Hi @donmccurdy,

Thanks for the reality check on server costs! We’ve dropped the live server-rendering idea and are pivoting entirely to 100% static/client-side approaches. We’re currently weighing 2 paths:

Path A: Shared WebGL Context (MapLibre + Three.js) MapLibre handles the vector basemap natively. We share its WebGL context with Three.js and use the 3d-tiles-renderer to stream our massive BIM models over the map ( This is currently just a conceptual theory for me, and I am not sure if it holds up at scale). Example: https://maplibre.org/maplibre-gl-js/docs/examples/add-3d-tiles-using-threejs/#__tabbed_1_1

Path B: Cesium + Single High-Res Image We render the vector basemap into one massive, high-res image locally and upload it to Cesium ion, letting Ion handle the tiling and terrain draping. For the 3D buildings, we pre-bake the terrain heights and output static .i3dm tilesets.

Path A lets us keep our complex vector styles intact, while B lean fully into Cesium’s core strengths. Have you seen teams succeed at scale with Path A’s shared-context approach, or do you think B is the safer bet to avoid hidden WebGL bottlenecks?

Thanks again for your time!

I don’t think I have a preference between (A) and (B) myself, but perhaps others might. I believe either could work.

WebGL certainly can efficiently render vector tiles (that’s what we’re working toward as well, in CesiumJS!), so I think the performance costs you’re seeing with the background rasterization are more specific to that setup and some unknown details in the client-side MVT → MapLibre → image → CesiumJS pipeline. So MapLibre+three.js could in theory work well here, but I don’t have experience combining the two in production.

I know that manually handling the tile loading and memory management in Three.js will be a tough challenge without CesiumJS doing the heavy lifting under the hood, but sharing that single WebGL context seems like the only viable path to hit our performance goals right now.