WebGL — 2.1 Ray Tracing with reflections

Sushindhran Harikrishnan
Neosavvy Labs
Published in
4 min readMay 4, 2017

In this post, we’re going to do something slightly more engrossing using the same building blocks we put in place in the previous post. We are going to add multiple spheres to our scene and add reflections.

1. Adding Multiple Spheres

Let’s first add multiple spheres and start by defining a macro for the number of spheres we want.

#define NUM_SPHERES 5

We already defined a struct for our sphere in our previous post. Let’s use that and define 4 new spheres in the initialize() function to add to our scene.

// Create spheres
spheres[0].center = vec3(-0.25, 1.5 * sin(uTime), -0.25);
spheres[0].radius = 0.3;
spheres[0].color = vec3(1.0, 0.0, 0.0);
spheres[1].center = vec3(0.5 * sin(uTime), 0.25, 0.75);
spheres[1].radius = 0.2;
spheres[1].color = vec3(0.0, 1.0, 0.0);
spheres[2].center = vec3(-0.75, 0.0, 0.5);
spheres[2].radius = 0.2;
spheres[2].color = vec3(0.0, 0.0, 1.0);
spheres[3].center = vec3(-0.25 , 0.4 * sin(uTime), 0.1* sin(uTime));
spheres[3].radius = 0.25;
spheres[3].color = vec3(0.0, 1.0, 1.0);
spheres[4].center = vec3(-1.5, 0.15 * sin(uTime), 0.15 * sin(uTime));
spheres[4].radius = 0.35;
spheres[4].color = vec3(1.0, 1.0, 0.0);

2. Drawing the spheres

We now have multiple spheres and we have to draw them on the screen keeping in mind that the nearest sphere to the camera/ray origin gets rendered. So I’ve modified our getIntersection() function from the previous post to be more generic, and take in a sphere and the ray, and just return the intersection point of the ray with that sphere.

float getIntersection(Sphere sphere, Ray ray) {
vec3 sphereCenter = sphere.center;
vec3 colorOfSphere = sphere.color;
float radius = sphere.radius;
vec3 cameraSource = ray.origin;
vec3 cameraDirection = ray.direction;
vec3 distanceFromCenter = (cameraSource - sphereCenter);
float B = 2.0 * dot(cameraDirection, distanceFromCenter);
float C = dot(distanceFromCenter, distanceFromCenter) - pow(radius, 2.0);
float delta = pow(B, 2.0) - 4.0 * C;
float t = 0.0;
if (delta > 0.0) {
float sqRoot = sqrt(delta);
float t1 = (-B + sqRoot) / 2.0;
float t2 = (-B - sqRoot) / 2.0;
t = min(t1, t2);
}
if (delta == 0.0) {
t = -B / 2.0;
}
return t;
}

This function now has to be called for each sphere in the scene, and the smallest intersection point is what is considered in our fragment shader.

for (int i=0; i < NUM_SPHERES; i++) {
float t = getIntersection(spheres[i], ray);
if (t > 0.0 && t < minT) {
minT = t;
sphereToShow = spheres[i];
}
}

Note that the initial value of minT is INFINITY. So we define a macro for that. Let INFINITY be a relatively large number for our scene. I’m defining it to be 100000.0

#define INFINITY 100000.0

Now that we have sphereToShow for our scene, we can return the color of that sphere to `gl_FragColor`. But we also need to return the reflectedRay for that point along with the color. So I’ve defined a new struct called RayTracerOutput.

struct RayTracerOutput {
Ray reflectedRay;
vec3 color;
};

3. Computing the Reflected Ray

A good mathematical explanation for computing the reflected ray can be found here. Essentially, if we have an incoming ray direction W and a surface normal direction n, we can calculate the emergent reflection direction by:

R = 2 (-Wn) n + W

We can therefore form a new ray, starting a small distance ε outside the sphere surface, as:

Origin = S + ε R, where S is the surface point and ε is a very small number

Direction = R

Now our new ray (ie the reflected ray) would be:

Origin + t * Direction

This is how all this looks in code.

if (minT > 0.0 && minT != INFINITY) {
vec3 surfacePoint = cameraSource + (minT * cameraDirection);
vec3 surfaceNormal = normalize(surfacePoint - sphereCenter);
// Reflection
vec3 reflection = 2.0 * dot(-ray.direction, surfaceNormal) * surfaceNormal + ray.direction;
reflectionRay.origin = surfaceNormal + epsilon * reflection;
reflectionRay.direction = reflection;
color = colorOfSphere * (ambience + ((1.0 - ambience) * max(0.0, dot(surfaceNormal, lightSource))));
rayTracer.color = color;
rayTracer.reflectedRay = reflectionRay;
}

I’ve collated all of this into one function called trace().

RayTracerOutput trace(Sphere spheres[NUM_SPHERES], Ray ray, Light light) {
RayTracerOutput rayTracer;
Ray reflectionRay;
Sphere sphereToShow;
float minT = INFINITY;
vec3 cameraSource = ray.origin;
vec3 cameraDirection = ray.direction;
vec3 lightSource = light.position;
float ambience = light.ambience;
vec3 color = vec3(0.0, 0.0, 0.0);
for (int i=0; i < NUM_SPHERES; i++) {
float t = getIntersection(spheres[i], ray);
if (t > 0.0 && t < minT) {
minT = t;
sphereToShow = spheres[i];
}
}
vec3 sphereCenter = sphereToShow.center;
vec3 colorOfSphere = sphereToShow.color;
if (minT > 0.0 && minT != INFINITY) {
vec3 surfacePoint = cameraSource + (minT * cameraDirection);
vec3 surfaceNormal = normalize(surfacePoint - sphereCenter);
// Reflection
vec3 reflection = 2.0 * dot(-ray.direction, surfaceNormal) * surfaceNormal + ray.direction;
reflectionRay.origin = surfaceNormal + epsilon * reflection;
reflectionRay.direction = reflection;
color = colorOfSphere * (ambience + ((1.0 - ambience) * max(0.0, dot(surfaceNormal, lightSource))));
rayTracer.color = color;
rayTracer.reflectedRay = reflectionRay;
}
return rayTracer;
}

4. Adding reflected color to other spheres.

Now all we have to do is call trace() with the reflected ray and the same set of spheres. But this time, we are concerned with the color of the sphere that is hit by the reflected ray. We add that color to the original color of the sphere from the first trace() call to see reflections. This is how the main() function looks.

void main() {
initialize();
RayTracer rayTracer = trace(spheres, rays[0], light[0]);
// Second call to get reflections
RayTracer reflection = trace(spheres, rayTracer.reflectedRay, light[0]);
gl_FragColor = vec4(rayTracer.color + reflection.color, 1.0);
}

We now have a working ray tracer with multiple spheres and reflections!

If you have any thoughts, suggestions or notice bugs in the code, please leave a comment. You can find the code here and a running version of it here.

--

--