ACG: Ray Marching

This is a full example of doing ray marching from scratch in GLSL. Everything is done in a single fragment shader using all the advanced tricks and techniques we learned in class.

/* Constants defined at the top. */
const float NEAR_CLIPPING_PLANE  = 0.1;
const float FAR_CLIPPING_PLANE   = 10.0;
const int MARCHING_STEPS         = 100;
const float EPSILON              = 0.0001;
const float DISTANCE_BIAS        = 1.0;
const vec3 LIGHT_COL             = vec3( 2.5 );

float sdSphere( vec3 p, float r ) {
   return length(p) - r;
}
float sdBox( vec3 p, in vec3 b ) {
   vec3 q = abs(p) - b;
   return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}
float sdTorus( vec3 p, vec2 t ) {
  vec2 q = vec2(length(p.xy)-t.x,p.z);
  return length(q)-t.y;
}
float sdPlane( vec3 p, vec4 n ) {
   return dot( p, n.xyz ) + n.w;
}
vec2 sdUnion( vec2 a, vec2 b ) {
   return (a.x < b.x) ? a : b;
}
float sdSmoothUnion( float a, float b, float k ) {
   float h = max( k-abs(a-b), 0.0 )/k;
   return min( a, b ) - h*h*k*(1.0/4.0);
}

/* The scene is defined here, and is a function of time (u_time uniform) */
vec2 scene( vec3 pos )
{
   float ds = sdSphere( vec3(pos) + vec3(0.0,-0.05,0.0), 0.25 );
   float dx = sdTorus( vec3(pos) + vec3(0.0,-0.05,0.0), vec2(0.2, 0.05));
   float db = sdBox(pos - vec3(0.0, -0.25,0.0), vec3(0.2,0.05,0.2));
   float dp = sdPlane( vec3(pos), vec4( 0.0, 1.0, 0.0, 0.25 ) );

   vec2 dtop = mix( vec2(ds,0.0), vec2(dx,1.0), sin(u_time)*0.5+0.5 );
   float dbase = sdSmoothUnion(db,dp,0.1);
   return sdUnion( vec2(dbase,2.0), dtop );
}

/* Computes the normals using a tetrahedron for efficiency. */
vec3 normal( vec3 pos )
{
   const float e = 0.0001;
   const vec2 k = vec2(1.0,-1.0);
   return normalize(
      k.xyy*scene( pos + k.xyy*e ).x +
      k.yyx*scene( pos + k.yyx*e ).x +
      k.yxy*scene( pos + k.yxy*e ).x +
      k.xxx*scene( pos + k.xxx*e ).x
   );
}

/* The heart of our raymarching approach. */
vec2 raymarch( vec3 ro, vec3 rd )
{
   vec2 t = vec2( NEAR_CLIPPING_PLANE, 0.0 );
   for (int i=0; i < MARCHING_STEPS; i++) {
      vec3 pos = ro + t.x*rd;

      vec2 h = scene(pos);
      if (h.x < EPSILON)
         break;
      t.x += h.x * DISTANCE_BIAS;
      t.y = h.y;
      if (t.x > FAR_CLIPPING_PLANE)
         break;
   }
   if (t.x > FAR_CLIPPING_PLANE)
      t = vec2( -1.0, 0.0 );
   return t;
}

/* We are approximating soft shadows for "free" with raymarching. */
float softshadow( vec3 ro, vec3 rd, float k )
{
   float res = 1.0;
   float ph = 1e20;
   float t = NEAR_CLIPPING_PLANE;
   for (int i=0; i < MARCHING_STEPS; i++) {
      float h = scene( ro+t*rd ).x;
      if (h < EPSILON)
         return 0.0;
      float y = h*h/(2.0*ph);
      float d = sqrt(h*h-y*y);
      res = min( res, k*d/max(0.0,t-y) );
      ph = h;
      t += h * DISTANCE_BIAS;
      if (t > FAR_CLIPPING_PLANE)
         break;
   }
   return res;
}

/* This is an analytically anti-aliased checkerboard pattern. */
float checkerboard(vec2 p)
{
   vec2 w = fwidth(p) + 0.001;
   vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w;
   return 0.5 - 0.5*i.x*i.y;
}

void main ()
{
   /* Light is a point light with no falloff that moves around based on time. */
   vec3 LIGHT_POS = 3.0 * vec3( sin(u_time*0.7), 1.0, cos(u_time*0.7) );

   /* Very simple orthogonal camera model. Rays go in the negative z-axis. */
   vec2 uv = (2.0*gl_FragCoord.xy -  u_resolution.xy) / u_resolution.y;
   vec3 ro = vec3(0.0,0.0,1.0);
   vec3 rd = normalize( vec3(uv, -1.5) );

   /* Raymarch the scene here. */
   vec2 t = raymarch( ro, rd );

   vec3 col; /* We determine the colour from the material. */
   if (t.x > 0.0) {
      /* Determine the colour mased on material (stored in t.y) */
      vec3 pos = ro + t.x * rd;
      vec3 nor = normal( pos );
      vec3 light_dir = normalize(LIGHT_POS - pos);
      vec3 mat;
      /* For the floor we use the checkerboard pattern. */
      if (t.y == 2.0)
         mat = vec3(checkerboard( pos.xz*20.0 ) * 0.2 + 0.7) * 0.2;
      /* Otherwise we interpolate between two different colours. */
      else
         mat = mix( vec3(0.2,0.0,0.0), vec3(0.0,0.2,0.0), t.y );

      /* Shadows computed with raymarching. */
      float sha = softshadow( pos + nor*0.001, light_dir, 8.0 );
      /* Lighting is simple based on a point light. */
      float sun = max( dot( nor, light_dir ), 0.0 );
      vec3 lin  = sun*LIGHT_COL * pow(vec3(sha),vec3(1.5));

      /* Ilumination and material are multiplied together. */
      col = lin * mat;
   }
   /* Ray did not hit anything so we draw "the sky" */
   else
      col = vec3(0.4,0.5,0.75) - 0.4*rd.y;

   /* Gamma correction. */
   col = pow( col, vec3(1.0/2.2) );
   colour_out = vec4(col,1.0);
}