Cesium

Cesium for Geospatial 3D Globes

CesiumJS 76,128 (Apache-2.0, 1.145.0, github.com/CesiumGS/cesium (https://github.com/CesiumGS/cesium 15,780 ), about 15,800 stars) renders a whole-Earth globe with terrain, imagery, 3D Tiles (the open format it created for streaming city-scale models) and time-dynamic entities. Its commercial side, Cesium ion, hosts terrain and imagery behind an access token; the bundled Natural Earth II imagery needs none:

A CesiumJS globe with bundled imagery and a point at Kota KinabaluHTMLLive
<script>window.CESIUM_BASE_URL =
  'https://cdn.jsdelivr.net/npm/cesium@1.145.0/Build/Cesium/';</script>
<script src="https://cdn.jsdelivr.net/npm/cesium@1.145.0/Build/Cesium/Cesium.js"></script>
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/cesium@1.145.0/Build/Cesium/Widgets/widgets.css">
<div id="globe" style="width:600px;height:205px"></div>
<script type="module">
const imagery = Cesium.TileMapServiceImageryProvider.fromUrl(
  Cesium.buildModuleUrl('Assets/Textures/NaturalEarthII'));        // bundled, no ion token
const viewer = new Cesium.Viewer('globe', {
  baseLayer: Cesium.ImageryLayer.fromProviderAsync(imagery), baseLayerPicker: false,
  geocoder: false, timeline: false, animation: false, homeButton: false,
  sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false,
});
viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.07, 5.98, 9e6) });
viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(116.07, 5.98),
  point: { pixelSize: 12, color: Cesium.Color.fromCssColorString('#e09a10') },
  label: { text: 'Kota Kinabalu', font: '14px sans-serif',
           pixelOffset: new Cesium.Cartesian2(0, -18) } });
const globe = viewer.scene.globe;
globe.tileLoadProgressEvent.addEventListener((queued) => {    // 0 queued: tiles are in
  if (queued === 0 && globe.tilesLoaded) window.__done = true;
});
</script>
Browser output of Listing 3.161
Browser output of 161

The globe drew in about 2 s on the GTX 1650 through WebGL 2. Cesium is the heaviest library here (Library Comparison), so it belongs to applications built around a globe, such as flight tracking or digital twins.

A raw WebGL sketch of what Cesium does: a WGS84 globe with a graticule and a point placed at Kota Kinabalu from degreesHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .wrap { position: relative; max-width: 600px; }
  canvas { display: block; width: 100%; }
  #tag { position: absolute; font: bold 12px system-ui; color: #fff; text-shadow: 0 1px 2px #000; transform: translate(-50%, -210%); white-space: nowrap; }
  p { max-width: 600px; margin: 4px 6px; font: 11px monospace; }
</style>
<div class="wrap"><canvas id="c" width="600" height="340"></canvas><div id="tag">Kota Kinabalu</div></div>
<p id="out"></p>
<script>
// Cartesian3.fromDegrees: longitude, latitude, height on the WGS84 ellipsoid to Earth-fixed x, y, z (metres).
function fromDegrees(lon, lat, h = 0) {
  const a = 6378137, e2 = 6.69437999014e-3, L = lon * Math.PI / 180, B = lat * Math.PI / 180;
  const N = a / Math.sqrt(1 - e2 * Math.sin(B) ** 2);
  return [(N + h) * Math.cos(B) * Math.cos(L), (N + h) * Math.cos(B) * Math.sin(L), (N * (1 - e2) + h) * Math.sin(B)];
}
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition; uniform mat3 uTurn; uniform float uAspect; out vec3 vNormal; out vec2 vLonLat;
void main() {
  vNormal = uTurn * normalize(aPosition * vec3(1, 1, 1.0067));   // ellipsoid normal
  vLonLat = vec2(atan(aPosition.y, aPosition.x), asin(normalize(aPosition).z));
  vec3 p = uTurn * aPosition * 0.8;
  gl_Position = vec4(p.x / uAspect, p.y, -p.z * 0.5, 1); gl_PointSize = 12.0; }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float; in vec3 vNormal; in vec2 vLonLat; uniform int uPoint; out vec4 c;
void main() {
  if (uPoint == 1) { if (length(gl_PointCoord - 0.5) > 0.5) discard; c = vec4(0.88, 0.6, 0.06, 1); return; }
  vec2 deg = degrees(vLonLat) / 15.0, grid = abs(fract(deg - 0.5) - 0.5) / fwidth(deg);
  float line = 1.0 - min(min(grid.x, grid.y), 1.0);              // a 15-degree graticule
  float light = 0.3 + 0.7 * max(dot(normalize(vNormal), normalize(vec3(-0.3, 0.4, 1))), 0.0);
  vec3 ocean = mix(vec3(0.08, 0.2, 0.36), vec3(0.2, 0.45, 0.66), light);
  c = vec4(mix(ocean, vec3(0.9, 0.93, 0.95), line * 0.5), 1); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
// Ellipsoid mesh in units of the equatorial radius (Earth's polar radius is 0.33% shorter).
const positions = [], indices = [], R = 64;
for (let i = 0; i <= R; i++) for (let j = 0; j <= R * 2; j++) {
  const p = fromDegrees(j / (R * 2) * 360 - 180, i / R * 180 - 90);
  positions.push(...p.map((v) => v / 6378137));
}
for (let i = 0; i < R; i++) for (let j = 0; j < R * 2; j++) {
  const k = i * (R * 2 + 1) + j; indices.push(k, k + 1, k + R * 2 + 1, k + 1, k + R * 2 + 2, k + R * 2 + 1);
}
const city = fromDegrees(116.07, 5.98).map((v) => v / 6378137 * 1.003);
positions.push(...city);
gl.bindVertexArray(gl.createVertexArray());
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
// Turn the Earth-fixed frame so the city faces the camera: z-up world to y-up screen.
const lon = 116.07 * Math.PI / 180, tilt = 0.25;
const cl = Math.cos(-lon + Math.PI / 2), sl = Math.sin(-lon + Math.PI / 2), ct = Math.cos(tilt), st = Math.sin(tilt);
const spin = [cl, sl, 0, -sl, cl, 0, 0, 0, 1];                    // about the polar axis
const toScreen = [-1, 0, 0, 0, -st, ct, 0, ct, st];              // z up, y toward us (so x points left), tipped by 'tilt'
const turn = new Array(9).fill(0);
for (let c = 0; c < 3; c++) for (let r = 0; r < 3; r++) for (let k = 0; k < 3; k++) turn[c * 3 + r] += toScreen[k * 3 + r] * spin[c * 3 + k];
gl.uniformMatrix3fv(gl.getUniformLocation(program, 'uTurn'), false, turn);
gl.uniform1f(gl.getUniformLocation(program, 'uAspect'), 600 / 340);
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0);
gl.uniform1i(gl.getUniformLocation(program, 'uPoint'), 1);
gl.drawArrays(gl.POINTS, positions.length / 3 - 1, 1);
// Place the HTML label where the point landed (the same maths on the CPU).
const s = [0, 1, 2].map((r) => (turn[r] * city[0] + turn[3 + r] * city[1] + turn[6 + r] * city[2]) * 0.8);
Object.assign(document.getElementById('tag').style, { left: (50 + s[0] / (600 / 340) * 50) + '%', top: (50 - s[1] * 50) + '%' });
document.getElementById('out').textContent = `fromDegrees(116.07, 5.98) = [${fromDegrees(116.07, 5.98).map((v) => Math.round(v)).join(', ')}] m`;
</script>