loading...
         
WebGL shader implementation of ray marching a signed distance field. Based on explanation by loopit.

Video

The shape is defined as a signed distance field (SDF), a function that for every point P returns the distance to the surface of the shape. A positive value defines the exterior of the surface, 0 defines the surface itself, and negative values are inside the surface. The shape is derived from the SDF of a sphere. The SDF below returns a value for every point in 3D space P, signifying the distance to the surface of a sphere placed at the point C with a radius of r:
sphere(P) = distance(P, C) - r           P,C ∈ ℝ³ , r ∈ ℝ
The shape is then modified by displacing the center of the sphere based on the position of P, which warps the surface shape:
shape(P) = distance(P, warp(P)) - r
The warp function is based on the x and y coordinates of P, with constants ω and 'strength' denoting a frequency and amplitude of a sinusoid displacement, respectively:
          [ sin( ω P.y ) strength ]
warp(P) = [ sin( ω P.x ) strength ]      ω,strength ∈ ℝ
          [ 2                     ]
The shape is animated by adding a time parameter t (seconds):
shape(P, t) = distance(P, warp(P, t)) - r
The warp function is frequency and amplitude modulated by two sinusoids for both x and y, with modulation frequencies fmf0, fmf1, amf0, and amf1:
            [ sin( ω P.y sin( fmf0 t ) ) strength sin( amf0 t ) ]
warp(P,t) = [ sin( ω P.x sin( fmf1 t ) ) strength sin( amf1 t ) ]   amf0,amf1,fmf0,fmf1 ∈ ℝ
            [ 2                                                 ]
This is implemented in a straightforward way in GLSL, replacing constants with numbers:

float sdf(vec3 p)
{
    return distance(p, vec3(
        sin( p.y * sin(2.0 * time) * 20.0 ) * sin( 5.0 * time ) * 0.2,
        sin( p.x * sin(2.7 * time) * 20.0 ) * sin( 3.0 * time ) * 0.2,
        2.0))
        - 2.0;
}
The ray marching algorithm steps into the from a point through a screen pixel. This code is run for every pixel:

vec3 pos = vec3(vPosition.x, vPosition.y, 0.0);
vec3 dir = pos - vec3(0.0, 0.0, -1.0);

// ray marching
float d = 10.0;
for(int i =0; i < 256; i++)
{
    d = sdf(pos);
    pos += direction * d;
    if(d < 0.02 || pos.z > 100.0) break;
}

// close enough to sphere: white, otherwise black
vec3 col;
if(d<=0.02) gl_FragColor = vec3(1,1,1); 
else        gl_FragColor = vec3(0,0,0);
The full shader code is here:

void main(void)
{
    vec3 pos = vec3(vPosition.x, vPosition.y, 0.0);
    vec3 dir = pos - vec3(0.0, 0.0, -1.0);

    // ray marching
    float d = 10.0;
    for(int i =0; i < 256; i++)
    {
        d = sdf(pos);
        pos += direction * d;
        if(d < 0.02 || pos.z > 100.0) break;
    }

    float r = 0.0, g = 0.0, b = 0.0;
    if(d<=0.02)
    {
        // estimate normal based on finite difference approximation of gradient
        vec3 gradient = sdf(pos) - vec3(
            sdf(pos + vec3(.001,.000,.000)),
            sdf(pos + vec3(.000,.001,.000)),
            sdf(pos + vec3(.000,.000,.001))
            );
        vec3 normal = normalize( gradient );

        r = dot( normal, normalize( vec3( 0.5,-0.5, 0.5)) ) * 1.0; //   red light
        g = dot( normal, normalize( vec3(-0.4, 0.4, 0.4)) ) * 0.2; // green light
        b = dot( normal, normalize( vec3( 0.9,-0.3, 0.4)) ) * 0.3; //  blue light
    }
    gl_FragColor = vec4(r, g, b, 1.0);
}