About
This is a fast general relativistic ray-tracing engine built with Rust. The long-term goal is to support relativistic visualisation (e.g. black hole shadows, gravitational lensing, and eventually radiative transfer effects like attenuation) in arbitrary spacetime geometries.
All quantities are in geometrized units ($G=c=1$).
Installation
CLI
cargo install nullgeo-cli
This installs the nullgeo binary. Build with --features parallel to enable Rayon-based parallel ray tracing:
cargo install nullgeo-cli --features parallel
Library
cargo add nullgeo
Or in Cargo.toml:
[]
= "0.1"
Enable the optional parallel feature for Rayon support: nullgeo = { version = "0.1", features = ["parallel"] }.
Example Usage (with CLI)
e.g. Schwarzschild shadow, $512 \times 512$, camera at $x=-30$
nullgeo shadow \
--metric=schwarzschild \
--mass=1.0 \
--width=512 --height=512 \
--fov-deg=30.0 \
--cam-x=-30.0 \
--dl=0.005 \
--max-steps=20000 \
--out=shadow.pgm
Theory
Hamiltonian Formulation
The Hamiltonian for photons in GR is
$H(x, p) = \frac{1}{2} g^{\mu \nu}(x) p_{\mu} p_{\nu}$
together with the null constraint $H=0$. Hamilton's equations give the trajectory:
$$ \dot{x}{\mu} = \frac{\partial H}{\partial p_{\mu}} = g{\mu \nu} p_{\nu}, \quad \dot{p}{\mu} = -\frac{\partial H}{\partial x^{\mu}} = -\frac{1}{2} \partial{\mu} g^{\rho \sigma} p_{\rho} p_{\sigma} $$ where the dot signifies differentiation with respect to an affine parameter $\lambda$.
Pinhole Camera and the Local Orthonormal Frame
A local tetrad ${ e^{\mu}_{a} }$ is constructed at the camera's position. Note that the Latin $a$ in this notation labels the vector in the basis, and is not an index.
The spatial unit vectors $e{\mu}_i$ are constructed by projecting the coordinate basis $\partial_i$ onto the 3-plane orthogonal to the timelike unit vector $e{\mu}_0$ ($\sim u$) and carrying out Gram-Schmidt orthogonalization with the induced metric $h:= g + u \otimes u$ (see e.g. Wald).
A photon with spatial direction $\mathbf{n}$ and energy $E$ in the local tetrad has four momentum $pa = E(1, ni)$. The covariant components in the coordinate basis can then be obtained via $p_{\mu} = g_{\mu \nu} {e_a}{\nu} pa$.
Numerics
We use Rayon's par_iter_mut() over the image buffer. For each pixel, a worker:
- Initializes the ray state by computing the photon's null covector at the camera position.
- Integrates the equations of motion using a 4th-order Runge-Kutta scheme to obtain the trajectory $(x^{\mu}(\lambda), p_{\mu}(\lambda))$.
- Applies termination checks, e.g. stopping if the ray falls inside the horizon.
- Writes the pixel value to the grayscale image buffer.