Skip to content

feat(extensions): TerrainExtension GlobeView support#10251

Open
charlieforward9 wants to merge 15 commits into
masterfrom
cr/feat/terrain-extension-globe
Open

feat(extensions): TerrainExtension GlobeView support#10251
charlieforward9 wants to merge 15 commits into
masterfrom
cr/feat/terrain-extension-globe

Conversation

@charlieforward9
Copy link
Copy Markdown
Collaborator

@charlieforward9 charlieforward9 commented Apr 19, 2026

Summary

Makes TerrainExtension (draped rendering of layers onto a terrain surface) work on GlobeView as well as MapView.

The root bug is that the terrain cover / height-map textures are 2D render targets, but the old implementation derived their bounds and UVs from the live viewport's common space. On GlobeView that common space is sphere cartesian, which is not comparable with screen-space bounds and is not a stable 2D sampling basis. As a result, cover and height-map UV math broke on globe.

This branch keeps the terrain textures in a projection-invariant absolute Web Mercator common-space basis. The important point is that Mercator here is not being chosen as the display projection; it is the texture space of the offscreen terrain cover / height-map FBOs, which are rendered through a WebMercatorViewport in both map and globe modes.

Changes

  • projection-utils
    Added a shared Mercator reference viewport plus helpers to compute bounds in absolute Mercator common space.
  • terrain-cover, height-map-builder
    Layer bounds are projected through the Mercator reference viewport. On globe, viewport-bound intersection is skipped where it would otherwise mix sphere-cartesian viewport bounds with Mercator layer bounds.
  • shader-module
    Globe terrain positions are converted directly from globe cartesian to Mercator world coordinates in the shader, instead of going through atan/degrees/project_mercator_ round-trips. Bounds stay packed in absolute Mercator, with no commonOrigin subtract.
  • Review follow-up on preview plumbing
    Folded in the minimal TerrainLayer / tile traversal fixes needed for the example preview to render on GlobeView without bringing in the grid tesselator work from fix(geo-layers): TerrainLayer GlobeView support #10250:
    • reload tiled terrain on projection-mode changes
    • treat globe meshes as LNGLAT only when their mesh bounds are actually lng/lat, so stale MapView tiles are not misinterpreted during toggles
    • cull far-hemisphere globe tiles during traversal
  • Example app
    examples/website/terrain-extension now has a MapView / GlobeView toggle and uses the minimal globe terrain fixes above for local previewing.

Addressed Review Changes

  • Clarified in code and here why the hardcoded texture basis is Mercator: it matches the offscreen terrain FBO render space, not the on-screen view projection.
  • Replaced the globe-cartesian -> lng/lat -> project_mercator_ path with direct globe-cartesian -> Mercator math in the shader.
  • Reduced the chance of stale-tile projection mismatches during map/globe toggles by reloading tiled terrain on projection changes and guarding the globe coordinate-system switch with mesh-bounds checks.

Why

_TerrainExtension is already used for draping icons, labels, and polygons onto terrain. Extending it to GlobeView lets applications keep the same draped layers across projection toggles instead of maintaining separate map and globe code paths.

Test plan

  • MapView with TerrainExtension and TerrainLayer source -- unchanged behavior
  • GlobeView with TerrainExtension and TerrainLayer source -- draped layers and terrain preview render correctly
  • Projection toggle without invalid terrain tile reuse corrupting globe rendering
  • USE_HEIGHT_MAP (elevation offsets for icons/text) correct on both projections
  • USE_COVER (draped overlays on terrain surface) correct on both projections
  • Focused headless checks:
    • test/modules/extensions/terrain
    • test/modules/geo-layers/terrain-layer.spec.ts
    • test/modules/geo-layers/tileset-2d/utils.spec.ts

Companion PRs

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.

Copy link
Copy Markdown
Collaborator

@felixpalmer felixpalmer left a comment

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
Copy link
Copy Markdown

coveralls commented May 12, 2026

Coverage Status

coverage: 83.382% (+0.02%) from 83.366% — 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.

Copy link
Copy Markdown
Collaborator

@felixpalmer felixpalmer left a comment

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

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

4 participants