Skip to content
Always Shippable

WorldLock · Part 1 of 3

WorldLock: an AR window onto the actual planet

A native Android AR app that georegisters city towers, the whole Earth and the live ISS to the real world with ARCore's Geospatial API.

Updated 5 min read
A phone above the Seattle skyline at dusk, its screen showing an AR tower, the Earth and the ISS
On this page

Point your phone west from Seattle and there’s a glowing tower rising out of Tacoma, labeled Tacoma 42.4 km. Pan up and the night side of the Earth curves away from you, city lights and all, with NYC’s tower readable 3,815 km over the horizon. A white dash crosses the sky where the ISS actually is, right now.

That’s WorldLock: a native Android AR app that georegisters content to the real world using ARCore’s Geospatial API. The phone knows where it is and which way it’s pointing, to within a few meters and degrees, so a tower drawn over a city 3,800 km away lines up with where that city actually is.

Source: github.com/jonborchardt/worldlock

Phone screenshot: the night side of the Earth with distance-labeled towers on the horizonPhone screenshot at ground level: a thin blue Tacoma tower and a wide red glowing Seattle towerPhone screenshot of the data-pillar tower style: orange scrolling bands and rotating collar rings
Three screenshots taken from Capitol Hill in Seattle: what you'd see if each of those cities had a 20-mile-high tower and you could see through the Earth. Left to right: Spokane, Missoula, Tacoma, Seattle.

What’s in it#

  • City towers. A default set (Seattle, Tacoma, Spokane, Missoula, NYC, Tokyo, …) plus any city you add by name; it geocodes and drops a tower. Seven render styles, from a Tron-shell prism to a shrine beam to a “data pillar” with scrolling bands and rotating collar rings, all done in one shader with a style table. Bases sit on real terrain via ARCore terrain anchors.
  • The whole Earth. A per-fragment ray-cast against a sphere, not a mesh, drawn as one fullscreen triangle, with Blue Marble day and night textures blended at the real solar terminator computed from the current time.
  • The live ISS. Position fetched every 3 seconds from wheretheiss.at, linearly extrapolated between fixes, drawn as a little truss-and-solar-wings model that scales with distance so it stays visible at 400+ km.
Tessellated sphere versus per-fragment sphere ray-castSide-by-side schematic. On the left a coarse triangle mesh approximates the curve under a camera standing on the surface, with vertices marked as jittering. On the right the same camera looks through a single fullscreen triangle and one ray per pixel hits a smooth analytic sphere, so there are no vertices to jitter.
Why the globe is a ray-cast, not a mesh. Left: a tessellated sphere seen from its own surface: huge flat facets, and every vertex is 6.4 million metres from the origin, so it snaps to a half-metre float grid. Right: one fullscreen triangle; each fragment intersects its own ray with the analytic sphere.

The one hard rule#

Everything interesting in this app is a coordinate-precision problem. Earth-centered coordinates are ~6.4 million meters from the origin; 32-bit floats have ~7 significant digits; do the math and your geometry jitters by meters. So the rule is: authoritative positions are ECEF in doubles, converted to camera-relative ENU in doubles, and only the final origin-relative meters ever touch a GL float. The render origin stays pinned near you, where float precision is millimeters. The geodetic math even polices itself: a startup self-check verifies the conversions against exact WGS84 reference points and crashes the app at launch if they ever break.

The pipeline, with the one place a double becomes a float:

Coordinate pipeline from ECEF doubles to GL floatsGeospatial pose and city coordinates are held as ECEF doubles, then converted to ENU metres relative to a render origin near the phone. Only those small origin-relative values are cast to 32-bit floats and handed to OpenGL.

How bad is it if you skip that? A 32-bit float has 24 bits of mantissa, so the gap between one representable value and the next grows with the magnitude of the number:

32-bit float step size versus distance from the render originOn a log–log chart the step size climbs in a straight line from about 0.1 micrometres at 1 metre to about 0.5 metres at 6,371 kilometres, the Earth's radius. Near the origin precision is effectively free; at Earth-centre magnitudes a coordinate can only move in half-metre steps, and a few arithmetic operations turn that into metre-scale jitter.
Float32 step size (unit in the last place) at increasing magnitudes
Distance from originFloat32 step
1 m1.2e-7 m
10 m9.5e-7 m
100 m7.6e-6 m
1 km6.1e-5 m
10 km9.8e-4 m
100 km7.8e-3 m
1,000 km0.0625 m
6,371 km (Earth radius)0.5 m
Float32 step size against distance from the render origin, log–log. Keep the origin near the phone and every coordinate the GPU sees is small; put it at the Earth's centre and each coordinate can only move in half-metre steps.

The other quiet trick is the reference anchor: the first time the Geospatial pose is accurate enough (position < 15 m, yaw < 15°), the app plants an ARCore geospatial anchor at that pose and rides it from then on. Georegistration survives pause/resume and works indoors after the first good fix, instead of waiting for VPS to re-converge every time.

There’s a looser render gate in front of that (yaw < 25°, with 2× hysteresis so it doesn’t flicker), so towers appear early and swing into place while the anchor waits for the tight fix:

Georegistration states, from first fix to reference anchorThe app starts hidden. When Geospatial position accuracy is under 15 metres and yaw under 25 degrees it renders using the camera pose as origin, dropping back to hidden if accuracy degrades past twice those bounds. The first time yaw accuracy is also under 15 degrees it plants a reference anchor, an ARCore geospatial anchor, and becomes anchored; that state survives pause and resume and indoor use. If ARCore stops tracking the anchor the app falls back to the accuracy gate, rendering from the camera pose or hiding, and re-plants the anchor when the tight fix returns.

The globe follows the same rule. Tessellating a planet-sized sphere near the camera is a precision disaster; a ray that solves the quadratic in the fragment shader is analytic at every distance. The constant term is precomputed in doubles on the CPU, leaving only a possible float shimmer right at the horizon. The sphere passes through local sea level, deliberately not the true ellipsoid.

The stats that matter#

The entire app is 8 Kotlin files, about 1,900 lines, with one AR dependency (com.google.ar:core) plus the Play Services location library that ARCore’s Geospatial mode requires. No Android Studio was involved at any point; it’s built, deployed, and debugged entirely from the command line. How that works, and why the codebase stayed that small, are the next two posts.