/ @ortho-earth/core

ortho-earth Engine โ€” Technical Overview

A map engine that draws everything on a sphere, in the browser, with no map server of its own
@ortho-earth/core · @ortho-earth/globe · WebGPU with a WebGL2 fallback · true-scale terrain · worker pipeline

Table of Contents
  1. Four Layers โ€” Hard Core, Soft Apps
  2. Threads and Workers
  3. Backends โ€” WebGPU First, WebGL2 Always
  4. Camera, Projection and Zoom
  5. Terrain at True Scale
  6. Basemaps, Rasters and the World Band
  7. Gint โ€” Vectors You Can Ask Questions
  8. 3D Meshes โ€” Buildings, 3D Tiles, I3S, Models
  9. The Sky and the Shared Clock
  10. The Public Face
History
This page replaces the overview of ortho-map, the first engine (v1: d3 orthographic projection, one worker per layer, WebGL2 only). v1 was retired in September 2026. Its ideas โ€” orthographic globe, OffscreenCanvas, GeoPBF/AltPBF/Gint โ€” carried over; the implementation did not.

1. Four Layers โ€” Hard Core, Soft Apps

The engine is split so that the lower a part sits, the harder it is: guarded by test gates, type definitions and versions. Apps on top are shells and may change freely. New work starts by naming the layer it belongs to.

apps apps/* shells โ€” japan, world, equal, census2020, solar, the globe pages โ€ฆ each bundles the engine into its own build
โ†“ opts.region (a declaration, not code)
region pack @ortho-earth/jp what one country knows: basemap, elevation, buildings, search, POI, attribution, home view
โ†“
host @ortho-earth/globe createGlobe() โ€” boot, the world band, region-free gadgets, the SDK types, the gates
โ†“
core @ortho-earth/core drawing, camera, projection, terrain, rasters, labels, gint, the overlay contract โ€” knows no region
โ†“
foundation geopbf ยท altpbf ยท ephem the vector format (MIT) ยท elevation tiles (MIT) ยท the clock and the sky

The rule that holds it together: core and globe never name a country. A machine gate (verify:regionless) counts region words in globe's code and fails if the count grows. The direction of imports is one-way โ€” japan โ†’ globe โ†’ core โ†’ ephem and japan โ†’ jp โ†’ core; globe and jp never import each other. They meet only through the declaration that the app passes in.

2. Threads and Workers

The main thread holds the camera, input and DOM. Everything that decodes, builds or draws runs in workers. All workers start from one entry file (worker.ts) and are told their role by the Worker's name โ€” so the bundler builds one worker graph, and shared code (the GeoPBF core, loaders.gl, the shaders) exists once, not once per worker.

1main
camera ยท input ยท flight ยท gadgets (DOM) ยท the clock ยท sends draw {cam}
โ†“
2tile workers
fetch ยท decode MVT / MLT ยท triangulate ยท draw lists (transferred, not copied)
โ†“
3scene worker
merge the visible tiles into one scene per frame set
โ†“
4render worker
OffscreenCanvas ยท terrain ยท rasters ยท gint ยท labels ยท same-frame overlays โ€” one rAF
RoleWhat it does
renderOwns the canvas. Draws globe, terrain, basemap scene, rasters, gint layers, labels and overlays in one frame with one camera.
ortho:tile / ortho:sceneTile fetch and build / scene merge (the basemap pipeline).
mesh / meshdecoderStreams large 3D meshes (buildings) โ€” decode, dedup, ground, LOD, persist to OPFS/IndexedDB.
modelglTF/GLB, extrusions, 3D Tiles and I3S content; also sun-shadow and viewshed analysis.
gintbakeBakes gint layers (LOD ranks, edges) off the render thread.
ortho:heightDecodes elevation tiles.
rastertiles / imagequadServes raster tiles from local GeoPackage/MBTiles, or from an image pinned by four corners.
columnarReads and packs column-chunk layers (GeoParquet, chunked GeoPBF) for map.addColumnar.
vtdraw / vtextrudeMapLibre fill / line / circle / symbol layers and fill-extrusions on vector sources.
iiif / copcGeoreferenced IIIF map images baked into tiles / COPC point clouds read by range.
decoder:* / encoder:* / geopbf:*GeoPBF format conversion (dropped files, exports).

A region pack starts its own workers from its own entry (Japan's statistics worker, estat-worker.ts in @ortho-earth/jp), so the host never knows about them. Cross-origin isolation is not required: with it, GeoPBF buffers travel to workers without copying (SharedArrayBuffer); without it, they are copied once. Every feature works either way, and a gate (verify:nocoi) checks that.

3. Backends โ€” WebGPU First, WebGL2 Always

The core has two renderers with the same face โ€” set, draw, dispose โ€” one on WebGPU, one on WebGL2. The boot picks one at start:

ConditionBackend
?gl2=1WebGL2 (manual escape hatch)
no navigator.gpuWebGL2 directly
AndroidWebGL2 (silent black-screen faults on some mobile GPU drivers)
Safari / WebKit, on a page that declares glOverlay: trueWebGL2 (a WebGL2 overlay over WebGPU is not composited there)
a previous WebGPU start failed in this tabWebGL2 (once: the next boot only; twice: for the rest of the tab session; ?gpu=1 retries)
otherwiseWebGPU

WebGPU has three safety nets: an init failure falls back to WebGL2 inside the worker; a silent failure (initialised, but nothing reaches the screen) is caught by reading back the first frame; and if no frame arrives in 20 seconds the page restarts on WebGL2.

Two quality levers keep frames smooth. Transition anti-aliasing (WebGPU; WebGL2 fixes its anti-aliasing when the context is created): while the camera moves, frames are drawn at 1ร— MSAA; once it has been still for about half a second, one 4ร— frame is drawn. Dynamic resolution: when frames get slow, the canvas resolution steps down, and line widths stay constant on screen (both backends).

Text on the GPU (since 2.1): place names, road names, icons and road shields are baked into an SDF glyph atlas and drawn by WebGPU in the same frame as the map. Seen from straight above, text is drawn at its style layer's position, so a style that puts lines above its labels stacks as it does in MapLibre. On WebGL2 the engine falls back to Canvas2D text by itself; ?text=2d chooses it on WebGPU too.

4. Camera, Projection and Zoom

The camera is a small plain object โ€” center, zoom, pitch, bearing โ€” and the projection is one 4ร—4 matrix. Orthographic and perspective are one continuum: orthographic is perspective with the eye at infinite distance, so the projection is a slider, not a wall.

The view lives in the URL โ€” #zoom/lat/lon[/45t][/-30r][/15roll][/l=โ€ฆ][/c=theme][/t=UTC][/s=speed] โ€” so the address bar is always a link to what you see.

5. Terrain at True Scale

Elevation is drawn without exaggeration. The heights come from AltPBF: tiles of three spans (1ยฐ, 10ยฐ and 90ยฐ), decoded in a worker and packed into two elevation atlases โ€” a near atlas of fine cells around the camera, and a far atlas of coarse cells for the horizon. The shader reads one continuous height field from both.

6. Basemaps, Rasters and the World Band

Below about z8, the globe draws the world on its own โ€” Natural Earth 10 m countries, lakes, rivers and maritime boundaries, and a hypsometric tint computed every frame from global elevation and a climate field, so no raster tiles are downloaded. That is honestly as far as world data can carry.

Above that, a region takes over from the zoom its declaration names (Japan: z6.5, with the GSI vector basemap). Vector tiles go through the tile and scene workers; in tilted 3D views, a zoom-out keeps the finer tiles on screen while they still cover the ground, instead of swapping them for coarser ones. Tile formats are plugins on one registry: MVT is built in, and MLT (MapLibre Tile) comes from @ortho-earth/tile-formats, enabled at build time by aliasing #tile-formats to @ortho-earth/core/tileformats-mlt, which registers it โ€” the decoder loads on the first MLT tile, so a map that never meets one pays nothing. A vector source says "encoding": "mlt" as in MapLibre; a PMTiles archive is recognised by its header.

Rasters โ€” XYZ, WMS, WMTS, PMTiles, local GeoPackage/MBTiles, or one image by four corners โ€” are first composited into a ground atlas, and the terrain shader samples that atlas per pixel. That is why a raster follows the slope of a mountain instead of lying flat across it. External MapLibre style.json files are converted into the same pipeline (map.setStyle).

7. Gint โ€” Vectors You Can Ask Questions

Tiles are for drawing; Gint is for knowing. A Gint layer holds the whole geometry of a GeoPBF file on the GPU, so every feature can be picked, highlighted and joined to a table.

7.1 Column chunks โ€” drawing without the topology pass

Gint's strengths cost a full pass before the first frame: shared arcs, ranks, degree anchors, a bake. For files you only want to look at, map.addColumnar(src) takes the other road (#90): GeoParquet row groups, or a GeoPBF cut into spatial chunks, are packed in a worker straight into GPU arrays (unit-sphere xyz relative to a chunk origin, earcut triangles, 2โ€“3 grid-snapped LODs) and drawn by a same-frame overlay. Attributes stay columns; paint and filter (the Gint vocabulary) are evaluated in the worker into the same fid table Gint uses, so recolouring never rebuilds geometry. A dropped .parquet always takes this road; a .geopbf or .fgb does above columnarBytes (4 MB) โ€” unless the reader's first chunk looks like a choropleth with long shared arcs (meta.share: admin1, administrative maps), which Gint draws better and keeps. The layer drapes on the terrain per vertex and is hidden by the scene depth; shared borders are drawn twice, and editing hands the data back to Gint.

8. 3D Meshes โ€” Buildings, 3D Tiles, I3S, Models

One mesh core streams every kind of 3D data. It does not know which country the buildings belong to โ€” the region only declares where the data is.

SourceHow it enters
City buildings (Japan's PLATEAU, the Netherlands' 3D Basisvoorziening (PDOK))per-district loads declared by the region pack
Any 3D Tiles tilesetmap.add3DTiles(url) โ€” screen-space-error traversal, b3dm / i3dm / pnts / cmpt / glb
I3S scene layersmap.addI3S(url) โ€” decoded with loaders.gl
glTF / GLB modelsdropped files, ?g= links or map.model(src, { at, heading, scale, slot }) โ€” several side by side (one per slot); the handle moves, turns, scales and lifts the model afterwards (setLngLat / setHeading / setScale / setAltitude, no re-bake: the batches get a rigid transform, grounding follows the new spot) and gizmo(true) shows drag handles for placing by hand โ€” a dropped model shows them at once

The path is the same for all: decode (Draco included) โ†’ Earth-centred coordinates โ†’ the ortho sphere โ†’ duplicate faces removed โ†’ grounded on the terrain โ†’ LOD โ†’ relative-to-origin vertices โ†’ a coverage mask that hides the basemap's own flat building footprints underneath. Decoded districts are kept in OPFS and IndexedDB in the form the GPU eats, so a second visit goes straight from disk to the GPU.

9. The Sky and the Shared Clock

Below about z5 the globe shows the sky: real stars, planets and the Moon, constellations, the ecliptic and the celestial equator, and the night side. Zooming out further leads into the solar system at real scale.

All of it follows one clock (map.clock, from @ortho-earth/ephem โ€” the same clock the Solar System app uses). It runs in real time by default, and can be paused, fast-forwarded, run backwards or set to any moment between 1800 and 2049. The Sun's sub-solar point and sidereal time come from the same formulas in both apps, so the night side, the stars and the satellites always agree. The clock is shared with the render worker as one anchor โ€” {sim, wall, rate} โ€” sent only when its state changes.

10. The Public Face

import { createGlobe } from "@ortho-earth/globe";

const map = await createGlobe({ target: "#map", view: "#3/20/140" });
map.flyTo({ center: [139.767, 35.681], zoom: 14, pitch: 60 });
AreaExamples
CamerajumpTo ยท easeTo ยท flyTo ยท fitBounds ยท cameraForBounds ยท setMaxBounds
Style and layerssetStyle ยท addSource / addLayer ยท setPaintProperty ยท setFilter ยท queryRenderedFeatures
3D and terrainadd3DTiles ยท addI3S ยท setTerrain ยท extrusions ยท models
AnalysissunShadow ยท viewshed ยท lineOfSight ยท profiles ยท measurement
Timemap.clock ยท map.on("time")
Extendingmap.overlay(module) โ€” your own canvas drawn in the render worker in the same frame, with the same camera and api.time ยท addProtocol ยท transformRequest ยท Marker / Popup

The shape of the API follows MapLibre where it can, so existing styles and habits carry over. Everything above is covered by gates that boot pages through the published package itself โ€” what is tested is what you install. Since 2.0 the libraries are written in strict TypeScript and ship their type definitions (compiled .js + .d.ts on npm); the 1.x compatibility layers are gone โ€” see MIGRATION.md.

@ortho-earth/core ยท @ortho-earth/globe ยท GPL-3.0-or-later ยท Kenji Yoshida ยท 2026