In flat-space ray tracing, you fire rays through a screen and call it a day. In curved spacetime, even defining “which direction is forward” requires building a local reference frame from scratch. Here’s how.

This is Part 3 of the series. Part 1 covered the basic setup; Part 2 covered the difficulties of tracing straight lines in non-Cartesian coordinates. Now for one of the key elements: the camera.

In Euclidean space, setting up rays is straightforward: fire them from an origin through a screen and associate each one with a pixel (see Generating Camera Rays). For a GR raytracer, there are two complications:

  • The spacetime is curved.
  • We want to model physical entities like photons.

How to set up the camera

The geodesic equation tells us how a photon travels once it is moving. But we still need to answer a prior question: what are the initial conditions? In other words, which rays does the camera fire, and in which directions?

The local observer frame

The geodesic equation is written in a coordinate chart covering the region we trace through. One chart need not cover the whole spacetime. A camera, however, is a local device. It sits at one point in spacetime and measures directions relative to itself. We therefore need a way to translate between coordinate components and what the observer locally perceives.

This is done via a tetrad, which is a set of four orthonormal vectors $\lbrace e_t, e_x, e_y, e_z\rbrace$ in the tangent space at the observer’s position. Think of it as a set of local measuring axes carried by the observer. The vector $e_t = u$ is the observer’s four-velocity (their notion of “forward in time”), while $e_x$ (right), $e_y$ (up), and $e_z$ (forward) span the local spatial directions.

Orthonormality here is defined with respect to the spacetime metric $g_{\mu\nu}$:

\[g_{\mu\nu}\, (e_a)^\mu (e_b)^\nu = \eta_{ab}\]

where $\eta_{ab} = \operatorname{diag}(1,-1,-1,-1)$ is the flat Minkowski metric. Throughout this post I use the \(+---\) signature and units $G=c=1$. The tetrad makes inner products at that event look like those of special relativity. It does not remove spacetime curvature or tidal effects over a finite region.

The tetrad depends on both the position in spacetime and the observer’s state of motion. For a different spacetime geometry (Schwarzschild, Kerr, …) the tetrad takes a different form, but its role is always the same: translate between coordinate components and the observer’s local measurements.

Example: stationary observer in flat spacetime

The simplest case is a stationary observer in flat spacetime using Cartesian coordinates. The four-velocity is just

\[u^\mu = (1, 0, 0, 0)\]

and a natural choice for the spatial tetrad vectors is

\[e_x^\mu = (0, 1, 0, 0), \quad e_y^\mu = (0, 0, 1, 0), \quad e_z^\mu = (0, 0, 0, 1).\]

Each spatial vector is orthogonal to the four-velocity, $e_x^\mu u_\mu = 0$, and they are orthonormal among themselves.

Generating rays

The approach here follows Seeing relativity – I. We assign a direction in the observer’s local frame to each image pixel. A physical pinhole camera with a flat sensor gives a rectilinear projection: the direction is proportional to \(e_z + i'e_x + j'e_y\). Here we choose a stereographic projection instead, which gives a different mapping between the image plane and the sky.

For dimensionless image coordinates \((i', j')\), centered on the optical axis, first define the auxiliary spacelike vector

\[\mathbf{w} = e_z + i'\, e_x + j'\, e_y\]

Following Eq. 5 of the paper, the inverse stereographic projection gives the unit spacelike direction

\[N = -e_z + \frac{2\,\mathbf{w}}{-\mathbf{w} \cdot \mathbf{w}}\]

Its components in the spatial tetrad are

\[(N_x,N_y,N_z) = \frac{(2i',\ 2j',\ 1-i'^2-j'^2)}{1+i'^2+j'^2}.\]

Stereographic projection maps circles on the sky to circles on the image plane, except circles through the projection point, which become straight lines. This preserves the circular outline of a Schwarzschild shadow when it fits within the image (see Sec. II of Seeing relativity I).

For $R$ rows and $C$ columns, the demo uses zero-based pixel centres $(r,c)$ and a full vertical field of view $F$:

\[i' = \frac{2\tan(F/4)}{R}\left(c+\frac12-\frac C2\right), \qquad j' = \frac{2\tan(F/4)}{R}\left(r+\frac12-\frac R2\right).\]

The angle from the optical axis is \(\vartheta = 2\arctan\sqrt{i'^2+j'^2}\). At the top and bottom image boundaries on the vertical centreline, it is $F/2$, which explains the factor $F/4$ in the tangent. The Rust camera’s parameter $\alpha$ uses the half-angle convention, $\alpha=F/2$; the paper also uses a half-angle, but along the horizontal direction.

Now for the time direction. Seeing relativity I writes the arriving photon’s wave vector as proportional to $u-N$, where $N$ points from the observer toward the source. With observed energy $\omega>0$, its future-directed four-momentum is $k^\mu = \omega(u^\mu-N^\mu)$. To follow the same path back to its source, we reverse the whole vector and choose a convenient normalization:

\[p^\mu = -\frac{k^\mu}{\omega} = N^\mu - u^\mu.\]

This tracing tangent is null, $p\cdot p = -1+1=0$, and past-directed, $u\cdot p=-1$. In the camera frame its components are $(-1,N_x,N_y,N_z)$: increasing the tracing parameter moves into the observer’s past and toward the source. The integrator receives this $p^\mu$. The vector $u+N$ is also null, but describes a photon emitted outward by the camera; it cannot generally replace the past-directed ray when computing the image or Doppler shifts.

The interactive visualization below shows how the pixel grid maps to directions on the unit sphere. Drag to rotate the view. Equal-size image pixels do not cover equal solid angles on the sky; their angular footprints shrink away from the optical axis.

These components $p^\mu$, expressed in the chosen coordinate chart, together with the camera position are the initial conditions fed into the geodesic equation. From here the numerical integrator takes over and traces the photon’s path through the curved spacetime.

Orientation and motion

Two more physical ingredients are worth noting.

The camera can be oriented in any direction by rotating the spatial tetrad vectors $e_x$, $e_y$, $e_z$ before computing the rays. This works the same way as rotating a camera in flat space.

If the observer is moving relative to a chosen local reference observer, the tetrad must be Lorentz-boosted accordingly. A moving observer sees stars shifted toward the direction of motion, an effect known as relativistic aberration. Applying the boost to all four tetrad vectors before generating $p=N-u$ includes this effect in the traced image.

In the next demo, the dots represent fixed sources in a reference sky. The velocity slider changes their apparent directions for an observer moving along $+z$; the dots are not fixed pixels in that moving observer’s camera. If $\mathbf{n}$ points toward a source in the reference frame, its observed direction is

\[n'_z = \frac{n_z+v}{1+v n_z}, \qquad n'_{x,y} = \frac{n_{x,y}}{\gamma(1+v n_z)}, \qquad \gamma = \frac{1}{\sqrt{1-v^2}}.\]

Mapping a fixed camera pixel back to a direction in the reference sky uses the inverse transformation, obtained by replacing $v$ with $-v$ and swapping primed and unprimed directions.

Example: observer moving along the $z$-axis

Consider an observer moving along the $z$-axis with velocity $v$. The Lorentz boost gives the four-velocity

\[u^\mu = (\gamma, 0, 0, \gamma v), \quad \gamma = \frac{1}{\sqrt{1 - v^2}}.\]

The spatial tetrad vectors $e_x$ and $e_y$ are unaffected by the boost, but $e_z$ picks up a timelike component:

\[e_x^\mu = (0, 1, 0, 0), \quad e_y^\mu = (0, 0, 1, 0), \quad e_z^\mu = (\gamma v, 0, 0, \gamma).\]

Checking: $e_z^\mu u_\mu = 0$ and $e_z^\mu (e_z)_\mu = -1$ still hold. The mixing of the time and space components produces aberration for off-axis sources, which shift toward $+z$. Sources exactly on the $+z$ or $-z$ axis stay on that axis.

Initial conditions in the Schwarzschild geometry

With the general framework in place, let’s apply it to the geometry we actually care about. For the concrete case of Schwarzschild spacetime, we need to specify an explicit tetrad. The Schwarzschild metric in spherical coordinates $(t, r, \theta, \varphi)$ is

\[\mathrm{d}s^2 = f(r)\,\mathrm{d}t^2 - \frac{\mathrm{d}r^2}{f(r)} - r^2\mathrm{d}\theta^2 - r^2\sin^2\theta\,\mathrm{d}\varphi^2, \qquad f(r) = 1 - \frac{2M}{r}.\]

A first guess for the tetrad would be to simply normalise the coordinate basis, which gives a timelike vector proportional to $\partial_t$ and spatial vectors proportional to $\partial_r$, $\partial_\theta$, $\partial_\varphi$. This works, but only outside the horizon where $f(r) > 0$. Inside the horizon $f(r)$ changes sign, so $\partial_t$ becomes spacelike and $\partial_r$ becomes timelike. The tetrad no longer has the right signature.

A better choice, following Seeing relativity I, is the tetrad associated to a freely falling observer starting from rest at infinity with zero angular momentum. This is known as the rain frame. (The paper calls it a Zero Angular Momentum Observer (ZAMO), but that term more commonly refers to the non-infalling locally non-rotating observer, which in Schwarzschild is just the static one.) Its explicit components in Schwarzschild coordinates (Eq. 8 of the paper) are

\[e_t^\mu = \left(\frac{1}{f(r)},\ -\sqrt{\frac{2M}{r}},\ 0,\ 0\right), \qquad e_r^\mu = \left(-\frac{\sqrt{2M/r}}{f(r)},\ 1,\ 0,\ 0\right),\] \[e_\theta^\mu = \left(0,\ 0,\ \frac{1}{r},\ 0\right), \qquad e_\varphi^\mu = \left(0,\ 0,\ 0,\ \frac{1}{r\sin\theta}\right).\]

This freely falling physical frame extends across the horizon, but these particular coordinate components do not: the factors $1/f(r)$ diverge at $r=2M$ because Schwarzschild coordinates are singular there. To represent the frame at the horizon, use a chart regular across it, such as ingoing Eddington–Finkelstein coordinates. The angular basis also needs a different choice at the polar axis, where $\sin\theta=0$.

In practice the observer placed in front of the scene is not necessarily freely falling. The rain-frame tetrad therefore serves as a reference frame: the actual observer velocity is applied on top of it via a Lorentz boost, exactly as described in the moving observer example above. The boost transforms the freely-falling frame into the observer’s own frame before the rays are generated.