An interactive Newtonian N-body universe simulator in C++20 and OpenGL 3.3: real astronomical data, four selectable integrators, live conserved-quantity diagnostics, runtime body spawning, and a GPU-deformed "spacetime" grid that is clearly labelled as the visual analogy it is.
Built warning-free on four toolchains -- MinGW g++ 16.2, MSVC 19.44, gcc 13.3 and clang 18.1. CI builds every push on Linux, Windows and macOS, and runs the OpenGL self-test headlessly on Linux through Mesa's software rasteriser, so continuous integration exercises the real GL path rather than only compiling.
Every orbit here is produced by integrating a = Σ G m / r². Nothing is animated on a path.
- Simulates gravitational attraction between arbitrary bodies with
F = G m1 m2 / r^2, in double precision, in SI units. - Ships fourteen scene presets built from real masses and semi-major axes, from a bouncing-ball kinematics lab to a compact object tearing through the inner solar system.
- Flies around in 3D, or locks the camera to any body and orbits it.
- Spawns new bodies at runtime, from the UI or the command line, which immediately participate in the pairwise sum.
- Reports kinetic, potential and total energy, momentum, angular momentum and relative energy drift, live.
- Shows osculating orbital elements for the selected body: semi-major axis, eccentricity, periapsis, apoapsis, period, inclination.
- Pauses, single-steps, resets, and accelerates time from 0.01x to 1e9x without ever enlarging the physics timestep.
- Draws a warped grid that responds to mass, as a visualisation -- see docs/PHYSICS.md.
- Renders through an HDR pipeline with bloom, ACES tone mapping and a procedural starfield, so stars glow rather than being flat discs.
- Solves gravity by exact O(N^2) summation or a Barnes-Hut octree, with a 2000-body asteroid belt preset to run the tree against.
- Demonstrates real celestial mechanics: Trojan asteroids librating at the L4 and L5 Lagrange points, with a control showing an arbitrary angle does not hold.
- Forecasts where the selected body will go by integrating a copy of the whole system, so the prediction includes perturbation from every other body rather than being a Kepler ellipse.
- Has an optional post-Newtonian correction that reproduces Mercury's perihelion precession to 42.77 arcsec/century against a predicted 43.
The parts that were genuinely hard, and what they cost:
- Floating-origin rendering. Physics runs in
double; Earth's orbital radius is 1.5e11 m andfloatwould quantise its position into 16 km steps. Every position is made camera-relative in double and only then narrowed to float, so nothing downstream ever sees an absolute astronomical coordinate. - Scale compression that preserves ordering. The Sun is 109 Earth radii, so
any constant exaggeration large enough to make Earth visible draws the Sun
wider than Earth's entire orbit. Drawn radius is
gain * (r / metresPerUnit) ^ 0.35, with the gain solved per scene from a named reference body. Gravity never reads a drawn radius. - Symplectic integration, and a test that proves it. Velocity Verlet by default. The energy test asserts boundedness rather than smallness: peak drift over orbits 1-25 is compared against orbits 26-50, which is the property that actually separates a symplectic method from an accurate one. The inner solar system holds to better than 1e-5 relative energy drift over a simulated decade, and Earth completes exactly one orbit per Julian year.
- Time acceleration that does not corrupt the physics. Up to 1e9x by taking more fixed steps, never a larger one, with a step budget that degrades into slow motion instead of freezing and reports when it is hit.
- Honest failure analysis. A compact object thrown through a star moves the total energy by a factor of thousands. Rather than hide it, a test refines the timestep across 600 s, 150 s and 37.5 s and requires the drift to fall monotonically, which distinguishes unresolved-encounter truncation error from a masked singularity.
- Testability enforced by layering.
sim/has no OpenGL, GLFW or ImGui dependency and is a separate static library, so all 160 unit tests run headless. The picking and scale maths were deliberately moved intosim/so they could be tested without a GL context. - Barnes-Hut octree, measured rather than claimed.
--benchmarktimes it against exact summation and prints speed and accuracy, because a solver that is faster and wrong is not faster. Measured cost per doubling of N: direct x4.0 (quadratic), tree x2.3 (N log N), crossing over near 4000 bodies and reaching 4.8x at 16384 with the force error flat at ~1.2%. The tree also breaks Newton's third law, so a test asserts its momentum drift is worse than direct summation rather than pretending otherwise. - Reproduces the classic test of general relativity. An optional 1PN correction advances Mercury's perihelion by a measured 42.77 arcsec/century against the closed-form 43.00. Getting there required noticing that velocity Verlet precesses a Kepler orbit on its own -- about -37 arcsec/century at a 600 s step, the same order as the effect and in the opposite direction. The measurement differences a Newtonian run against a relativistic one to cancel it, and a separate test proves that drift is truncation error by showing it falls exactly 4x per halving of the step (-149.98 / -37.49 / -9.37 arcsec/century at 1200 / 600 / 300 s).
- Trajectory forecasting that is not a conic section. The predicted path is produced by stepping a copy of the live system with the same integrator, so it accounts for every other body. A test forecasts a quarter of Earth's year, advances the real system by the same amount and requires the endpoints to agree to 1e-6 AU; another predicts Mercury over forty years with and without Jupiter present and requires the answers to differ, which a closed-form ellipse could not do.
- HDR render pipeline. Multisampled
RGBA16Ftarget so a star's core can exceed 1.0 and survive to the bright pass, then bloom, ACES tone mapping and a procedural starfield. At 8 bits the overflow clips to white and the halo disappears entirely.
Windows x64 portable build
-- 1 MB, no installer and no runtime redistributable. Unzip and run
universe-sim.exe. Needs an OpenGL 3.3 capable GPU.
Or build from source, which takes about a minute; see below.
Rendered with tools/make_demo.sh, which is deterministic: simulated time
advances a fixed amount per output frame, so the same command reproduces the
same footage.
| Clip | What it shows |
|---|---|
| solar-system.mp4 | Sun through Neptune, camera sweeping a full circle |
| three-body.mp4 | Three equal masses on a Lagrange triangle, drifting into chaos |
| spawned-star.mp4 | A star inserted at runtime tearing the inner system apart |
| precession.mp4 | Relativistic perihelion advance, exaggerated into a rosette |
| trojans.mp4 | Asteroids librating around Jupiter's L4 and L5 points |
Left: relativistic perihelion precession, exaggerated 300000x so the rosette is visible in seconds. At strength 1 this is Mercury's real 43 arcseconds per century, measured by the test suite as 42.77. Right: Trojan swarms librating 60 degrees ahead of and behind Jupiter at L4 and L5, held there by nothing but the stability of the equilibrium.
Left: 2000 asteroids, each attracting every other, solved with the Barnes-Hut octree. Right: a one-year forecast, produced by integrating a copy of the system rather than by drawing an ellipse.
Requires CMake 3.24+, a C++20 compiler and an OpenGL 3.3 capable GPU. GLFW, GLM and Dear ImGui are fetched automatically at configure time; GLAD is vendored.
cmake -S . -B build -G Ninja
cmake --build buildLinux additionally needs the X11 and Wayland development packages. GLFW builds both backends by default, so the Wayland codegen tooling is required even if you only ever run under X11:
sudo apt install -y ninja-build libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev libwayland-dev wayland-protocols libxkbcommon-devWindows builds with either MSVC or MinGW. MSVC requires /Zc:preprocessor,
which the build sets automatically: the legacy preprocessor mangles raw string
literals when a macro stringizes them, which the test assertions do.
Run the tests:
build/bin/simcore-tests.exe # or: ctest --test-dir buildctest --test-dir build # 160 unit tests + the application self-test
ctest --test-dir build -E selftest # skip the test that needs a GPU160 unit tests with no third-party test framework, plus a self-test that drives every UI-reachable state transition against a real GL context (scene loading, spawning, deleting, focusing, the reset buttons, every integrator and stabilisation mode, and emptying the scene entirely) and checks 5000+ invariants afterwards.
build/bin/universe-sim.exe # solar system, panels open
build/bin/universe-sim.exe --scene three-body
build/bin/universe-sim.exe --list-scenes
build/bin/universe-sim.exe --help| Key | Action |
|---|---|
| Hold right mouse | Look around |
W A S D |
Move |
Space / Ctrl |
Up / down |
Shift / Alt |
Faster / slower |
| Scroll | Fly speed, or orbit distance |
C |
Toggle orbit / free-flight camera |
F |
Focus the selected body |
| Left click | Select a body |
Tab |
Cycle selection |
P |
Pause / resume |
. |
Single step |
[ ] |
Halve / double time scale |
R |
Reset the scene |
N |
Spawn a body ahead of the camera |
G T V B |
Toggle grid / trails / velocity vectors / Schwarzschild radius |
F5 / F12 |
Reload shaders / screenshot |
Esc |
Release the mouse, then quit |
| Key | Scene |
|---|---|
bounce |
Kinematics lab: uniform 9.81 m/s^2, restitution, friction |
attract |
Three masses released from rest |
two-body |
Earth around the Sun |
earth-moon |
Sun, Earth and Moon at Earth-Moon scale |
inner |
Inner solar system |
solar-system |
Sun through Neptune |
binary |
Two solar-mass stars plus a circumbinary planet |
three-body |
Three stars in a chaotic configuration |
intruder |
A 0.8 solar-mass star falling into the inner system |
compact |
A 12 solar-mass compact object with a probe ring |
compact-vs-sun |
A 30 solar-mass object crossing the solar system |
belt |
Sun, Jupiter and 2000 mutually attracting asteroids (Barnes-Hut) |
precession |
Relativistic perihelion advance, exaggerated into a rosette |
trojans |
Jupiter's L4/L5 Trojan swarms librating about the Lagrange points |
Every preset is also a JSON file in configs/. Files there are loaded at
startup and replace the compiled-in preset with the same key, so masses,
radii, orbital radii and initial velocities can be edited without rebuilding.
build/bin/universe-sim.exe --export-configs configs # regenerate from code
build/bin/universe-sim.exe --no-configs # ignore configs/Numbers are written with 17 significant digits, so a save/load round trip reproduces the same trajectory exactly -- there is a test that asserts bitwise equality of the state after 4000 steps, not just approximate agreement.
SI throughout: metres, kilograms, seconds. The UI formats values for reading
(6371000 m becomes 6371.000 km, 1.496e11 m becomes 1.0000 AU, masses in
solar or Earth masses, times in days and years) but every stored quantity is SI.
Three independent scales, which must never be confused:
| Name | Meaning | Read by |
|---|---|---|
| physical radius | the body's true radius in metres | collisions and merging |
metresPerUnit |
metres per world unit | position transform only |
| body size gain/exponent | drawn-radius exaggeration | model matrix only |
Gravity uses mass and position only. No code in sim/ reads a drawn radius.
Body radii are exaggerated by a power law, not a multiplier:
drawnRadius = gain * (trueRadius / metresPerUnit) ^ exponent
A constant multiplier cannot work here. The Sun is 109 Earth radii, so any
factor large enough to make the Earth visible draws the Sun wider than the
Earth's entire orbit -- which is exactly what the first attempt did. An exponent
below 1 compresses that ratio while preserving the ordering. True scale in the
Rendering panel turns the exaggeration off, at which point the planets vanish;
that is worth looking at once.
sim/ pure C++20 + glm. No OpenGL, GLFW or ImGui.
^
| one-way
engine/ window, input, timing, camera, assets, screenshots
render/ shaders, meshes, renderers
ui/ ImGui panels
sim/ is a separate static library and the tests link against only that.
If a physics change cannot be tested without opening a window, the layering has
been broken. Details in docs/ARCHITECTURE.md.
The full statement is in docs/PHYSICS.md. The essentials:
- The dynamics are Newtonian by default. A post-Newtonian correction is available but off unless you enable it, and it is the two-body Schwarzschild term applied pairwise rather than the full EIH N-body Lagrangian.
- The warped grid is a visualisation. It is displaced by a softened Newtonian potential and is a strictly one-way read of simulation state. Nothing in the physics reads it back. It is the rubber-sheet analogy, not general relativity, and not a picture of curved spacetime.
- Compact objects are not black holes. They are Newtonian point masses with
a small radius. Their Schwarzschild radius
r_s = 2GM/c^2is displayed as a reference figure; no event horizon, lensing or time dilation is modelled. - Softening is a documented modification, not a hidden fudge, and it does not make a close encounter accurate -- only finite. The error there is timestep resolution, which the tests demonstrate by refining the step.
- Orbits start as circles at the semi-major axis; real eccentricities and phases are not reproduced.
- All presets are coplanar, so the grid can lie in the orbital plane.
- Bodies are point masses for gravity: no oblateness, tides, rotation or axial tilt.
- Barnes-Hut trades exactness for speed: it approximates distant groups, so momentum is conserved only to the opening-angle error. Direct summation stays the default everywhere except the asteroid belt, and long runs should use it. Neither solver is GPU-accelerated.
- Lighting is not inverse-square, for readability across four orders of magnitude of scene scale.
- The merge model discards the kinetic energy of relative motion and does not conserve spin angular momentum, because rotation is not modelled.
- At wide zoom a small body can sit inside a larger body's drawn sphere and become unclickable -- the Moon inside the Earth in the inner-system view, for instance. Select it from the Bodies list or cycle with Tab instead. The self-test reports how many bodies this affects rather than hiding it.
- The background starfield is visible through the spacetime sheet, including "below" it. That is correct for a transparent visualisation plane rather than a floor, but it reads oddly at very shallow camera angles.
- Mouse and keyboard interaction is not driven by any automated test. The logic behind every control is covered by the self-test, but the click that reaches it is not.
Rendering milestones are verified by capturing frames offscreen and inspecting them, not by assuming a draw call worked:
build/bin/universe-sim.exe --scene inner --no-ui --warmup 24000000 \
--screenshot out.png --frame 4
tools/capture_scenes.sh docs/images # every preset at onceRuntime spawning is verified through the same code path the UI uses:
build/bin/universe-sim.exe --scene inner --spawn "Sun" --spawn-distance 14 \
--warmup 20000000 --settle 60000000 --no-ui --screenshot spawned.pngThat prints each body's orbital radius before and after. With no spawn the planets move by under 0.3% over the same interval; with a star inserted at runtime, Mercury's distance from the origin grows by 803% and Earth's by 531% -- from nothing but the extra term in the pairwise sum.
Progress and per-milestone results, including the bugs found along the way, are in docs/PROGRESS.md.
MIT, see LICENSE. Third-party components and their licences are listed in THIRD-PARTY.md; GLFW, GLM and Dear ImGui are fetched at configure time rather than redistributed here.









