DEV Community

Run Cheng
Run Cheng

Posted on

Building a browser Solar System explorer with NASA data and raw WebGL2

I'm runcheng, and I build Space Simulator Online: a free 3D explorer that starts at Earth, goes out through the Solar System and the Milky Way, and ends with colliding galaxies. It runs in the browser, needs no install or account, and is available in 10 languages.

The rule I set for the project is on the About page: everything on screen is based on scientific data and real images, and where the data shows something we can't see directly, the site says so. This post is about what that rule means for the code: where the data comes from, how it is rendered without a 3D engine, and which parts are deliberately not to scale.

Why raw WebGL2

The site is one JavaScript bundle that talks to canvas.getContext('webgl2') directly. There is no three.js or other scene-graph library. If the context can't be created, the page tells you, in your language, to open it in a current Chrome, Edge, Safari or Firefox.

The main reason is that the most interesting views aren't meshes at all. The Solar System view is a second, independent WebGL2 context where one fragment shader ray-traces the Sun and the planets. The same shader can also ray-march a procedural landscape so you can stand on a world. At any moment it holds 18 worlds: the Sun and nine planets, plus the selected world, its siblings, the small worlds on their own orbits and the big moons. A ray tracer in a full-screen quad fits that "a few spheres, rendered very well" problem better than a general-purpose engine.

Some details that came out of this:

  • Post-processing by hand. The frame renders into a texture, the brightest light spreads into a soft halo through a 5-level pyramid, then a light vignette and film grade go on top. The Milky Way view uses a float framebuffer tone-mapped with extended Reinhard, so the shadows stay unchanged and bright light rolls off instead of clipping.
  • Textures that don't freeze the page. Big planet maps are decoded off the main thread and uploaded at most one per frame. Real surface maps stream in after the first frame, and until a map arrives that world falls back to a procedural look.

Where the positions come from

Planet positions use JPL's "Approximate Positions of the Planets" (Table 1, valid 1800–2050 AD). That is why the time slider covers 1800–2100: the elements drift only slowly after 2050. Orientation comes from the IAU WGCCRE rotation models, including the larger periodic correction terms. The Moon's direction uses the main terms of Meeus, chapter 47.

Asteroids and comets use osculating elements from the JPL Small-Body Database and the Minor Planet Center.

Spacecraft are the fun part. Their trajectories come from NASA/JPL Horizons. Between Horizons data points the code doesn't interpolate linearly. It propagates two-body motion under the Sun's gravity using universal variables. When Horizons gives velocities, it blends the two-body arcs from both ends. When it doesn't, it solves Lambert's problem for the arc through each pair of points.

The clock starts at today's real date. That one clock drives everything: about 8 days per second in the overview, one hour per second near a world, and stopped on the ground.

Honest about scale

A Solar System at true scale is mostly empty black space, so some compromises are unavoidable. The trick is to make them explicit:

  • Distances are compressed, but directions are true: r_display = 10 + 14·sqrt(r_AU). Orbits are drawn as true Keplerian ellipses in their own planes, mapped through the same compression.
  • Planet sizes use their true mean radii relative to each other. Only the Sun is shrunk.
  • Moons keep their prime meridian facing their planet, so the IAU rotation model fixes where each one is on its orbit, while their distances are compressed.

For the scale-honest experience there's a separate Journey mode. The first leg runs in the Solar System view, where the distance readout follows the real scale of each shot. Then a short dip to black hands over to the galaxy view, whose camera keeps flying outward in true light-years, all the way to the cosmic microwave background.

Surfaces from mission imagery

Planet maps come from NASA and mission archives: Blue Marble and Black Marble for Earth, LRO for the Moon, MESSENGER for Mercury, a Viking mosaic with MOLA relief for Mars, Hubble OPAL for Jupiter, Cassini and New Horizons for the outer worlds, and some CC BY 4.0 textures from Solar System Scope. Small bodies like Bennu, Ryugu, Itokawa, Eros and Arrokoth use real shape models from NASA PDS and the missions. Their irregular radius is sampled from a radius map along each body-frame direction.

Real data you can poke at

A few modes go beyond "look at a pretty planet":

  • Planet Hunter loads real TESS 2-minute light curves from MAST. There is a minimal FITS reader in the bundle (FITS is big-endian), a detrend step using running medians, a box search over a log-spaced grid of periods that draws the periodogram as it grows, and vetting checks against the classic false positives: eclipsing binaries, spotted or pulsating stars, and "planets" bigger than about two Jupiters. Because a visitor's network may not reach the archive, a few pre-downloaded light curves ship as a fallback.
  • Gravitational waves plays LIGO Open Science Center strain data as sound. The data is whitened by the detector's own noise spectrum and band-passed to 35–350 Hz, and the best-fit waveform is aligned by matched filtering. The audio is shifted up in frequency because phone speakers barely reproduce anything under about 150 Hz.
  • Exoplanets places host stars from the NASA Exoplanet Archive at their measured distances in a 3D Milky Way built from a face-on map, lifted off the plane by published scale heights, with ESA Gaia's sky shown when you are near the Sun.

Galaxy collisions: WebGL2 for drawing, WebGPU when available

The galaxy collision view uses simulation units of 1 kpc with G = 1. The velocity unit is tuned so a Milky-Way-like model rotates at 220 km/s at 8 kpc. Scenes are set up from real encounters: the Milky Way and Andromeda, the Cartwheel ring, the Mice, and others.

When the browser supports WebGPU, an N-body mode does direct O(N²) summation in a compute shader, tiled through workgroup memory. Bulge and halo stay smooth analytic fields, while the discs are made of live, massive stars. Each frame's positions are read back into the WebGL textures, so lensing, bloom and video recording work unchanged. At startup it times a few steps at 16,384 stars and picks a star count that keeps one step under about 4 ms. If the GPU still falls behind, simulated time slows instead of the picture lagging, and if that isn't enough it steps down to fewer stars. Without WebGPU the button stays visible and explains why it can't run, which beats silently hiding a feature.

Small things that mattered

  • Share links. Every 3D view can produce a link that opens the same place: a planet, the Milky Way, a journey stage, or an exact galaxy-collision moment. State is a compact base64url token in the #fragment.
  • Narration. The guided tour is modeled on a museum docent: point at the object, tell its story with an everyday comparison, ask one question, reveal, move on. It uses recorded clips generated with the open Supertonic and Kokoro voice models, and falls back to the browser's speechSynthesis, then to timed captions.

What I'd tell someone starting a similar project

Pick your data sources first and write the credits as you go. Decide which quantities are allowed to lie (for me, distances in the overview) and say so on screen. And treat low-end devices as a first-class target: almost every performance decision above (one map upload per frame, time slowing instead of stuttering, fewer stars on weaker GPUs) came from that.

Try it at spacesimulator.online. If you spot a data error, the contact page is the fastest way to reach me, and corrections are very welcome.

Top comments (0)