|
| 1 | +# Globe projection |
| 2 | + |
| 3 | +This guide describes the inner workings of globe projection. |
| 4 | +Globe draws the same vector polygons and lines as mercator projection, |
| 5 | +ensuring a clear, unstretched image at all view angles and support for dynamic layers and geometry. |
| 6 | + |
| 7 | +The actual projection is done in three steps: |
| 8 | + |
| 9 | +- compute angular spherical coordinates from source web mercator tile data |
| 10 | +- convert spherical coordinates to a 3D vector - a point on the surface of a unit sphere |
| 11 | +- project the 3D vector using a common perspective projection matrix |
| 12 | + |
| 13 | +So the globe is a unit sphere from the point of view of projection. |
| 14 | +This also simplifies a lot of math, and is used extensively in the globe transform class. |
| 15 | + |
| 16 | +Geometry is projected to the sphere in the vertex shader. |
| 17 | + |
| 18 | +## Zoom behavior |
| 19 | + |
| 20 | +To stay consistent with web mercator maps, globe is automatically enlarged when map center is nearing the poles. |
| 21 | +This keeps the map center visually similar to a mercator map with the same x,y and zoom. |
| 22 | +However, when panning the globe or performing camera animations, |
| 23 | +we do not want the planet to get larger or smaller when changing latitudes. |
| 24 | +Map movement thus compensates for the planet size change by also |
| 25 | +changing zoom level along with latitude changes. |
| 26 | + |
| 27 | +This behavior is completely automatic and transparent to the user. |
| 28 | +The only case when the user needs to be aware of this is when |
| 29 | +programmatically triggering animations such as `flyTo` and `easeTo` |
| 30 | +and using them to both change the map center's latitude and *at the same time* |
| 31 | +changing the map's zoom to an amount based on the map's starting zoom. |
| 32 | +The example [globe-zoom-planet-size-function](https://maplibre.org/maplibre-gl-js/docs/examples/globe-zoom-planet-size-function/) demonstrates how to |
| 33 | +compensate for planet size changes in this case. |
| 34 | +All other camera animations (that either specify target zoom |
| 35 | +that is not based on current zoom or do not specify zoom at all) will work as expected. |
| 36 | + |
| 37 | +## Shaders |
| 38 | + |
| 39 | +Most vertex shaders use the `projectTile` function, which |
| 40 | +accepts a 2D vector of coordinates inside the currently drawn tile, |
| 41 | +in range 0..EXTENT (8192), and returns its final projection that can |
| 42 | +be directly passed to `gl_Position`. |
| 43 | +When drawing a tile, proper uniforms must be set to convert from |
| 44 | +these tile-local coordinates to web mercator. |
| 45 | + |
| 46 | +The implementation of `projectTile` is automatically injected into the shader source code. |
| 47 | +Different implementations can be injected, depending on the currently active projection. |
| 48 | +Thanks to this many shaders use the exact same code for both mercator and globe, |
| 49 | +although there are shaders that use `#ifdef GLOBE` for globe-specific code. |
| 50 | + |
| 51 | +## Subdivision |
| 52 | + |
| 53 | +If we were to draw mercator tiles with globe shaders directly, we would end up with a deformed sphere. |
| 54 | +This is due to how polygons and lines are triangulated in MapLibre - the earcut algorithm |
| 55 | +creates as few triangles as possible, which can sometimes result in huge triangles, for example in the oceans. |
| 56 | +This behavior is desirable in mercator maps, but if we were to project the vertices of such large triangles to globe directly, |
| 57 | +we would not get curved horizons, lines, etc. |
| 58 | +For this reason, before a tile is finished loading, its geometry (both polygons and lines) is further subdivided. |
| 59 | + |
| 60 | +The figure below demonstrates how globe would look without subdivision. |
| 61 | +Note the deformed oceans, and the USA-Canada border that is not properly curved. |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +It is critical that subdivision is as fast as possible, otherwise it would significantly slow down tile loading. |
| 66 | +Currently the fastest approach seems to be taking the output geometry from `earcut` and subdividing that further. |
| 67 | + |
| 68 | +When modifying subdivision, beware that it is very prone to subtle errors, resulting in single-pixel seams. |
| 69 | +Subdivision should also split the geometry in consistent places, |
| 70 | +so that polygons and lines match up correctly when projected. |
| 71 | + |
| 72 | +We use subdivision that results in a square grid, visible in the figure below. |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | +Subdivision is configured in the Projection object. |
| 77 | +Subdivision granularity is defined by the base tile granularity and minimal allowed granularity. |
| 78 | +The tile for zoom level 0 will have base granularity, tile for zoom 1 will have half that, etc., |
| 79 | +but never less than minimal granularity. |
| 80 | + |
| 81 | +The maximal subdivision granularity of 128 for fill layers is enough to get nicely curved horizons, |
| 82 | +while also not generating too much new geometry and not overflowing the 16 bit vertex indices used throughout MapLibre. |
| 83 | + |
| 84 | +Raster tiles in particular need a relative high base granularity, as otherwise they would exhibit |
| 85 | +visible warping and deformations when changing zoom levels. |
| 86 | + |
| 87 | +## Floating point precision & transitioning to mercator |
| 88 | + |
| 89 | +Shaders work with 32 bit floating point numbers (64 bit are possible on some platforms, but very slow). |
| 90 | +The 23 bits of mantissa and 1 sign bit can represent at most around 16 million values, |
| 91 | +but the circumference of the earth is roughly 40 000 km, which works out to |
| 92 | +about one float32 value per 2.5 meters, which is insufficient for a map. |
| 93 | +Thus if we were to use globe projection at all zoom levels, we would unsurprisingly encounter precision issues. |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +To combat this, globe projection automatically switches to mercator projection around zoom level 12. |
| 98 | +This transition is smooth, animated and can only be noticed if you look very closely, |
| 99 | +because globe and mercator projections converge at high zoom levels, and around level 12 |
| 100 | +they are already very close. |
| 101 | + |
| 102 | +The transition animation is implemented in the shader's projection function, |
| 103 | +and is controlled by a "globeness" parameter passed from the transform. |
| 104 | + |
| 105 | +## GPU "atan" error correction |
| 106 | + |
| 107 | +When implementing globe, we noticed that globe projection did not match mercator projection |
| 108 | +after the automatic transition described in previous section. |
| 109 | +This mismatch was very visible at certain latitudes, the globe map was shifted north/south by hundreds of meters, |
| 110 | +but at other latitudes the shift was much smaller. This behavior was also inconsistent - one would |
| 111 | +expect the shift to gradually increase or decrease with distance from equator, but that was not the case. |
| 112 | + |
| 113 | +Eventually, we tracked this down to an issue in the projection shader, specifically the `atan` function. |
| 114 | +On some GPU vendors, the function is inaccurate in a way that matches the observed projection shifts. |
| 115 | + |
| 116 | +To combat this, every second we draw a 1x1 pixel framebuffer and store the `atan` value |
| 117 | +for the current latitude, asynchronously download the pixel's value, compare it with `Math.atan` |
| 118 | +reference, and shift the globe projection matrix to compensate. |
| 119 | +This approach works, because the error is continuous and doesn't change too quickly with latitude. |
| 120 | + |
| 121 | +This approach also has the advantage that it works regardless of the actual error of the `atan`, |
| 122 | +so MapLibre should work fine even if it runs on some new GPU in the future with different |
| 123 | +`atan` inaccuracies. |
| 124 | + |
| 125 | +## Clipping |
| 126 | + |
| 127 | +When drawing a planet, we need to somehow clip the geometry that is on its backfacing side. |
| 128 | +Since MapLibre uses the Z-buffer for optimizing transparency drawing, filling it with custom |
| 129 | +values, we cannot use it for this purpose. |
| 130 | + |
| 131 | +Instead, we compute a plane that intersects the horizons, and for each vertex |
| 132 | +we compute the distance from this plane and store it in `gl_Position.z`. |
| 133 | +This forces the GPU's clipping hardware to clip geometry beyond the planet's horizon. |
| 134 | +This does not affect MapLibre's custom Z values, since they are set later using |
| 135 | +`glDepthRange`. |
| 136 | + |
| 137 | +However this approach does not work on some phones due to what is likely a driver bug, |
| 138 | +which applies `glDepthRange` and clipping in the wrong order. |
| 139 | +So additionally, face culling is used for fill and raster layers |
| 140 | +(earcut does not result in consistent winding order, this is ensured during subdivision) |
| 141 | +and line layers (which have inconsistent winding order) discard beyond-horizon |
| 142 | +pixels in the fragment shader. |
| 143 | + |
| 144 | +## Raster tiles |
| 145 | + |
| 146 | +Drawing raster tiles under globe is somewhat more complex than under mercator, |
| 147 | +since under globe they are much more prone to having slight seams between tiles. |
| 148 | +Tile are drawn as subdivided meshes instead of simple quads, and the curvature |
| 149 | +near the edges can cause seams, especially in cases when two tiles of different |
| 150 | +zoom levels are next to each other. |
| 151 | + |
| 152 | +To make sure that there are both no seams and that every pixel is covered by |
| 153 | +valid tile texture (as opposed to a stretched border of a neighboring tile), |
| 154 | +we first draw all tiles *without* border, marking all drawn pixels in stencil. |
| 155 | +Then, we draw all tiles *with* borders, but set stencil to discard all pixels |
| 156 | +that were drawn in the first pass. |
| 157 | + |
| 158 | +This ensures that no pixel is drawn twice, and that the stretched borders |
| 159 | +are only drawn in regions between tiles. |
| 160 | + |
| 161 | +## Symbols |
| 162 | + |
| 163 | +Symbol rendering also had to be adapted for globe, as well as collision detection and placement. |
| 164 | +MapLibre computed well-fitting bounding boxes even for curved symbols under globe projection |
| 165 | +by computing the AABB from a projection of the symbol's box' corners and box edge midpoints. |
| 166 | +This is an approximation, but works well in practice. |
| 167 | + |
| 168 | +## Transformations and unproject |
| 169 | + |
| 170 | +Most projection and unproject functions from the transform interface are adapted for globe, |
| 171 | +with some caveats. |
| 172 | +The `setLocationAtPoint`function may sometimes not find a valid solution |
| 173 | +for the given parameters. |
| 174 | +Globe transform currently does not support constraining the map's center. |
| 175 | + |
| 176 | +## Controls |
| 177 | + |
| 178 | +Globe uses slightly different controls than mercator map. |
| 179 | +Panning, zooming, etc. is aware of the sphere and should work intuitively, |
| 180 | +as well as camera animations such as `flyTo` and `easeTo`. |
| 181 | + |
| 182 | +Specifically, when zooming, the location under the cursor stays under the cursor, |
| 183 | +just like it does on a mercator map. |
| 184 | +However this behavior has some limitations on the globe. |
| 185 | +In some scenarios, such as zooming to the edge of the planet, |
| 186 | +this way of zooming would result in rapid and unpleasant map panning. |
| 187 | +Thus this behavior is slowly faded out at low zooms and replaced with an approximation. |
| 188 | + |
| 189 | +There are also other edge cases, such as when looking at the planet's poles |
| 190 | +and trying to zoom in to a location that is on the other hemisphere ("behind the pole"). |
| 191 | +MapLibre does not support moving the camera across poles, so instead we need to rotate around. |
| 192 | +In this case, an approximation instead of exact zooming is used as well. |
| 193 | + |
| 194 | +Globe controls also use panning inertia, just like mercator. |
| 195 | +Special care was taken to keep the movement speed of inertia consistent. |
0 commit comments