Realtime Raytracing - Part 2 (Adding Reflections and Animation)

Written by Dean Edis.

Welcome to the Gimpy Software article on creating a GPU-powered Raytracer! (Part 2)

In this article we'll be extending our implementation to cover reflections and a simple animation. The former will add another level of realism, and the latter will help show the real-time nature of the rendering.

Let’s start with the code, which you can paste directly into the shader editor:

 

precision mediump float;

varying vec2 position; // Texture position being processed.
uniform float time;    // The current time.


// Calculate whether a ray hits a sphere.
// If it hits, return the distance to the object.
float sphereHit(inout vec3 rayPos, inout vec3 rayDir, vec3 origin, float radius, inout vec4 rgb)
{
  vec3 p = rayPos - origin;

  // We need to solve a quadratic to find the distance.  
  float a = dot(rayDir, rayDir);
  float b = 2.0 * dot(p, rayDir);
  float c = dot(p, p) - (radius * radius);
  
  if (a == 0.0)
    return 999.0; // No hit.
  
  float f = b * b - 4.0 * a * c;
  if (f < 0.0)
    return 999.0;
  
  float lamda1 = (-b + sqrt(f)) / (2.0 * a);
  float lamda2 = (-b - sqrt(f)) / (2.0 * a);
  
  if (max(lamda1, lamda2) <= 0.0)
    return 999.0;
  
  // Find nearest hit point.
  if (lamda1 <= 0.0)
    lamda1 = lamda2;
  else if (lamda2 <= 0.0)
    lamda2 = lamda1;
  
  float dist = min(lamda1, lamda2);
  
  // Reflect ray off the surface.
  rayPos = rayPos + dist * rayDir;
  vec3 normal = normalize(rayPos - origin);
  rayDir = reflect(rayDir, normal);
  
  return dist;
}

// Calculate whether a ray hits the floor plane.
// If it hits, return the distance to the object.
float planeHit(inout vec3 rayPos, inout vec3 rayDir, float y, inout vec4 rgb)
{
  if (rayDir.y == 0.0)
    return 999.0; // Ray is parallel to the plane.
  
  float lamda = (y - rayPos.y) / rayDir.y;
  float dist = lamda * length(rayDir);
  
  // Reflect ray off the surface.
  rayPos = rayPos + dist * normalize(rayDir);
  rayDir = reflect(rayDir, vec3(0.0, 1.0, 0.0));
  
  return dist;
}

// Check for a collision between ray and object.
// If there is one, return the distance to the object.
float objectHit(int id, inout vec3 rayPos, inout vec3 rayDir, inout vec4 rgb)
{
  if (id == 0)
  {
    // Object 0 - Red sphere.
    rgb = vec4(1.0, 0.0, 0.0, 1.0);
    return sphereHit(rayPos, rayDir, vec3(-0.3, -0.2, 0.0), 0.5, rgb);
  }
  
  if (id == 1)
  {
    // Object 1 - Yellow sphere.
    rgb = vec4(1.0, 1.0, 0.0, 1.0);
    return sphereHit(rayPos, rayDir, vec3(0.3, sin(time)* 0.6 + 0.3, -0.2), 0.3, rgb);
  }
  
  // Object 2 - The floor.
  rgb = vec4(1.0);
  return planeHit(rayPos, rayDir, -0.5, rgb);
}

// The bulk of the raytrace work is done here.
// Cast a ray through the scene and see if it hits an object.
vec4 castRay(inout vec3 inRayPos, inout vec3 inRayDir)
{
  vec4 rgb = vec4(0.0);    // Default color.
  float d_nearest = 999.0; // Distance to the nearest hit object.

  vec4 hit_rgb;
  vec3 bouncedRayPos, bouncedRayDir;
  vec3 testRayPos, testRayDir;
  
  // Check for a collision with each of the objects in the scene.
  for (int id = 0; id < 3; id++)
  {
    testRayPos = vec3(inRayPos); testRayDir = vec3(inRayDir);
    float d = objectHit(id, testRayPos, testRayDir, hit_rgb);
    
    // If there was a hit, and it occurred nearer than any other object...
    if (d > 0.0 && d < d_nearest)
    {
      // ...remember it.
      d_nearest = d;
      rgb = hit_rgb;
      bouncedRayPos = vec3(testRayPos); bouncedRayDir = vec3(testRayDir);
    }
  }

  // If we hit something...
  if (d_nearest > 0.0 && d_nearest < 999.0)
  {
    inRayPos = bouncedRayPos;
    inRayDir = bouncedRayDir;
  }
  
  return rgb;
}

// The entry point to the shader.
void main() {
  // Invert the texture Y coordinate so our scene renders the right way up.
  vec2 p = vec2(position.x, 1.0 - position.y);
  
  // Set the camera properties.
  float cameraDist = 2.0;
  
  // Define the position and directions of the 'ray' to fire.
  vec3 rayPos = vec3(p.x - 0.5, 0.5 - p.y, -cameraDist);
  vec3 rayDir = normalize(vec3(p.x - 0.5, 0.5 - p.y, 1.0));
  
  // Fire the ray into the scene.
  vec4 rgb = castRay(rayPos, rayDir);
  if (rgb.a > 0.0)
  {
    // The ray hit an object!
    
    // Advance the ray by a small amount.
    rayPos += rayDir / 1000.0;
    
    // Cast the reflected ray, and combine the results with the original RGB.
    float shine = 0.2;
    vec4 rgb_bounce = castRay(rayPos, rayDir);
    rgb = rgb * (1.0 - shine) + rgb_bounce * shine;
  }
  else
  {
    // The ray missed all objects - Plot a black pixel.
    rgb = vec4(0.0, 0.0, 0.0, 1.0);
  }
  
  gl_FragColor = rgb;
}

The main change in this code is in the main() function. After casting a ray into the scene we check to see if it hits an object. If it does we use the reflected ray vector (I.e. the position and direction of the ray as it ‘bounces off’ the object) in another call to castRay. This second call will return another RGB pixel value which we combine with the first – Hey presto, reflections! Adjust the ‘shine’ constant (which for demo purposes applies to all objects in the scene, but doesn’t need to) to effect how the two RGB values are mixed.

Again, to keep the implementation simple, we only reflect the ray once. It’s certainly possible to add more iterations, but at the expense of code complexity and performance. I’ll leave it as an exercise to the reader, if you’re feeling adventurous! (And please send us screenshots of your results if you do!)

The second change is in the objectHit function. This one is very simple – We just use the ‘time’ constant with a sine function to update the Y position of the second (yellow) sphere. The reflections in the sphere are updated as it moves, making for quite a satisfying result!

Raytracing Example

 

Note the floor, even though is defined as pure white, is now being drawn slightly grey – Caused by it reflecting a fraction of the black ‘sky’. The color of the reflection in each sphere is tinted slightly to the color of the other too.

In the next article we'll be extending our implementation to cover lighting. Again, small changes which will result in significant results.