Hi, thanks for opening this thread — we have a slightly unusual workflow that touches CRS support from an angle I don’t see mentioned above.
Our use case
We render a custom globe built from glTF (.glb) models rather than 3D Tiles or the built-in terrain/imagery pipeline. Geometry is generated server-side by our own C++ backend (much faster than doing it in JS, and gives us full control over LOD and tiling strategy), packaged as .glb, and streamed to CesiumJS which loads them as Cesium.Model instances.
We ship many .glb files, some of them very large — a single .glb typically holds one primitive with hundreds of thousands of triangles spanning the entire globe. The .glb path gives us the performance we need for this volume: binary vertex buffers, one draw call per file, no per-frame JS work. We drive per-vertex attributes and per-frame animation (movement, appearance changes) entirely through CustomShader uniforms, so the geometry itself is uploaded once and only the uniforms change frame to frame. We even render labels through .glb — the text is baked into textures on the C++ side and drawn as textured quads,
which turned out to be significantly faster than any DOM/canvas-based labeling approach we tried.
Each .glb is stored in identity model matrix. Today, our vertices carry world-space Cartesian x/y/z, and our custom vertex shader converts back to lat/lon/alt (via asin(normal.y) / atan(normal.x, normal.z)) before applying the projection. Ideally we’d store lat/lon/alt directly in the vertex buffers to skip that conversion entirely, but we haven’t tried yet because we’re not sure the CesiumJS Model pipeline (bounding volume computation, culling, picking, LOD) would accept vertices in a non-Cartesian coordinate space — the “identity model matrix” contract seems to assume Cartesian. If anyone from the Cesium team
can confirm whether the Model pipeline would tolerate arbitrary vertex semantics as long as the vertex shader emits proper clip-space positions, that would already unlock a nice optimization for us. The projection itself is then applied entirely on the GPU inside a custom vertex shader we inject via CustomShader. This lets us switch projection instantly (3D sphere / 2D / Columbus View / custom LCC / Web Mercator) without re-uploading or re-tessellating any geometry — the GPU re-projects every frame.
In production we use a spherical Lambert Conformal Conic projection (LCC with +lat_1=10 +lat_2=50 +lat_0=30 +lon_0=20, forced spherical with +a=+b=sphereRadius so it matches our shader’s spherical LCC formula exactly). Cesium’s CPU-side Proj4Projection handles the projection for the GeoJSON entities and the entity layer, while the GPU shader handles the same projection for our custom glTF geometry — both share the same sphere radius via a uniform so the two rendering paths stay pixel-aligned.
We took this route because 3D Tiles didn’t fit our constraints: we needed direct control over the vertex format, we didn’t want to bake geometry per-projection, and the server-side generation gives us much better throughput than doing tessellation in the browser.
The friction
The projection math has to run in the vertex shader, so the shader needs the current projection’s parameters as uniforms. Today, CesiumJS exposes very little about the active MapProjection to shaders: for GeographicProjection and WebMercatorProjection, everything is hardcoded in the automatic uniforms and GLSL functions; for anything else, there’s no API for a MapProjection implementation to publish its parameters to the shader side.
We ended up precomputing the projection constants JS-side (for our Lambert case: the spherical LCC exponent N, scale factor F, K0, false easting/northing…) and uploading them as CustomShader uniforms every time we build a shader. It works, but it’s tightly coupled — our CustomShader factory has to know about every projection type we support and how to unpack it into vec4-ish uniforms. Adding a projection means editing both the JS uniform packer AND the GLSL shader in lockstep. And every consumer has to reimplement the projection math in GLSL from scratch — no shared reference.
Related upstream work
We contributed the custom projection classes themselves in Add support of Proj4JS & custom map projections by mickae1 · Pull Request #13471 · CesiumGS/cesium · GitHub (Proj4Projection, CustomProjection, Matrix4Projection — adapted from @likangning93’s PR #7502). That PR already adds five GLSL automatic uniforms (czm_mapProjectionType, czm_projectionParams, czm_projectionOffsets, czm_projectionEllipsoidParams, czm_projectionEllipsoidParams2) that publish the active projection’s parameters to the shader side — exactly the kind of API we need for our workflow.
What would help us most
A stable, documented contract by which a MapProjection publishes its parameters to shaders, so any custom projection (ours or a third-party’s) can be applied on the GPU without every consumer inventing their own uniform packing scheme. The uniforms in #13471 are a good starting point but they’re currently sized for a small set of projections. Something like MapProjection.getShaderUniforms(): { [name: string]:
UniformValue } that the CustomShader pipeline picks up automatically, plus a companion GLSL include (czm_projection.glsl or similar) with reference implementations of project(lat, lon, altitude) for the built-in projections (including LCC), so users doing GPU-side projection don’t have to reimplement Snyder from scratch.
Priority
For us this is significant complexity, not blocking — we’ve built around it. But every new projection we want to support means duplicated work on both the JS and GLSL sides. A first-class API would let us delete a fair amount of glue code and would probably encourage other users to move projection work to the GPU where it belongs for large scenes.
Happy to elaborate on any of this or share code snippets if useful.
This post was drafted with the help of Claude to synthesize the work we’ve done on this workflow.