feat(extensions): TerrainExtension GlobeView support#10251
Conversation
Make TerrainExtension's draped rendering (height-map and terrain-cover FBOs) work on GlobeView as well as MapView. The existing implementation packed layer bounds into the live viewport's common space, which on GlobeView is sphere cartesian — incomparable with screen-space bounds, and invalid as a UV basis for the draped sampler. All bounds are now expressed in ABSOLUTE Web Mercator common space, regardless of the active projection. The FBO is rendered via a WebMercatorViewport in both cases, so the cover/height-map texture is projection-invariant and a projection toggle no longer requires re-rendering. Changes: - projection-utils: new `MERCATOR_REFERENCE_VIEWPORT` plus helpers `lngLatToMercatorCommon` and `getMercatorReferenceViewport` so other modules can compute bounds in the shared absolute-Mercator basis. - terrain-cover, height-map-builder: project layer bounds through the Mercator reference viewport; on globe, skip the viewport-bounds intersection in `getRenderBounds` (which would yield sphere cartesian coords) and use full layer bounds. - shader-module: compute `terrainMercPos` per-fragment by unprojecting globe cartesian back to lng/lat and forward-projecting through project_mercator_, so USE_HEIGHT_MAP / USE_COVER samples against the absolute-Mercator bounds uniform on any projection. The project module's helpers are VS-only, so the value is passed via varying; terrain meshes are fine enough that interpolation error is negligible. Bounds uniform is packed as absolute Mercator (no commonOrigin subtract), since the shader computes absolute xy itself. Example updated with a GlobeView toggle so the terrain-extension demo exercises both projections (requires TerrainLayer's grid tesselator from the companion geo-layers PR. EOF )
| if (terrain.mode == TERRAIN_MODE_USE_HEIGHT_MAP) { | ||
| vec3 anchor = geometry.worldPosition; | ||
| anchor.z = 0.0; | ||
| vec3 anchorCommon = project_position(anchor); |
There was a problem hiding this comment.
OK, so if I understand correctly you're basically forcing project_position() to internally take the following branch:
if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) {A natural question that comes to mind is that if we are going to hardcode a projection, then why Mercator? Sure it works well for MapView but it doesn't fit the other views.
There was a problem hiding this comment.
Perhaps related, I have been feeling that if we extend layers to take a "deformation" when generating geometry, such as a deformation UV grid for BitmapLayer than we could generalize that to support any projection.
| // Convert bounds to the common space, as [minX, minY, width, height] | ||
| // Pack bounds as [minX, minY, width, height] | ||
| bounds: bounds | ||
| ? [ |
There was a problem hiding this comment.
I worry this will introduce precision issues
| const targetLayer = this.targetLayer; | ||
| let shouldRedraw = false; | ||
|
|
||
| // Bounds are computed in ABSOLUTE Mercator common space — NOT the live |
There was a problem hiding this comment.
Could we try to use a cartesian space? Seems like a closer match to the spherical coordinates
| // Recalculate cached bounds | ||
| // Recalculate cached bounds. | ||
| // Use a Mercator reference viewport so layer bounds live in ABSOLUTE | ||
| // Mercator common space — same rationale as terrain-cover.ts. On |
There was a problem hiding this comment.
Should we add mercator common space as a supported coordinateSystem?
Something like first class support for EPSG: 3857?
Or that is not helpful?
| if (terrain.mode == TERRAIN_MODE_USE_HEIGHT_MAP) { | ||
| vec3 anchor = geometry.worldPosition; | ||
| anchor.z = 0.0; | ||
| vec3 anchorCommon = project_position(anchor); |
There was a problem hiding this comment.
Perhaps related, I have been feeling that if we extend layers to take a "deformation" when generating geometry, such as a deformation UV grid for BitmapLayer than we could generalize that to support any projection.
felixpalmer
left a comment
There was a problem hiding this comment.
@charlieforward9 I've done a pass a tidied a few things:
- Neater
terrain_globe_to_mercatorequations - More succinct comments
- Revert removal of
commonOriginoffset in bounds. This way the existing behavior isn't changed, only globeview support is added
Could you check that I haven't broken anything? Otherwise looks good to merge!
|
Confirmed locally: the TerrainExtension example loads tiles with the Mapbox token, and the View selector is now in the example controls. CI is green. |
felixpalmer
left a comment
There was a problem hiding this comment.
Tested with new globe controls, works nicely
…xtension-globe # Conflicts: # modules/geo-layers/src/terrain-layer/terrain-layer.ts
chrisgervang
left a comment
There was a problem hiding this comment.
Observations as I've been reviewing this.
- The elevation "jump" that we see at zoom level 12 when using
MapViewseems unrelated to this PR. It doesn't jump onGlobeView - I observe that the attempt to use flat lighting with
material: falseon theTerrainLayerisn't working onMapViewat all zooms, orGlobeViewwhen zoom less than 12.GlobeViewwith zoom greater than 12 does have flat lighting. Likely unrelated to this PR and something to follow up on. - The
drapeeffect only supports one view, so side-by-side comparisons don't render theGeoJsonLayer. This is a preexisting limitation
Remaining Issue: the offset isn't working on GlobeView when zoom is less than 12.
parameters: { depthCompare: 'always' }is obfuscating a bug onGlobeViewwhen zoom is less than 12. Notice how the flag isn't actually offset at zoom 11.9?
Proposed fix left in comments
| float terrainZ = texture(terrain_map, texCoords).r; | ||
| geometry.position.z += terrainZ; | ||
| if (project.projectionMode == PROJECTION_MODE_GLOBE) { | ||
| geometry.position.xyz += normalize(geometry.position.xyz) * terrainZ; |
There was a problem hiding this comment.
| geometry.position.xyz += normalize(geometry.position.xyz) * terrainZ; | |
| // Height map is written in Mercator common space (units = TILE_SIZE / EARTH_CIRCUMFERENCE / cos(lat)) | |
| // Convert to globe radial units (units = GLOBE_RADIUS / EARTH_RADIUS) | |
| terrainZ *= cos(radians(geometry.worldPosition.y)) * PI; | |
| geometry.position.xyz += normalize(geometry.position.xyz) * terrainZ; |
Result
Verified locally in the terrain-extension example on GlobeView.
chrisgervang
left a comment
There was a problem hiding this comment.
Tested again, looks good!
| iconAtlas: `${DATA_URL_BASE}/flag-icons.png`, | ||
| iconMapping: `${DATA_URL_BASE}/flag-icons.json`, | ||
| parameters: { | ||
| cullMode: 'none', |
There was a problem hiding this comment.
General commentary: It's not obvious that billboarded layers need cullMode: 'none' to work on GlobeView.
I think it'd be good practice to add a remark on layer docs that need this until deck offers an automatic solution.


Summary
Makes
_TerrainExtensiondraped rendering work onGlobeViewas well asMapView.Why
Terrain cover and height-map textures are 2D render targets. The previous implementation derived their bounds and UVs from the live viewport common space; on
GlobeViewthat space is globe cartesian, which is not a stable 2D sampling basis for those textures.Change
Validation
TerrainExtensionandTerrainLayersource remains unchanged.TerrainExtensionandTerrainLayersource renders draped layers and terrain preview correctly.test/modules/extensions/terrain,test/modules/geo-layers/terrain-layer.spec.ts, andtest/modules/geo-layers/tileset-2d/utils.spec.ts.Merge Notes
Approved by Felix after follow-up cleanup. #10250 remains the broader TerrainLayer globe grid/tessellator work; this PR intentionally keeps only the minimal preview plumbing needed for the extension change.
Note
Medium Risk
Touches GPU terrain shaders, FBO coordinate logic, and tiled terrain on globe; MapView behavior is intended to stay the same but projection switching and elevation sampling are easy to get wrong.
Overview
TerrainExtensionnow supportsGlobeViewby building terrain height-map and cover FBOs in a fixed Web Mercator texture space instead of the live viewport’s globe cartesian common space, so draped/offset layers can sample the same targets when switching between map and globe.Shared
projection-utilshelpers (lngLatToMercatorCommon,getMercatorReferenceViewport) drive bounds forHeightMapBuilderandTerrainCover: on globe, layer bounds stay Mercator and viewport intersection is skipped where globe bounds would be invalid. The terrain shader maps globe positions to Mercator for UVs, writes radial height on the globe, and applies sampled elevation along the surface normal.Supporting globe validation:
TerrainLayerusesGlobeViewportfor tile overlap clamping andLNGLATmeshes when bounds look like degrees; OSM tile traversal culls tiles behind the horizon on globe. Docs note Map/Globe usage andtesselator: 'grid'forTerrainLayeron globe; the terrain-extension example adds a MapView/GlobeView toggle and related render tweaks.Reviewed by Cursor Bugbot for commit c520bde. Bugbot is set up for automated code reviews on this repo. Configure here.