Skip to content
Rendering · Rendering methodsIntermediate

Raymarching

Draw a 3D scene with no triangles. Shoot a ray per pixel and step along it by the distance to the nearest surface.

Live demo coming soon

How it works

The usual pipeline starts from triangles: the vertex shader places them, the rasterizer finds the pixels they cover, and the fragment shader colors those pixels. Raymarching turns this around. It draws one rectangle that covers the screen, and the fragment shader does all the 3D work itself. For each pixel it shoots a ray from the camera into the scene and searches along it for the first surface.

The scene isn’t a mesh. It is a function.

Scenes as distance functions

A signed distance function (SDF) takes a point in space and returns how far that point is from the nearest surface: positive outside, negative inside, zero exactly on it. A sphere of radius r at the origin is one line:

float sdSphere(vec3 p, float r) {
    return length(p) - r;
}

Boxes, tori and capsules have short formulas too, and they combine: the min of two distances is both shapes at once, the max is where they overlap. A whole scene becomes one function, usually called map(p).

Sphere tracing

The distance is what makes the search fast. If map(p) returns 2.0, nothing in the scene is closer than 2.0 to p, in any direction. So the ray can jump 2.0 forward without passing through anything. At the new point, ask again and jump again. Far from everything the steps are long; as the ray nears a surface they shrink, and when the distance falls below a small threshold, the ray has hit.

#version 300 es
precision highp float;

uniform vec2 uResolution;
out vec4 fragColor;

float map(vec3 p) {
    return length(p) - 1.0; // one unit sphere at the origin
}

void main() {
    // a camera at z = 3 looking toward -z, one ray per pixel
    vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
    vec3 origin = vec3(0.0, 0.0, 3.0);
    vec3 dir = normalize(vec3(p, -1.5)); // -1.5 sets the field of view

    float t = 0.0;      // distance travelled along the ray
    bool hit = false;
    for (int n = 0; n < 96; n++) {
        float d = map(origin + dir * t);
        if (d < 0.001) { hit = true; break; }
        t += d;
        if (t > 20.0) break; // left the scene
    }

    vec3 color = vec3(0.05);
    if (hit) color = vec3(1.0);
    fragColor = vec4(color, 1.0);
}

This draws a white disc on a dark background: the sphere, found, but not yet shaded.

Normals from the gradient

Lighting needs the surface normal. A mesh stores it per vertex; an SDF gives it for free in a different way. The distance grows fastest straight away from the surface, so the direction in which map increases, its gradient, is the normal. Sampling map a tiny step either side on each axis estimates it:

vec3 normalAt(vec3 p) {
    const vec2 e = vec2(0.001, 0.0);
    return normalize(vec3(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)
    ));
}

That costs six more calls to map. A cheaper version samples the four corners of a tetrahedron instead. With a normal and a light direction, the dot product shades the hit as on any mesh:

vec3 n = normalAt(origin + dir * t);
float diffuse = max(dot(n, normalize(vec3(0.6, 0.8, 0.4))), 0.0);

What it costs

Every pixel runs the loop, and every step evaluates the whole scene. Cost is pixels × steps × the price of map. Rays that skim past a surface without touching it are the expensive ones: the distance stays small, so the steps stay small. The usual levers are a step limit, a hit threshold that grows with distance, a maximum ray length, and a lower rendering resolution.

The method also depends on map telling the truth. A function that overestimates the distance (common after twisting or bending space) makes the ray jump past thin surfaces, which then show holes. Scaling every step by 0.5 to 0.9 trades speed for safety.

What it buys is just as real. Soft shadows and ambient occlusion come from a few more marches toward the light or along the normal. Infinite repetition is a mod on the input point. Shapes melt into each other with a smooth min. None of that is easy with triangles.

Meeting a mesh scene

On its own, a raymarched shape doesn’t know about the meshes around it. To let them hide each other, the shader writes the hit’s depth (gl_FragDepth in GLSL, DEPTH in Godot) so the depth test can compare it with the rest of the scene. Without that, raymarching suits full-screen demos, backgrounds, and effects inside a box or quad.

In Godot

Any shader can loop, so the same code works in a canvas_item shader on a ColorRect (2D, full screen) or a spatial shader on a cube, where the ray starts at the camera and passes through each covered pixel. In the spatial case, writing DEPTH lets the result sit inside a 3D scene.

On this site

The Sphere–box smooth blend card is a full-screen raymarcher. Its inputs are UV and time; it also reads i.resolution for the aspect ratio, and builds the camera ray the same way, starting from i.uv instead of gl_FragCoord.

Further reading