Skip to content

feat(extensions): TerrainExtension GlobeView support#10251

Merged
chrisgervang merged 23 commits into
masterfrom
cr/feat/terrain-extension-globe
Jun 11, 2026
Merged

feat(extensions): TerrainExtension GlobeView support#10251
chrisgervang merged 23 commits into
masterfrom
cr/feat/terrain-extension-globe

Conversation

@charlieforward9

@charlieforward9 charlieforward9 commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes _TerrainExtension draped rendering work on GlobeView as well as MapView.

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 GlobeView that space is globe cartesian, which is not a stable 2D sampling basis for those textures.

Change

  • Uses an absolute WebMercator texture basis for terrain cover and height-map FBOs in both map and globe modes.
  • Adds shared projection helpers for Mercator reference bounds.
  • Updates terrain cover and height-map bounds to avoid mixing globe-cartesian viewport bounds with Mercator layer bounds.
  • Converts globe terrain positions directly to Mercator world coordinates in the shader for terrain sampling.
  • Includes only the minimal TerrainLayer/tile traversal preview fixes needed to validate the extension on GlobeView.
  • Adds a MapView/GlobeView toggle to the terrain-extension website example.

Validation

  • MapView with TerrainExtension and TerrainLayer source remains unchanged.
  • GlobeView with TerrainExtension and TerrainLayer source renders draped layers and terrain preview correctly.
  • Projection toggles no longer reuse stale terrain tiles incorrectly.
  • Focused checks cover test/modules/extensions/terrain, test/modules/geo-layers/terrain-layer.spec.ts, and test/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
TerrainExtension now supports GlobeView by 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-utils helpers (lngLatToMercatorCommon, getMercatorReferenceViewport) drive bounds for HeightMapBuilder and TerrainCover: 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: TerrainLayer uses GlobeViewport for tile overlap clamping and LNGLAT meshes when bounds look like degrees; OSM tile traversal culls tiles behind the horizon on globe. Docs note Map/Globe usage and tesselator: 'grid' for TerrainLayer on 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.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread modules/extensions/src/terrain/shader-module.ts Outdated
// Convert bounds to the common space, as [minX, minY, width, height]
// Pack bounds as [minX, minY, width, height]
bounds: bounds
? [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 felixpalmer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@charlieforward9 I've done a pass a tidied a few things:

  • Neater terrain_globe_to_mercator equations
  • More succinct comments
  • Revert removal of commonOrigin offset 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!

Comment thread examples/website/terrain-extension/app.tsx Outdated
@coveralls

coveralls commented May 12, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 83.394% (+0.008%) from 83.386% — cr/feat/terrain-extension-globe into master

@charlieforward9

Copy link
Copy Markdown
Collaborator Author

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 felixpalmer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with new globe controls, works nicely

Comment thread modules/extensions/src/terrain/terrain-cover.ts Outdated
Comment thread modules/extensions/src/terrain/height-map-builder.ts Outdated
Comment thread examples/website/terrain-extension/app.tsx Outdated
Comment thread examples/website/terrain-extension/app.tsx Outdated

@chrisgervang chrisgervang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observations as I've been reviewing this.

  • The elevation "jump" that we see at zoom level 12 when using MapView seems unrelated to this PR. It doesn't jump on GlobeView
  • I observe that the attempt to use flat lighting with material: false on the TerrainLayer isn't working on MapView at all zooms, or GlobeView when zoom less than 12. GlobeView with zoom greater than 12 does have flat lighting. Likely unrelated to this PR and something to follow up on.
  • The drape effect only supports one view, so side-by-side comparisons don't render the GeoJsonLayer. 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 on GlobeView when zoom is less than 12. Notice how the flag isn't actually offset at zoom 11.9?

Zoom 11.9
Screenshot 2026-06-10 at 3 13 00 PM

Zoom 12.1
image

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Image

Verified locally in the terrain-extension example on GlobeView.

@chrisgervang chrisgervang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested again, looks good!

iconAtlas: `${DATA_URL_BASE}/flag-icons.png`,
iconMapping: `${DATA_URL_BASE}/flag-icons.json`,
parameters: {
cullMode: 'none',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda related to #10262 @ibgreen

@chrisgervang chrisgervang merged commit 148eb40 into master Jun 11, 2026
6 checks passed
@chrisgervang chrisgervang deleted the cr/feat/terrain-extension-globe branch June 11, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TerrainLayer zooming, TerrainExtension draping [Bug] Cannot dynamically alternate between terrain ON/OFF

5 participants