Skip to content

feat(core): Add getZoomFromElevation and getElevationFromZoom utility functions#10015

Closed
Copilot wants to merge 3 commits into
masterfrom
copilot/fix-layer-drawing-issue
Closed

feat(core): Add getZoomFromElevation and getElevationFromZoom utility functions#10015
Copilot wants to merge 3 commits into
masterfrom
copilot/fix-layer-drawing-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 20, 2026

Users working with 3D tilesets outside of deck.gl need to construct a WebMercatorViewport from a physical camera position (lat/lng/elevation in meters), but the API only accepts zoom — requiring manual derivation of the conversion formula.

Change List

  • New export getZoomFromElevation(options) — converts a physical camera elevation (meters) to the zoom level for WebMercatorViewport. Supports altitude, fovy, pitch, and height to match the viewport configuration.
  • New export getElevationFromZoom(options) — inverse of the above; returns the camera elevation in meters for a given zoom level.
  • Both are exported from @deck.gl/core and deck.gl.
import {WebMercatorViewport, getZoomFromElevation} from '@deck.gl/core';

// Camera at 5000m elevation, pitch=0
const zoom = getZoomFromElevation({elevation: 5000, latitude: 37.8, height: 600});

const viewport = new WebMercatorViewport({latitude: 37.8, longitude: -122.4, zoom, width: 800, height: 600});
// Pass viewport to Tileset3D for correct LOD/SSE/frustum computation

Formula: zoom = getMeterZoom({latitude}) + log2(altitudeRatio × cos(pitch) × height / elevation)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/repos/visgl/deck.gl/discussions/9743
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (http block)

If you need me to access, download, or install something from one of these locations, you can either:


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Fix layer drawing issue in deck.gl feat(core): Add getZoomFromElevation and getElevationFromZoom utility functions Feb 20, 2026
@charlieforward9
Copy link
Copy Markdown
Collaborator

Closes #9743

@charlieforward9
Copy link
Copy Markdown
Collaborator

@andreasplesch

@felixpalmer
Copy link
Copy Markdown
Collaborator

I think this code would be better placed in the @math.gl/web-mercator module, as it just provides some extra math on top of the functions exposed there

@andreasplesch
Copy link
Copy Markdown

I implemented the suggested new getZoomFromElevation function and was able to test it a bit with a simple 3D tiles scene. The function invoked with the default altitude(ratio) of 1.5 generates usable zoom levels for WebMercatorViewport which lead to more or less expected tile selections when navigating the scene.

I noticed, however, that supplying the actual fovy (vertical field of view angle) which in my framework by default is only 45 degrees, has an adverse impact.

In this case altitudeRatio is calculated from the fovy by the fovyToAltitude function:
https://github.com/visgl/math.gl/blob/64f7fd537b20113ad528fea4169215590d6053dd/modules/web-mercator/src/web-mercator-utils.ts#L409

0.5 / Math.tan(0.5 * fovy * DEGREES_TO_RADIANS);

This comes out for 45 deg. fovy as about 1.2.
The default altitude of 1.5 corresponds to a 0.5 * fovy = atan(1/3) or a fovy of 36 degrees which seems strangely small.

altitudeRatio (altitude) is used by the getZoomFromElevation function as a factor inside a logarithm which can be written as the sum of logs. As an example, the change in zoom by changing the altitudeRatio from 1.5 to 1.2 is between 0.584962501 and 0.263034406, so about 0.3 which seems not dramatic but could be significant. I will investigate further.

I also calculate pitch correctly for each frame but sofar did not use it for the zoom calculation. The default pitch is 0, which has the maximum cos(pitch) as used in the formula (of 1, and log2(1)=0). Larger pitch means smaller cos(pitch) and more negative log2(cos(pitch)), for example log2(cos(60degrees))=-1. Larger pitch means smaller zoom. It looks like this is meant to take into account the larger distance along the view to the ground with larger pitch.

Another, perhaps unrelated, observation is that there can be NaN exceptions during updates of the tileset when navigating freely, for example when dipping below the ground level.

@andreasplesch
Copy link
Copy Markdown

andreasplesch commented Mar 19, 2026

Further testing revealed that using the actual pitch was important and helped with using the actual fovy. So this seems resolved.

The NaN errors were due to pitches > 90,, eg. looking upwards. Looking upwards may not be common in deck.gl but will be in any first person view navigation. In the formula pitches > 90 lead to cos(pitch) < 0 which is not allowed for logarithms.

Not sure what the proper solution would be but just taking the absolute value of the log argument seems to work well.

This also takes care of the elevation < 0 case, eg. when dipping beneath the surface. There should be a proper solution as well.

Elevation = 0 leads to infinity and elevation close to 0 leads to large numbers which may not be intended. Just clipping at 1 solves this and behaves well but is probably incorrect.

@andreasplesch
Copy link
Copy Markdown

andreasplesch commented May 13, 2026

Let me note that I ended up using 2 * actual fovy to address too aggressive culling for tile selection for WebMercatorviewport, and then also 2 * actual fovy to calculate zoom from elevation. I think it affects LOD switching a bit as well which seems a reasonable tradeoff.

Going further, with the global Google Photorealistic 3D Tileset, I found that using the WebMercatorViewport as the only documented option was not appropriate. I am rendering it in a perspective projection, from geocentric space, not as a map. So in hindsight, it makes a lot of sense there were selection issues due to this conflict.

#10066 and #10252 use FirstPersonViewport and GlobeViewport for 3d tiles layer, so it seems this is supposed to also work, somewhat ?

I tried automatic switching between WebMercator and GlobeViewports at a certain zoom threshold (6 or so) and it more or less works for the global views but tile selection seem to get perhaps confused. It holds on to way too many tiles (hundreds) navigating and zooming around. Perhaps tile selection during subsequent traversals is additive/subtractive and needs to be reset when switching viewports like this ?

@charlieforward9
Copy link
Copy Markdown
Collaborator

charlieforward9 commented May 13, 2026

usable zoom levels for WebMercatorViewport which lead to more or less expected tile selections when navigating the scene.

better placed in the @math.gl/web-mercator module

This could get some love: https://visgl.github.io/math.gl/docs#history

@ me in any active change / feature proposals

@andreasplesch
Copy link
Copy Markdown

Here is screen recording trying to show the increase in selected tiles. The #TILES number shown is just .selectedTiles.length. Apologies for the flashing:

layersgl2-2026-05-13_16.12.05.mp4

Hm, I also noticing that the calculated zoom changes quite a bit, so probably something else is going on.

@charlieforward9
Copy link
Copy Markdown
Collaborator

charlieforward9 commented May 13, 2026

I love the direction youre taking the controller movement - would be blessed to work it into a deck.gl release as a follow up to landing #10298

https://maplibre.org/roadmap/maplibre-native/lod/ & maplibre/maplibre-native#2958 might help dig into a working system that clearly spent time mastering tile selection math.

@andreasplesch
Copy link
Copy Markdown

The renderer is x3dom (https://github.com/x3dom/x3dom) and the controller switches to 'fly mode' (a standard navigation mode in X3D, see https://www.web3d.org/documents/specifications/19775-1/V4.0/Part01/components/navigation.html#NavigationInfo). It will be difficult to transplant to deck.gl since the concepts are very different but the main implementation is here:

https://github.com/x3dom/x3dom/blob/master/src/nodes/Navigation/modes/DefaultNavigation.js

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.

4 participants