How to Calculate a Torus SDF in GLSL Raymarching
This article explains how to define and evaluate the Signed Distance Function (SDF) of a torus inside a GLSL raymarcher. It covers the mathematical reduction of the 3D volume into a 2D cross-section, the derivation of the distance formula, and the standard GLSL code implementation used in real-time sphere tracing loops.
The Mathematics Behind the Torus SDF
A torus centered at the origin is defined by two parameters:
- Major radius (\(R\)): The distance from the center of the torus to the center of the cross-sectional tube.
- Minor radius (\(r\)): The radius of the tube itself.
Evaluating the distance to a 3D torus can be simplified by taking advantage of its rotational symmetry around a primary axis (conventionally the Y-axis).
Instead of measuring the distance to the full 3D surface directly, the problem is reduced to a 2D distance calculation to a circle:
Radial Projection: Project the query point \(P(x, y, z)\) onto the horizontal plane (\(XZ\)) to determine its radial distance from the central axis: \[\text{dist}_{\text{axis}} = \sqrt{x^2 + z^2}\]
Distance to the Core Circle: Measure the 2D offset from the core circle running through the center of the tube. Horizontally, this offset is \(\text{dist}_{\text{axis}} - R\). Vertically, the offset is simply the \(y\)-coordinate. This forms a 2D vector: \[\vec{q} = (\text{dist}_{\text{axis}} - R, y)\]
Distance to the Surface: The length of \(\vec{q}\) gives the distance from point \(P\) to the nearest point on the core ring. Subtracting the tube radius (\(r\)) produces the signed distance to the torus surface: \[d = \|\vec{q}\| - r\]
If \(d < 0\), the point is inside the torus; if \(d > 0\), it is outside; and if \(d = 0\), it lies exactly on the surface.
GLSL Implementation
In a GLSL fragment shader, this logic is implemented cleanly using vector swizzling and built-in vector math functions:
float sdTorus(vec3 p, vec2 t) {
// t.x = major radius (R)
// t.y = minor radius (r)
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}Integration into the Raymarching Loop
In a sphere tracer, a ray is cast from an origin \(\vec{ro}\) along a normalized direction \(\vec{rd}\). At each step, the scene distance is evaluated using the torus function. The step distance dictates how far the ray can safely advance without passing through geometry:
float map(vec3 p) {
// Defines a torus with R = 1.0 and r = 0.25
vec2 torusDimensions = vec2(1.0, 0.25);
return sdTorus(p, torusDimensions);
}
float raymarch(vec3 ro, vec3 rd) {
float distTraveled = 0.0;
const int MAX_STEPS = 100;
const float SURF_DIST = 0.001;
const float MAX_DIST = 100.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 currentPos = ro + rd * distTraveled;
float d = map(currentPos);
distTraveled += d;
if (d < SURF_DIST || distTraveled > MAX_DIST) {
break;
}
}
return distTraveled;
}Because the mathematical formulation produces an exact Euclidean distance, the raymarcher can step efficiently with optimal step sizes without suffering from overshooting or visual artifacts.