Scroll to move the camera. Escape leaves.

Gravitational Lensing

Source

Move the cursor to reposition the mass. Scroll to move it along the line of sight. Drag to reposition the mass. Pinch to move it along the line of sight.

5.0
0.50

Generating the background field

Tap or press Escape to exit

How it works

How it works

A mass placed between an observer and a distant light source bends the paths of the light rays that pass near it. The source appears displaced, stretched into arcs, and in some cases duplicated. This is gravitational lensing, and it is a prediction of general relativity that has been measured since 1919.

The deflection

For a light ray passing at a distance r from a point mass M, the deflection angle in the weak-field limit is

θ = 4GM / c2r

This is Einstein's thin-lens approximation: the whole deflection is treated as happening in a single plane, which is a good description when the lens is much smaller than the distances involved. It is twice the value Newtonian gravity predicts for a corpuscle of light, and the factor of two is what the 1919 eclipse measurement confirmed.

Why distance matters

How strongly a given mass lenses a given source is not a property of the mass alone. It depends on where the lens sits between the observer and the source. Writing DL for the distance to the lens, DS for the distance to the source and DLS for the distance between them, the Einstein radius is

θE2 = (4GM/c2) · DLS / (DL DS)

Scrolling moves the lens along the line of sight, which changes DL and so changes θE. Three things follow, and all three are visible on screen.

A more distant source is lensed more strongly. The procedural background is not one image but four planes, at different distances, and each is deflected by its own Einstein radius. The rings they form do not coincide; the furthest plane makes the widest one.

A source in front of the lens is not lensed at all. When DS is less than DL the factor DLS is negative and there is no lensing: the light reaches the observer without ever passing the mass. Those planes are also not hidden by the black hole, because they lie in front of it. Push the lens far enough away and the planes drop out of the effect one at a time, nearest first, until the foreground stars sit undisturbed over a black disc.

The horizon looks smaller from further away. The Schwarzschild radius is a fixed length, so the angle it subtends is rs / DL. The black disc shrinks as the lens recedes.

What the simulation computes

The image is built by working backwards. For each pixel on the screen, at an angular distance θ from the centre of the lens, and for each background plane, the simulation asks where in that plane the light must have come from:

β = θ − θE2 / θ

The plane contributes its brightness at β, magnified by μ = | θ / β |, and the planes are added, because light adds. Where β passes through zero, μ diverges and the source is smeared into a complete ring — the Einstein ring. The magnification is clamped to a factor of four, so the ring stays within the display range rather than saturating.

Inside the Schwarzschild radius no light escapes, so those pixels are black. Between rs and 1.5 rs lies the photon sphere, where light can orbit; it is darkened rather than removed.

Where the approximation ends

Three simplifications are worth naming. The thin-lens formula is a weak-field result, and this simulation applies it right up to the horizon, where it no longer holds; a correct treatment there requires integrating null geodesics in the Schwarzschild metric, which produces higher-order images the thin lens cannot reproduce. The distances are treated as adding and subtracting in flat space, so DLS = DS − DL; in cosmology the angular diameter distances do not combine that way, so the numbers here order the scene correctly without standing for real redshifts. And the background planes are flat sheets at fixed distances rather than a continuous distribution of matter.

Implementation

The calculation is per-pixel and order-independent, so it runs as a WebGL2 fragment shader: one pass, no geometry, at display resolution. The procedural planes are generated once from a fixed seed and uploaded as a single-channel texture array.

The original Python version performs the same computation with NumPy and pygame on the CPU, for one plane at a fixed distance. At the reference geometry — a single source at DS = 1 with the lens halfway to it — the two agree exactly: the geometric factor equals one, θE is 20M and the horizon subtends 3M, as it does in main.py.

Controls

ActionDesktopTouch
Move the mass across the skyMove the cursorDrag
Move it along the line of sightScrollPinch
Change the massUp and down arrow keys
Either, preciselyThe sliders