Truman Costello

Truman Costello

Computer Science, Game Development, Mathematics @ USC

Graphics Playground

Some source code for a raytracer I made.

Includes basic implementations of:

  • Shadow ray phong shading
  • Recursive reflections
  • Soft shadows (deferred)
  • Anti aliasing
struct ShadowBufferInfo
{
  float bufferWeight;
  float distance_to_occluder;
  float distance_to_light;
  float distance_to_camera;

  ShadowBufferInfo(float weight, float odist, float ldist, float cdist)
    : bufferWeight(weight), distance_to_occluder(odist), distance_to_light(ldist), distance_to_camera(cdist)
  {
  }

  ShadowBufferInfo& operator+=(const ShadowBufferInfo& rhs)
  {
    bufferWeight += rhs.bufferWeight;
    distance_to_occluder += rhs.distance_to_occluder;
    distance_to_light += rhs.distance_to_light;
    distance_to_camera += rhs.distance_to_camera;
    return *this;
  }

  ShadowBufferInfo& operator/=(const ShadowBufferInfo& rhs)
  {
    bufferWeight /= rhs.bufferWeight;
    distance_to_occluder /= rhs.distance_to_occluder;
    distance_to_light /= rhs.distance_to_light;
    distance_to_camera /= rhs.distance_to_camera;
    return *this;
  }

  ShadowBufferInfo& operator/=(float rhs)
  {
    bufferWeight /= rhs;
    distance_to_occluder /= rhs;
    distance_to_light /= rhs;
    distance_to_camera /= rhs;
    return *this;
  }
};

void plot_pixel_display(int x,int y,unsigned char r,unsigned char g,unsigned char b);
void plot_pixel_jpeg(int x,int y,unsigned char r,unsigned char g,unsigned char b);
void plot_pixel(int x,int y,unsigned char r,unsigned char g,unsigned char b);
std::pair<glm::vec3, ShadowBufferInfo> ray_trace(glm::vec3 camera_pos, glm::vec3 viewport_pos, size_t reflection_depth = 3);
glm::vec2 sphere_intersect(glm::vec3 origin, glm::vec3 direction, Sphere sphere);
std::pair<glm::vec3, ShadowBufferInfo> phong_illumination(glm::vec3 camera_position, glm::vec3 origin, glm::vec3 normal, glm::vec3 diffuse, glm::vec3 specular, float shininess);
void save_jpg();

glm::vec3 reflect_ray(glm::vec3 ray, glm::vec3 normal);

struct IntersectionInfo
{
  Triangle* triangle;
  float t;
  float u;
  float v;
};

bool is_nearly_zero(const glm::vec3& vector, float tolerance = EPSILON)
{
  return (glm::abs(vector.x) <= tolerance) &&
         (glm::abs(vector.y) <= tolerance) &&
         (glm::abs(vector.z) <= tolerance);
}
#define NUM_SAMPLES 8

float compute_soft_shadow(int pixel_x, int pixel_y, std::vector<std::vector<ShadowBufferInfo> >& shadowBuffer)
{
  ShadowBufferInfo* shadowInfo = &shadowBuffer[pixel_x][pixel_y];
  if (shadowInfo->bufferWeight >= 1.0f) return 1.0f;
  float occlusion = shadowInfo->distance_to_occluder / shadowInfo->distance_to_light;
  if (occlusion >= 1.0f) return 1.0f;

  float sample_size = (1.0f - occlusion) * SHADOW_RADIUS / shadowInfo->distance_to_camera;
  sample_size = glm::clamp(sample_size, 0.0f, 10.0f);
  sample_size *= 100;
  int sample_radius = static_cast<int>(sample_size);
  float D_Sample = sample_size;
  float D_max = sqrt(2.0f) * sample_size;

  for (int dy = -sample_radius; dy <= sample_radius; ++dy)
  {
    for (int dx = -sample_radius; dx <= sample_radius; ++dx)
    {
      int sample_x = pixel_x + dx;
      int sample_y = pixel_y + dy;

      if (sample_x < 0 || sample_x >= WIDTH || sample_y < 0 || sample_y >= HEIGHT)
        continue;

      ShadowBufferInfo* sample_shadowInfo = &shadowBuffer[sample_x][sample_y];
      float sample_occlusion = sample_shadowInfo->distance_to_occluder / sample_shadowInfo->distance_to_light;

      if (sample_occlusion >= 1.0f) {
        float distance_sq = static_cast<float>(dx * dx + dy * dy);
        if (distance_sq < D_Sample * D_Sample) {
          D_Sample = sqrt(distance_sq);
        }
      }
    }
  }

  float S_Shadow = D_Sample / D_max;
  S_Shadow = glm::clamp(S_Shadow, 0.0f, 1.0f);

  float final_occlusion = glm::mix(S_Shadow, shadowInfo->bufferWeight, 0.5f);
  return final_occlusion >= 1.0f ? 1.0f : final_occlusion;
}

void draw_scene()
{
  const double Vw = 2 * -FOCAL_LENGTH * tan(glm::radians(FOV) / 2.0);
  const double Vh = Vw * (static_cast<double>(HEIGHT) / WIDTH);
  const glm::vec3 camera_pos(0, 0, 0);

  // Super sampling anti-aliasing
  float jitterMatrix[NUM_SAMPLES * 2] = {
    1.0f/6.0f, 1.0f/6.0f,
    5.0f/6.0f, 1.0f/6.0f,
    1.0f/6.0f, 5.0f/6.0f,
    5.0f/6.0f, 5.0f/6.0f,
    3.0f/6.0f, 2.0f/6.0f,
    2.0f/6.0f, 4.0f/6.0f,
    4.0f/6.0f, 4.0f/6.0f,
    2.0f/6.0f, 2.0f/6.0f,
  };
  std::vector<std::vector<glm::vec3> > colorBuffer(WIDTH, std::vector<glm::vec3>(HEIGHT, glm::vec3(0.0f)));
  std::vector<std::vector<ShadowBufferInfo> > shadowBuffer(WIDTH, std::vector<ShadowBufferInfo>(HEIGHT, ShadowBufferInfo(0.0f, 0.0f, 0.0f, 0.0f)));
  for(unsigned int x=0; x<WIDTH; x++)
  {
    glPointSize(2.0);
    glBegin(GL_POINTS);
    for(unsigned int y=0; y<HEIGHT; y++)
    {
      glm::vec3 accumulated_color(0.0f);
      ShadowBufferInfo accumulated_shadow_info = ShadowBufferInfo(0.0f, 0.0f, 0.0f, 0.0f);
      for (int sample = 0; sample < NUM_SAMPLES; ++sample)
      {
        glm::vec3 viewport_pos(
            ((WIDTH / 2.0 - (x + jitterMatrix[2 * sample])) * (Vw / WIDTH)),
            -((y + jitterMatrix[2 * sample + 1]) - HEIGHT / 2.0) * (Vh / HEIGHT),
            -FOCAL_LENGTH
        );
        auto [color, shadow_info] = ray_trace(camera_pos, viewport_pos);

        accumulated_color += color;
        accumulated_shadow_info += shadow_info;
      }
      accumulated_color /= static_cast<float>(NUM_SAMPLES);
      accumulated_shadow_info /= static_cast<float>(NUM_SAMPLES);

      colorBuffer[x][y] = accumulated_color;
      shadowBuffer[x][y] = accumulated_shadow_info; // currently unused

      plot_pixel_display(x, y, accumulated_color.r, accumulated_color.g, accumulated_color.b);
      if (!SOFT_SHADOW_COND)
      {
        plot_pixel_jpeg(x, y, accumulated_color.r, accumulated_color.g, accumulated_color.b);
      }
    }
    glEnd();
    glFlush();
  }

  if (SOFT_SHADOW_COND)
  {
    for(unsigned int x = 0; x < WIDTH; x++)
    {
      glPointSize(2.0);
      glBegin(GL_POINTS);
      for(unsigned int y = 0; y < HEIGHT; y++)
      {
        glm::vec3 filtered_color = colorBuffer[x][y] * compute_soft_shadow(x, y, shadowBuffer);
        plot_pixel_display(x, y, filtered_color.r, filtered_color.g, filtered_color.b);
        plot_pixel_jpeg(x, y, filtered_color.r, filtered_color.g, filtered_color.b);
      }
      glEnd();
      glFlush();
    }
  }
  save_jpg();
  printf("Ray tracing completed.\n");
  fflush(stdout);
}

bool find_closest_sphere_intersection(
    const glm::vec3& camera_pos,
    const glm::vec3& ray_dir,
    Sphere*& closest_sphere,
    float& closest_intersection)
{
  closest_sphere = nullptr;
  closest_intersection = FLT_MAX;

  for (int i = 0; i < num_spheres; ++i)
  {
    const Sphere& sphere = spheres[i];
    glm::vec2 intersections = sphere_intersect(camera_pos, ray_dir, sphere);
    float intersect_min = glm::min(intersections[0], intersections[1]);

    if (intersect_min > 0 && intersect_min < closest_intersection)
    {
      closest_intersection = intersect_min;
      closest_sphere = const_cast<Sphere*>(&sphere);
    }
  }

  return closest_sphere != nullptr;
}

bool find_closest_triangle_intersection(
    const glm::vec3& camera_pos,
    const glm::vec3& ray_dir,
    IntersectionInfo& closest_intersection_info
    )
{
  closest_intersection_info.t = FLT_MAX;
  closest_intersection_info.triangle = nullptr;
  closest_intersection_info.u = 0.0f;
  closest_intersection_info.v = 0.0f;
  for (int i = 0; i < num_triangles; ++i)
  {
    const Triangle& triangle = triangles[i];
    glm::vec3 v0 = glm::vec3(triangle.v[0].position[0], triangle.v[0].position[1], triangle.v[0].position[2]);
    glm::vec3 v1 = glm::vec3(triangle.v[1].position[0], triangle.v[1].position[1], triangle.v[1].position[2]);
    glm::vec3 v2 = glm::vec3(triangle.v[2].position[0], triangle.v[2].position[1], triangle.v[2].position[2]);

    glm::vec3 edge1 = v1 - v0;
    glm::vec3 edge2 = v2 - v0;

    glm::vec3 h = glm::cross(ray_dir, edge2);
    float a = glm::dot(edge1, h);

    if (fabs(a) < EPSILON)
      continue; // Ray is parallel to the triangle.

    float f = 1.0f / a;
    glm::vec3 s = camera_pos - v0;
    float u = f * glm::dot(s, h);

    if (u < 0.0f || u > 1.0f)
      continue;

    glm::vec3 q = glm::cross(s, edge1);
    float v = f * glm::dot(ray_dir, q);

    if (v < 0.0f || u + v > 1.0f)
      continue;

    float t = f * glm::dot(edge2, q);

    if (t > EPSILON && t < closest_intersection_info.t)
    {
      closest_intersection_info.t = t;
      closest_intersection_info.triangle = const_cast<Triangle*>(&triangle);
      closest_intersection_info.u = u;
      closest_intersection_info.v = v;
    }
  }

  return closest_intersection_info.triangle != nullptr;
}

float get_closest_intersection_dist(glm::vec3 origin, glm::vec3 dir)
{
  Sphere* closest_sphere = nullptr;
  float closest_sphere_intersection;

  const bool sphere_hit = find_closest_sphere_intersection(origin, dir, closest_sphere, closest_sphere_intersection);

  IntersectionInfo closest_triangle_info;
  const bool triangle_hit = find_closest_triangle_intersection(origin, dir, closest_triangle_info);

  return std::min(closest_sphere_intersection, closest_triangle_info.t);
}

std::pair<glm::vec3, ShadowBufferInfo> ray_trace(glm::vec3 camera_pos, glm::vec3 viewport_pos, size_t reflection_depth)
{
    glm::vec3 ray_dir = glm::normalize(viewport_pos - camera_pos);

    Sphere* closest_sphere = nullptr;
    float closest_sphere_intersection;

    const bool sphere_hit = find_closest_sphere_intersection(camera_pos, ray_dir, closest_sphere, closest_sphere_intersection);

    IntersectionInfo closest_triangle_info;
    const bool triangle_hit = find_closest_triangle_intersection(camera_pos, ray_dir, closest_triangle_info);
    if (!sphere_hit && !triangle_hit)
    {
        return std::make_pair(BACKGROUND_COLOR, ShadowBufferInfo(1.0f, 0.0, 0.0, 0.0)); // No shadow
    }

    // Initialize variables to hold intersection details
    glm::vec3 intersection_pos;
    glm::vec3 normal;
    glm::vec3 specular;
    glm::vec3 local_color;
    ShadowBufferInfo shadow_factor = ShadowBufferInfo(1.0f, 0.0f, 0.0f, 0.0f);

    if (triangle_hit && (!sphere_hit || closest_triangle_info.t < closest_sphere_intersection))
    {
        // Triangle intersection is closer
        intersection_pos = camera_pos + closest_triangle_info.t * ray_dir;

        // Barycentric coordinates
        float u = closest_triangle_info.u;
        float v = closest_triangle_info.v;
        float w = 1.0f - u - v;

        Triangle* hit_triangle = closest_triangle_info.triangle;

        // Interpolate normals
        glm::vec3 normal0 = glm::normalize(glm::vec3(hit_triangle->v[0].normal[0], hit_triangle->v[0].normal[1], hit_triangle->v[0].normal[2]));
        glm::vec3 normal1 = glm::normalize(glm::vec3(hit_triangle->v[1].normal[0], hit_triangle->v[1].normal[1], hit_triangle->v[1].normal[2]));
        glm::vec3 normal2 = glm::normalize(glm::vec3(hit_triangle->v[2].normal[0], hit_triangle->v[2].normal[1], hit_triangle->v[2].normal[2]));

        glm::vec3 interpolated_normal = w * normal0 + u * normal1 + v * normal2;
        normal = glm::normalize(interpolated_normal);

        // Interpolate diffuse color
        glm::vec3 diffuse0 = glm::vec3(hit_triangle->v[0].color_diffuse[0], hit_triangle->v[0].color_diffuse[1], hit_triangle->v[0].color_diffuse[2]);
        glm::vec3 diffuse1 = glm::vec3(hit_triangle->v[1].color_diffuse[0], hit_triangle->v[1].color_diffuse[1], hit_triangle->v[1].color_diffuse[2]);
        glm::vec3 diffuse2 = glm::vec3(hit_triangle->v[2].color_diffuse[0], hit_triangle->v[2].color_diffuse[1], hit_triangle->v[2].color_diffuse[2]);

        glm::vec3 interpolated_diffuse = w * diffuse0 + u * diffuse1 + v * diffuse2;

        // Interpolate specular color
        glm::vec3 specular0 = glm::vec3(hit_triangle->v[0].color_specular[0], hit_triangle->v[0].color_specular[1], hit_triangle->v[0].color_specular[2]);
        glm::vec3 specular1 = glm::vec3(hit_triangle->v[1].color_specular[0], hit_triangle->v[1].color_specular[1], hit_triangle->v[1].color_specular[2]);
        glm::vec3 specular2 = glm::vec3(hit_triangle->v[2].color_specular[0], hit_triangle->v[2].color_specular[1], hit_triangle->v[2].color_specular[2]);

        glm::vec3 interpolated_specular = w * specular0 + u * specular1 + v * specular2;

        // Interpolate shininess
        double shininess0 = hit_triangle->v[0].shininess;
        double shininess1 = hit_triangle->v[1].shininess;
        double shininess2 = hit_triangle->v[2].shininess;

        specular = interpolated_specular;

        // Compute local color and shadow factor using Phong illumination
        std::pair<glm::vec3, ShadowBufferInfo> phong_result = phong_illumination(
            camera_pos,
            intersection_pos,
            normal,
            interpolated_diffuse,
            interpolated_specular,
            static_cast<float>(w * shininess0 + u * shininess1 + v * shininess2)
        );

        local_color = phong_result.first;
        shadow_factor = phong_result.second;
    }
    else if (sphere_hit)
    {
        // Sphere intersection
        intersection_pos = camera_pos + closest_sphere_intersection * ray_dir;
        normal = glm::normalize(intersection_pos - glm::vec3(closest_sphere->position[0], closest_sphere->position[1], closest_sphere->position[2]));
        specular = glm::vec3(closest_sphere->color_specular[0], closest_sphere->color_specular[1], closest_sphere->color_specular[2]);

        // Compute local color and shadow factor using Phong illumination
        std::pair<glm::vec3, ShadowBufferInfo> phong_result = phong_illumination(
            camera_pos,
            intersection_pos,
            normal,
            glm::vec3(closest_sphere->color_diffuse[0], closest_sphere->color_diffuse[1], closest_sphere->color_diffuse[2]),
            specular,
            closest_sphere->shininess
        );

        local_color = phong_result.first;
        shadow_factor = phong_result.second;
    }

    // Turn off reflection
    if (!REFLECTIONS)
    {
      specular = glm::vec3(0.0f);
    }

    // Mirror reflection
    if (reflection_depth == 0 || glm::length(specular) == 0.0f)
    {
        return std::make_pair(local_color, shadow_factor);
    }

    glm::vec3 reflected_dir = reflect_ray(-ray_dir, normal);
    glm::vec3 reflected_viewport_pos = intersection_pos + reflected_dir;
    std::pair<glm::vec3, ShadowBufferInfo> reflected_result = ray_trace(intersection_pos, reflected_viewport_pos, reflection_depth - 1);
    glm::vec3 reflected_color = reflected_result.first;
    ShadowBufferInfo reflected_shadow = reflected_result.second;

    if (reflected_color == BACKGROUND_COLOR)
    {
        reflected_color = local_color;
    }

    glm::vec3 final_color = (1.0f - specular) * local_color + specular * reflected_color;
    return std::make_pair(final_color, shadow_factor);
}

glm::vec3 reflect_ray(glm::vec3 ray, glm::vec3 normal)
{
  return 2.0f * normal * glm::dot(ray, normal) - ray;
}

glm::vec2 normalizeOffset(const glm::vec2& offset, float radius) {
  if (glm::length(offset) == 0.0f)
    return offset;
  return (offset / glm::length(offset)) * radius;
}

float halton(int index, int base)
{
  float f = 1.0f, r = 0.0f;
  while (index > 0)
  {
    f = f / base;
    r = r + f * (index % base);
    index = index / base;
  }
  return r;
}

std::vector<glm::vec2> generate_shadow_offsets(int samples, float radius)
{
  std::vector<glm::vec2> offsets;
  for(int i = 1; i <= samples; ++i)
  {
    float x = halton(i, 2) * 2.0f - 1.0f;
    float y = halton(i, 3) * 2.0f - 1.0f;
    glm::vec2 offset(x, y);
    if(glm::length(offset) <= 1.0f)
      offsets.emplace_back(glm::normalize(offset) * radius);
    else
      offsets.emplace_back(glm::vec2(0.0f));
  }
  return offsets;
}

#define SHADOW_SAMPLES (8)

float compute_weight(const glm::vec2& offset, float sigma = 0.5f)
{
  float distance = glm::length(offset);
  return std::exp(-(distance * distance) / (2.0f * sigma * sigma));
}

std::pair<glm::vec3, ShadowBufferInfo> phong_illumination(
    glm::vec3 camera_position,
    glm::vec3 origin,
    glm::vec3 normal,
    glm::vec3 diffuse,
    glm::vec3 specular,
    float shininess)
{
    glm::vec3 intensity = AMBIENT_COLOR;
    std::vector<glm::vec2> offsets = generate_shadow_offsets(SHADOW_SAMPLES, SHADOW_RADIUS);
    ShadowBufferInfo shadow_buffer_info = ShadowBufferInfo(0.0f, 0.0f, 0.0f, 0.0f);
    for (int i = 0; i < num_lights; ++i)
    {
        Light& light = lights[i];
        glm::vec3 light_pos = glm::vec3(light.position[0], light.position[1], light.position[2]);
        glm::vec3 light_dir = normalize(light_pos - origin);

        glm::vec3 eye_dir = normalize(camera_position - origin);
        glm::vec3 reflect_dir = glm::reflect(-light_dir, normal);

        glm::vec3 shadow_origin = origin + EPSILON * normal;
        float total_weight = 0.0f;
        float unoccluded_weight = 0.0f;
        float total_distance_to_occluder = 0.0f;
        for (auto& offset : offsets)
        {
            float weight = compute_weight(offset, 0.5f);
            glm::vec2 scaled_offset = normalizeOffset(offset, SHADOW_RADIUS);

            glm::vec3 light_sample_pos = light_pos + glm::vec3(scaled_offset, 0.0f);
            glm::vec3 offset_dir = glm::normalize(light_sample_pos - shadow_origin);

            float closest_intersection = get_closest_intersection_dist(shadow_origin, offset_dir);
            float light_distance = glm::length(light_pos - origin);

            total_weight += weight;
            shadow_buffer_info.distance_to_light += light_distance;
            total_distance_to_occluder += closest_intersection;
            if (closest_intersection < light_distance)
            {
                shadow_buffer_info.distance_to_occluder += closest_intersection;
                continue;
            }
            unoccluded_weight += weight;
        }
        if (!SOFT_SHADOW_COND)
        {
          float closest_intersection = get_closest_intersection_dist(shadow_origin, light_dir);
          float light_distance = glm::length(light_pos - origin);
          if (closest_intersection < light_distance) {
            continue;
          }
        } else
        {
          shadow_buffer_info.distance_to_light /= total_weight;
          shadow_buffer_info.distance_to_occluder /= total_weight;
          shadow_buffer_info.bufferWeight += (unoccluded_weight/total_weight);
        }

        float d = std::max(glm::dot(light_dir, normal), 0.0f);
        float s = std::max(glm::dot(glm::normalize(reflect_dir), eye_dir), 0.0f);

        glm::vec3 light_color = glm::vec3(light.color[0], light.color[1], light.color[2]);

        intensity += light_color * (diffuse * d + specular * std::pow(s, shininess));
    }
    intensity = glm::min(intensity, glm::vec3(1.0f));

    // Compute average shadow factor across all lights
    shadow_buffer_info.bufferWeight = shadow_buffer_info.bufferWeight / static_cast<float>(num_lights);
    shadow_buffer_info.distance_to_camera = glm::distance(origin, camera_position);
    shadow_buffer_info.distance_to_light = shadow_buffer_info.distance_to_light / static_cast<float>(num_lights);
    shadow_buffer_info.distance_to_occluder = shadow_buffer_info.distance_to_occluder / static_cast<float>(num_lights);

    return std::make_pair(255.0f * intensity, shadow_buffer_info);
}


glm::vec2 sphere_intersect(glm::vec3 origin, glm::vec3 direction, Sphere sphere)
{
  glm::vec3 origin_to_sphere = origin - glm::vec3(sphere.position[0], sphere.position[1], sphere.position[2]);
  double a = glm::dot(direction, direction);
  double b = 2 * glm::dot(origin_to_sphere, direction);
  double c = glm::dot(origin_to_sphere, origin_to_sphere) - sphere.radius*sphere.radius;

  double discriminant = b*b - 4*a*c;
  if (discriminant < 0) {
    return glm::vec2(0);
  }
  double first_intersect = (-b + sqrt(discriminant)) / (2*a);
  double second_intersect = (-b - sqrt(discriminant)) / (2*a);
  return glm::vec2(first_intersect, second_intersect);
}