The browser is a 3D engine now.
A modern browser can render real-time 3D scenes that would have needed a game engine a decade ago. No plugin, no download, no install — paste a URL, wait two seconds, and you're inside the simulation. The demo below is one 260 KB HTML file. Drag to orbit. Scroll to zoom. Click any planet to fly to it.
Real-time 3D is budget management.
Every frame of a WebGL scene has a budget — roughly 16 milliseconds to compute positions, run shaders, and push pixels to the screen. Miss the budget and the frame rate halves. Miss it repeatedly and the experience drops from "smooth" to "broken" faster than the visitor can blame their laptop. The discipline is not about cramming in more — it's about knowing what costs what, so you spend the frame budget on things that visibly matter.
A solar system is a useful subject because the physics is well-known (Kepler's laws since 1619) and the visual expectations are universal. Anyone can spot a planet in the wrong place. Anyone can tell when Saturn's rings are cartoonish. The challenge is rendering 8 planets, a dozen moons, Saturn's rings, an asteroid belt, a comet with a procedural tail, and a Milky Way skybox — all at 60 fps on a five-year-old laptop — while staying in a single HTML file that loads in two seconds over slow Wi-Fi.
Three.js, a few shaders, no backend.
The stack is deliberately thin. A heavy framework would triple the page weight without adding anything the simulation actually needs. Three.js handles the low-level WebGL plumbing so the interesting code is about astronomy, not graphics APIs.
Eight planets, one frame budget.
Wait for the scene to load (~2 s on first view — Three.js is fetched from Cloudflare), then the intro animation pulls the camera out from inside the Sun's corona until the whole system is framed. After that, everything is interactive: drag to orbit the camera, scroll to zoom, right-click to pan. Click any planet and the camera flies to it. Use the bottom controls to speed up or reverse time.
The numbers driving the scene.
The simulation is parameter-driven — every planet's size, orbital radius, period, axial tilt, and shader colour scheme comes from this table. Click any row to fly the camera to that body. The same values drive both "compressed" and "realistic" scale modes — the transform just changes the interpretation of the numbers.
| Planet | Radius (km) | Semi-major axis (AU) | Orbital period | Day length | Axial tilt | Moons |
|---|
"1,500 rocks at 60 frames per second."
The asteroid belt is the single most instructive part of this build — the gap between the naive solution (bad) and the correct solution (good) is larger than anywhere else in the scene, and the technique that fixes it is the same technique that makes every large-scale WebGL scene viable.
First version: for (let i=0; i<1500; i++) scene.add(new THREE.Mesh(geometry, material)). Looks correct. Runs at 6 fps on a MacBook Air. Every asteroid is a separate draw call, every draw call crosses the JavaScript-to-GPU boundary, and that boundary is the bottleneck. The GPU itself is bored; the bus between CPU and GPU is on fire.
Merge all asteroids into a single geometry — one mesh, one draw call. Frame rate jumps to 58 fps. But now every asteroid moves as one rigid blob — you can't orbit them individually. The visual suffers badly; the belt looks static.
Three.js's InstancedMesh. One geometry, one material, one draw call — but each instance has its own per-instance matrix, so every asteroid can orbit at its own angular velocity. Update the matrices in JavaScript each frame, the GPU consumes them. Frame rate: 60 fps (capped). CPU cost of matrix updates: ~0.8 ms. The belt feels alive; the performance budget is barely touched.
Replace the sphere geometry with a 6-sided icosahedron. 20 triangles per asteroid instead of 128. Nobody can tell the difference at belt-viewing distance — but 1,500 × 108 fewer triangles is 162,000 triangles saved per frame. GPU budget on the asteroid belt drops from 2.2 ms to 0.3 ms. That's 1.9 ms given back to the rest of the scene — enough for a second comet, richer sun shaders, or four more moons.
Why this generalises. The same three questions apply to almost every large-count problem in real-time 3D: one draw call or many? Unique geometry per instance or shared? How many triangles does a viewer actually perceive? Get those three right and the frame budget takes care of itself. Get any one wrong and the simulation stutters on anything older than last year's flagship.