/ @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.js) 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 ยท 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.
decoder:* / encoder:*GeoPBF format conversion (dropped files, exports).

A region pack can add roles (Japan adds its statistics worker) through a build-time slot, 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 render worker picks one at start:

ConditionBackend
?gl2=1WebGL2 (manual escape hatch)
no navigator.gpuWebGL2 directly
AndroidWebGL2 (silent black-screen faults on some mobile GPU drivers)
a previous WebGPU start failed in this tabWebGL2
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 run on both backends. Transition anti-aliasing: 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.

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][/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: 1ยฐ cells at three resolutions, 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.

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.

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' 3DBAG)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 or ?g= links

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.

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