This shader was created for the Shadertoy Competition 2017. The shader is a tutorial about raymarching distance fields (using a ray marcher in Shadertoy).

Click on the shader and press space to go to the next slide.

The Shadertoy Competition 2017 was an event of three weeks during which you had to create a new shader every week. The subject of the third week was “The Shader Professor” – we were asked to make a shader that explained an idea, a paper or technique.

I created the shader above: a tutorial that teaches how to render a 3D-scene in Shadertoy using distance fields.

Note that a shader is not the ideal medium to explain things, so you can find much better tutorials about raymarching elsewhere on the internet. See for example the links in the “Further reading” section below.

Nevertheless, I think that this shader looks good and is interesting (and meta) enough to write down its transcription (see below). I won the challenge of the third week (and ended fifth overall).

Raymarching Distance Fields

In this tutorial, you will learn how to render a 3D-scene in Shadertoy using distance fields.

As an example, we will create this black and white scene of three spheres on a plane.

Create a ray

First, we create a ray.
The ray origin (ro) will be at (0,0,1).

In code:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec3 ro = vec3(0, 0, 1);Code language: GLSL (glsl)

Now we place a virtual screen in the scene.
It is located at the origin and has dimensions of aspect_ratio x 1.

We compute the ray direction (rd) for each pixel (fragCoord.xy) of our virtual screen.

In code:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec3 ro = vec3(0, 0, 1);

    vec2 q = (fragCoord.xy - .5 * iResolution.xy ) / iResolution.y;
    vec3 rd = normalize(vec3(q, 0.) - ro); Code language: GLSL (glsl)

Distance fields

A distance field is used to find the intersection of our ray (ro, rd) and the spheres and plane of the scene.

A distance field is a function that gives an estimate (a lower bound of) the distance to the closest surface at any point in space.

The distance function for a sphere is the distance to the center of the sphere minus the radius of the sphere.

The code for a sphere located at (-1,0,-5):

float map(vec3 p) {
    float d = distance(p, vec3(-1, 0, -5)) - 1.;Code language: GLSL (glsl)

We combine different distance functions by taking the minimum value of these functions.

In code:

float map(vec3 p) {
    float d = distance(p, vec3(-1, 0, -5)) - 1.;
    d = min(d, distance(p, vec3(2, 0, -3)) - 1.);
    d = min(d, distance(p, vec3(-2, 0, -2)) - 1.);Code language: GLSL (glsl)

The total distance function for this scene (including the plane) is given by:

float map(vec3 p) {
    float d = distance(p, vec3(-1, 0, -5)) - 1.;
    d = min(d, distance(p, vec3(2, 0, -3)) - 1.); 
    d = min(d, distance(p, vec3(-2, 0, -2)) - 1.);
    d = min(d, p.y + 1.);
    return d;
}Code language: GLSL (glsl)

Now we can march the scene from ro in direction rd.
Each step size is given by the distance field.

We stop the march when we find an intersection:

    float h, t = 1.;
    for (int i = 0; i < 256; i++) {
        h = map(ro + rd * t);
        t += h;
        if (h < 0.01) break;
    }Code language: GLSL (glsl)

Lighting a ray

Now that we have found the intersection
(p = ro + rd * t) for our ray, we can give the scene some lighting.

To apply diffuse lighting we have to calculate the normal of shading point p.

The normal can be calculated by taking the central differences on the distance field:

vec3 calcNormal(in vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005;
    return normalize(
        e.xyy * map(p + e.xyy) +
        e.yyx * map(p + e.yyx) +
        e.yxy * map(p + e.yxy) +
        e.xxx * map(p + e.xxx));
}Code language: GLSL (glsl)

We calculate the diffuse lighting for a point light at position (0,2,0).

In code:

    if (h < 0.01) {
        vec3 p = ro + rd * t;
        vec3 normal = calcNormal(p);
        vec3 light = vec3(0, 2, 0);
        
        float dif = clamp(dot(normal, normalize(light - p)), 0., 1.);
        dif *= 5. / dot(light - p, light - p);
        
        fragColor = vec4(vec3(pow(dif, 0.4545)), 1);
    }Code language: GLSL (glsl)

And we are done!

Adding ambient occlusion, (fake) reflections, soft shadows, fog, ambient lighting and specular lighting is left as an exercise for the reader.

Full source code

You can find (the full source of) the fragment shader on Shadertoy:

Further reading

Similar posts

If you like this post, you may also like one of my other posts:

Raymarching distance fields
Tagged on: