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:

  1. 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}\]

  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)\]

  3. 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.