## Thursday, 6 January 2011

### Ray traced Google Robot in WebGL

It seems like everybody is rendering with distance fields these days. I'm somewhat late to the party, but distance fields do have a lot of interesting advantages:
- you can ray-cast them very quickly using sphere tracing
- it's very easy to do CSG operations on them (union, difference etc.)
- there's a clever fast approximation to ambient occlusion (originally due to Alex Evans, I think).

My original plan was do to a real-time version of the original light cycle from Tron (which is apparently comprised of only 57 geometric primitives), but that turned out to be a bit complicated. When I saw the Google Android robot, it seemed like a more suitable target (it's very easy to calculate the distance to spheres and capsules, and you can see the result above.

Anyway, the code is below, you can paste this into Iniqo Quielz's excellent ShaderToy and play with it yourself using WebGL. (It's somewhat better commented than most of the example shaders there, too.) You'll need the latest Chrome or Firefox beta to run this, there's a good FAQ here.

// distance field ray caster
// simon green 06/01/2011
//
// based on Iniqo Quilez's:
// http://www.iquilezles.org/www/material/nvscene2008/rwwtt.pdf
//
// Google Android robot:
// http://www.android.com/branding.html

#ifdef GL_ES
precision highp float;
#endif

uniform vec2 resolution;
uniform float time;

// CSG operations
float _union(float a, float b)
{
return min(a, b);
}

float intersect(float a, float b)
{
return max(a, b);
}

float difference(float a, float b)
{
return max(a, -b);
}

// primitive functions
// these all return the distance to the surface from a given point

float plane(vec3 p, vec3 planeN, vec3 planePos)
{
return dot(p - planePos, planeN);
}

float box(vec3 p, vec3 abc )
{
vec3 di=max(abs(p)-abc, 0.0);
//return dot(di,di);
return length(di);
}

float sphere(vec3 p, float r)
{
return length(p) - r;
}

// capsule in Y axis
float capsuleY(vec3 p, float r, float h)
{
if (p.y < 0.0) {
return length(p) - r;
} else if (p.y > h) {
return length(p - vec3(0, h, 0)) - r;
} else {
return length(vec2(p.x, p.z)) - r;
}
}

// given segment ab and point c, computes closest point d on ab
// also returns t for the position of d, d(t) = a + t(b-a)
vec3 closestPtPointSegment(vec3 c, vec3 a, vec3 b, out float t)
{
vec3 ab = b - a;
// project c onto ab, computing parameterized position d(t) = a + t(b-a)
t = dot(c - a, ab) / dot(ab, ab);
// clamp to closest endpoint
t = clamp(t, 0.0, 1.0);
// compute projected position
return a + t * ab;
}

// generic capsule
float capsule(vec3 p, vec3 a, vec3 b, float r)
{
float t;
vec3 c = closestPtPointSegment(p, a, b, t);
return length(c - p) - r;
}

float cylinderY(vec3 p, float r, float h)
{
float d = length(vec2(p.x, p.z)) - r;
d = difference(d, plane(p, vec3(0.0, -1.0, 0.0), vec3(0.0, h, 0.0)));
d = difference(d, plane(p, vec3(0.0, 1.0, 0.0), vec3(0.0)));
return d;
}

// transforms
vec3 rotateX(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
vec3 r;
r.x = p.x;
r.y = ca*p.y - sa*p.z;
r.z = sa*p.y + ca*p.z;
return r;
}

vec3 rotateY(vec3 p, float a)
{
float sa = sin(a);
float ca = cos(a);
vec3 r;
r.x = ca*p.x + sa*p.z;
r.y = p.y;
r.z = -sa*p.x + ca*p.z;
return r;
}

float halfSphere(vec3 p, float r)
{
return difference(
sphere(p, r),
plane(p, vec3(0.0, 1.0, 0.0), vec3(0.0)) );
}

// distance to scene
float scene(vec3 p)
{
//return box(p, vec3(1.0, 1.0, 1.0));
//return sphere(p, vec3(0.0), 1.0);

float d = 1e10;

p -= vec3(0.0, 1.0, 0.0);
vec3 hp = rotateY(p, sin(time*0.5)*0.5);

//d = sphere(p, 1.0);
d = halfSphere(hp, 1.0);

// eyes
d = _union(d, sphere(hp - vec3(-0.3, 0.3, 0.9), 0.1));
d = _union(d, sphere(hp - vec3(0.3, 0.3, 0.9), 0.1));

// antenna
d = _union(d, capsule(hp, vec3(-0.4, 0.7, 0.0), vec3(-0.75, 1.2, 0.0), 0.05));
d = _union(d, capsule(hp, vec3(0.4, 0.7, 0.0), vec3(0.75, 1.2, 0.0), 0.05));

// body
d = _union(d, capsuleY((p*vec3(1.0, 4.0, 1.0) - vec3(0.0, -4.6, 0.0)), 1.0, 4.0));
//d = _union(d, cylinderY(p - vec3(0.0, -1.1, 0.0), 1.0, 1.0));

// arms
//d = _union(d, capsuleY(p - vec3(-1.2, -0.9, 0.0), 0.2, 0.7));
//d = _union(d, capsuleY(p - vec3(1.2, -0.9, 0.0), 0.2, 0.7));
d = _union(d, capsuleY(rotateX(p, cos(time)) - vec3(-1.2, -0.9, 0.0), 0.2, 0.7));
d = _union(d, capsuleY(rotateX(p, sin(time)) - vec3(1.2, -0.9, 0.0), 0.2, 0.7));

// legs
d = _union(d, capsuleY(p - vec3(-0.4, -1.8, 0.0), 0.2, 0.5));
d = _union(d, capsuleY(p - vec3(0.4, -1.8, 0.0), 0.2, 0.5));

// floor
p += vec3(0.0, 1.0, 0.0);
d = _union(d, plane(p, vec3(0.0, 1.0, 0.0), vec3(0.0, -1.0, 0.0)));

return d;
}

// calculate scene normal
vec3 sceneNormal( in vec3 pos )
{
float eps = 0.0001;
vec3 n;
n.x = scene( vec3(pos.x+eps, pos.y, pos.z) ) - scene( vec3(pos.x-eps, pos.y, pos.z) );
n.y = scene( vec3(pos.x, pos.y+eps, pos.z) ) - scene( vec3(pos.x, pos.y-eps, pos.z) );
n.z = scene( vec3(pos.x, pos.y, pos.z+eps) ) - scene( vec3(pos.x, pos.y, pos.z-eps) );
return normalize(n);
}

// ambient occlusion approximation
float ambientOcclusion(vec3 p, vec3 n)
{
const int steps = 3;
const float delta = 0.5;

float a = 0.0;
float weight = 1.0;
for(int i=1; i<=steps; i++) {
float d = (float(i) / float(steps)) * delta;
a += weight*(d - scene(p + n*d));
weight *= 0.5;
}
return clamp(1.0 - a, 0.0, 1.0);
}

void main(void)
{
vec2 pixel = -1.0 + 2.0 * gl_FragCoord.xy / resolution.xy;

// compute ray origin and direction
float asp = resolution.x / resolution.y;
vec3 rd = normalize(vec3(asp*pixel.x, pixel.y, -2.0));
vec3 ro = vec3(0.0, 0.5, 4.5);

float a = time*0.5;
//rd = rotateY(rd, a);
//ro = rotateY(ro, a);

float t = 1.0;
bool hit = false;
vec3 pos;

// cast ray (sphere tracing)
for(int i=0; i<64; i++)
{
pos = ro + t*rd;
float d = scene(pos);
if (d < 0.01) {
hit = true;
break;
}
t += d;
}

vec3 rgb;
if(hit)
{
// calc normal
vec3 n = sceneNormal(pos);

// lighting
const vec3 lightPos = vec3(5.0, 10.0, 5.0);
const vec3 color = vec3(0.643, 0.776, 0.223);
const float shininess = 100.0;

vec3 l = normalize(lightPos - pos);
vec3 v = normalize(ro - pos);
vec3 h = normalize(v + l);
float diff = dot(n, l);
float spec = max(0.0, pow(dot(n, h), shininess)) * float(diff > 0.0);
//diff = max(0.0, diff);
diff = 0.5+0.5*diff;

float fresnel = pow(1.0 - dot(n, v), 5.0);
float ao = ambientOcclusion(pos, n);

rgb = vec3(diff*ao) * color + vec3(spec + fresnel*0.5);
//rgb = vec3(ao);
//rgb = vec3(fresnel);

} else {
rgb = mix(vec3(1.0), vec3(0.0), rd.y);
}

// vignetting
rgb *= 0.5+0.5*smoothstep(2.0, 0.5, dot(pixel, pixel));

gl_FragColor=vec4(rgb, 1.0);
}

dlai said...

Cool stuff. I like that you put so much effort into comments :).
I was about to write a long essay about some bugs (probably due to CG/HLSL->GLSL conversion) which were in the code that popped up in my RSS reader, but it turns out you fixed them on your site. Anyway nice job!

Simon Green said...

FYI, it looks like the latest Firefox and Chrome betas both use the ANGLE library, which translates OpenGL ES shaders to DX9 and breaks a lot of complex shaders like this in process!

It sounds like this was done to work around broken Intel OpenGL drivers. Thanks, Intel!

https://bugzilla.mozilla.org/show_bug.cgi?id=603367

Simon Green said...

Apparently you can fix this in Firefox 4.0b8 like so:

Type "about:config" into the address bar, right click in the window -> New -> Boolean
Name: "webgl.prefer_gl"
Value: "true"

NeARAZ said...

Nice! Just for kicks, I took the shader and tested it on iPhone 3Gs - works like a charm! Of course, runs at about 1 second/frame, and it does look weird to see Android logo running on iPhone... but fun ;)

Joakim said...

Nice shader! I was wondering if it would be ok if I added the shader as a scene to the music visualizer/screensaver Plane9?

Simon Green said...

Sure, go for it. The version in the post above is a bit more exciting, btw.

Joakim said...

Thanks! I replaced the background with a 'real' background and could adjust the ray iteration because of it to get a good 5x speed boost when viewed on a dual monitor setup!

Lakatusz said...

Hey Simon! I dont know your nation so i'm writing in english. Sorry for faults.
I'm living in Budapest (Hungary). I've found a 2GB slim stick while i walked in the Dayka Gabor street. There are in the stick a lot of things that i dont understand, but by this things i investigated that how i give back the stick's contain to you. I think maybe it's important to you, so if you need this plz write a mail to csabqa@hotmail.com.
Have a nice day, Csabqa.

Simon Green said...

@Lakatusz - wow thanks I've been looking for that thumb drive. Where do I wire the money too?

Lakatusz said...
This comment has been removed by the author.
Lakatusz said...

Hey, Simon!