Open in playproj

playproj

GuidePerspectiveCones and cylindersSight and shadeSpilhausGlobals and interruptedsPeeling the orangeDistortionEllipsoidGeoidOrthorectifications2 cellsH3 hexagonsL3 binsModel gridsIndex spaceMGRS and Sentinel-2AAT gridTiles and overviewsRectilinear chunksIcechunk and ZarrRasterizeAffineRegular in what?Convex hullCatastrophesTexture mappingPenguin datumFor exampleHomageVicGridNuyinaRhumb linesFrameTime and depthCutawayVoxels

How it works

A renderer draws triangles. A mesh is a list of vertices (points in 3D) and a list of triangles, each three vertex numbers. To put an image on it, each vertex also carries a texture coordinate (u, v): where that corner sits on the image, from 0 to 1 across and up. The right-hand view is the image with the triangles drawn at their u, v, the mesh's UV layout. On the small mesh you can drag the corners there and watch the picture slide over the surface.

For every pixel a triangle covers, the GPU works out the pixel's barycentric weights: three numbers that add to 1 and say how near it is to each corner. It blends the corners' u, v by those weights and reads the image there. Hover either view to see the weights, the blend and the point it reads. Inside one triangle that blend is linear, so the image is stretched as a flat sheet from corner to corner, and anything between the corners comes along for free, right or wrong.

The blend is linear on the triangle in 3D, not on the screen: a triangle seen at an angle is nearer the eye at one corner, and the GPU divides by depth so the image stays put (perspective-correct interpolation). Affine turns that off and blends on the screen, as early game consoles did; tilt the small mesh and the picture swims and kinks along the diagonals.

A basemap such as GEBCO is an equirectangular image: u is longitude and v is latitude. Drape it on a globe and each vertex's u, v is just its own lon and lat, so a lon/lat mesh lines up with the image cell for cell. Three things go wrong:

  • The seam. Longitude jumps from 180 to -180. If the vertices on the antimeridian are shared, the last column of triangles runs from u near 1 back to u = 0 and blends across the whole image backwards, a smear of the entire map squeezed into one strip. The fix is two vertices in the same place, one at u = 0 and one at u = 1. A triangulated globe (the icosphere) has triangles that straddle the seam at odd angles; the fix there is to copy their western corners at u + 1 and let the image repeat.
  • The poles. The top row of the image, every longitude, is one point. With one vertex there (and one u), the polar triangles shear into a fan of zigzags. A pole vertex for each triangle, at that triangle's middle u, gives each its own slice of the top row.
  • Coarse triangles. A lon/lat grid is linear in the image but not on the sphere or in a projected map. Between the corners the blend follows the straight triangle, not the curve, so a coarse mesh bends coastlines at the triangle edges. On the globe the facets also cut inside the sphere. In a map the error is the projection's own curvature: none in plate carree (which is the image), latitude only in Mercator, and the whole shape in polar stereographic and Mollweide. The readout gives how far the point the GPU reads is from where the pixel really is.

Per pixel skips the u, v altogether: the GPU interpolates the position instead (a 3D point, or x, y in the map), and a small shader turns it back into lon and lat for every pixel: the direction of the point from the centre on the globe, or the projection's inverse in the map. That is exact at any mesh size, with no seam or pole to fix, and it is how a warp with a projection's inverse fills a map (as PROJ and GDAL do, pixel by pixel). The mesh then only has to cover the right area. One catch: at the jump from 180 to -180 the GPU sees u change by nearly 1 between neighbouring pixels and picks the smallest copy of the image, drawing a fine line along the seam. The shader here picks whichever of u and u - 1/2 changes less across the pixel (a trick from Tarini, 2012) to avoid it.

The projections are spherical (R = 1), written once in src/core/texmap.js and again in the shader, and checked against proj4js in test/check-texmap.mjs, with the mesh seams and poles and the size of the per-vertex miss.

Hash keys: mesh, n, bend, proj, seam, pole, look, edges, aff, bm (none for the test card, photo, gebco, esri), uv (dragged corners of the small mesh), cam.

  • Tarini, M. (2012) Cylindrical and toroidal parameterizations without vertex seams, Journal of Graphics Tools 16(3), 144-150.
  • The photo is Caravaggio's Basket of Fruit (about 1599, Biblioteca Ambrosiana, Milan), public domain, from Wikimedia Commons by way of the three.js examples.
  • Coastlines from Natural Earth; GEBCO 2026 Grid (GEBCO Compilation Group) from Source Cooperative; projections checked with proj4js; drawn with three.js.
Source and contributing