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);
}