10 March 2019

Recently I (re)discovered ShaderToy (warning: lag!) and, after getting to grips with the main method of 3D rendering used on the site, I was planning to write a quick guide to go over the basic techniques. However, I then found that some people had already done that far better than I could have hoped to, so I thought I’d just make a briefer post showing what I’d done. (Honestly, I wish I had found that guide when I started looking – all I had to go off were code comments and Inigo Quilez’ superb but far more advanced writings!)

This was the first real scene I drew, and it stayed mostly the same throughout my experimentation with raymarching. It is a plane and a sphere, lit only with ambient lighting, and coloured according to the X and Z coords of the point to create a checkerboard. Seeing this work for the first time was great!

I was mainly interested in modelling light, so I began to work refraction and reflection into the renderer. Doing that on a GPU is a little odd as there is no recursion. After some work I got this…

…which I thought was correct for a long time. Turns out I’d never seen a glass sphere before because it should have looked like this.

As I said, the implementation felt a bit clunky and could no doubt be improved. I baked refraction and total internal reflection into one routine myrefract. The refraction consists of essentially re-casting the ray from the surface point with the refracted direction. However, the SDF is negative inside solids, and in my implementation, rays travel backwards when the SDF is negative! So I had to keep track of whether we were inside or outside solids to correctly choose the new direction…

vec3 myrefract(vec3 dir, vec3 norm, float ratio) {
    vec3 v = refract(dir, norm, ratio);
    if (v == vec3(0.0)) v = reflect(dir, norm);
    return v;
}

vec3 render(vec3 start, vec3 dir) {

    // ...

    for (int i=0; i<3; i++) {

        // ...

        if (refr > 0.0) {
            vec3 next = myrefract(dir, norm, inside ? 1.0/refr : refr);
            // have we refracted into the surface, ie no TIR?
            if (dot(next, norm) < 0.0) {
                dir = -next;
                // norm should point towards pos so start is on opp side of surface
                if (inside) norm = -norm;
                inside = !inside;
                col *= refrcol;
            } else {
                dir = next;
                // norm should point away from pos
                if (!inside) norm = -norm;
            }
            start = pos - 0.02*norm;
            continue;
            }
        }

        // ...

    }
}

Anyway, I was happy enough with that so I started on more typical light behaviour. After reading about the Phong reflection model (which works wonderfully well for its simplicity) I managed to render these.

This second image uses ambient occlusion to get the slight shadow beneath the spheres.

float calcAO(vec3 o, vec3 n) {
    float sum = 0.0;
    float mul = 1.0;
    for (int i=1; i<=5; i++) {
        float fi = float(i);
        vec3 p = o + 0.1*fi*n;
        // scene(p).x is the SDF evaluated at p
        sum += (0.1*fi - scene(p).x)*mul;
        mul *= 0.4;
    }
    return clamp(sum, 0.0, 1.0);
}

Then I played around with drawing reflections within the framework of the Phong model. To do that, I casted a reflected ray back into the scene from the point on the reflective surface and added (some colour-masked multiple of) the rendered colour of the hit point. Recasting the ray was done with the continue statement to return to the top of the for loop, but only after altering mult, which is the colour multiplier for whatever colour the ray finds.

if (refl > 0.0) {
    start = pos + 0.01*norm;
    dir = reflect(dir, norm);
    mult *= col*refl;
    continue; // restart raymarching
}

I wasn’t too happy with how this looked though, so I may work on that in the future.