This is an assignment written in C++ for Computer Graphics class. The goal of this assignment is to implement basic ray tracing effects such as shadows, reflection, refraction and depth of field.
The program is based on provided code and the following are our specific tasks and results I had.
1.Shadows Rays:
Cast a ray from a point to the different light sources in the scene. If another object lies in between the point and the light source, then this light does not contribute to the color of the point.
Image-1 is the result without casting shadows. Image-2 is the result with casting shadows.
Image-3 on the bottom is the result when the shadow ray is not offset by a small epsilon value. (if there's no offset, the ray will detect the object itself as an obstacle and thus cast shadow.)
2.Reflection & Refraction
To render mirrors and transparent materials, shading must also consider objects that could be reflected or seen through the surface hit by the camera (primary) rays. This can be achieved by shooting new rays from the hit position to the scene with a new direction.
The direction of the reflected Can be expressed as r = 2n(n·v)-v. The direction of the refracted ray can be derived from the Snell-Descartes law indicated on the figure.
Result Image:
3.Depth of Field
A basic depth of field effect can be achieved in our simplified camera model. Instead of only one ray per pixel, one can shoot several rays per pixel as illustrated by the figure below. In practice, this means the hole of our camera no longer has an infinitesimal size, a parameter known as the aperture in real-world cameras.
Here, the focal plane of our camera corresponds to our pixel grid (which we have put at a distance f from the camera center). This means that object A, which is close to the focal plane, will appear sharper than object B, which is more distant.
To implement a depth of field effect, one will need to average the contribution of several rays per pixels, sampled randomly as suggested in the picture.
result image:
Complete C++ Code:
////////////////////////////////////////////////////////////////////////////////
// C++ include
#include <fstream>
#include <iostream>
#include <limits>
#include <memory>
#include <string>
#include <vector>
// Eigen for matrix operations
#include "Eigen/Dense"
// Image writing library
#define STB_IMAGE_WRITE_IMPLEMENTATION // Do not include this line twice in your project!
#include "stb_image_write.h"
#include "utils.h"
// JSON parser library (https://github.com/nlohmann/json)
#include "json.hpp"
using json = nlohmann::json;
// Shortcut to avoid Eigen:: everywhere, DO NOT USE IN .h
using namespace Eigen;
////////////////////////////////////////////////////////////////////////////////
// Define types & classes
////////////////////////////////////////////////////////////////////////////////
struct Ray {
    Vector3d origin;
    Vector3d direction;
    Ray() { }
    Ray(Vector3d o, Vector3d d) : origin(o), direction(d) { }
};
struct Light {
    Vector3d position;
    Vector3d intensity;
};
struct Intersection {
    Vector3d position;
    Vector3d normal;
    double ray_param;
};
struct Camera {
    bool is_perspective;
    Vector3d position;
    double field_of_view; // between 0 and PI
    double focal_length;
    double lens_radius; // for depth of field
};
struct Material {
    Vector3d ambient_color;
    Vector3d diffuse_color;
    Vector3d specular_color;
    double specular_exponent; // Also called "shininess"
    Vector3d reflection_color;
    Vector3d refraction_color;
    double refraction_index;
};
struct Object {
    Material material;
    virtual ~Object() = default; // Classes with virtual methods should have a virtual destructor!
    virtual bool intersect(const Ray &ray, Intersection &hit) = 0;
};
// We use smart pointers to hold objects as this is a virtual class
typedef std::shared_ptr<Object> ObjectPtr;
struct Sphere : public Object {
    Vector3d position;
    double radius;
    virtual ~Sphere() = default;
    virtual bool intersect(const Ray &ray, Intersection &hit) override;
};
struct Parallelogram : public Object {
    Vector3d origin;
    Vector3d u;
    Vector3d v;
    virtual ~Parallelogram() = default;
    virtual bool intersect(const Ray &ray, Intersection &hit) override;
};
struct Scene {
    Vector3d background_color;
    Vector3d ambient_light;
    Camera camera;
    std::vector<Material> materials;
    std::vector<Light> lights;
    std::vector<ObjectPtr> objects;
};
////////////////////////////////////////////////////////////////////////////////
bool Sphere::intersect(const Ray &ray, Intersection &hit) {
// Compute the intersection between the ray and the sphere
// If the ray hits the sphere, set the result of the intersection in the
// struct 'hit'
    double A = ray.direction.squaredNorm();
    double B = (ray.origin - position).dot(ray.direction) * 2;
    double C = (ray.origin - position).squaredNorm() - radius * radius;
    double delta = B * B - 4 * A * C;
    if (delta < 0) return false;
    double t1 = (-B - std::sqrt(delta)) / (2 * A);
    double t2 = (-B + std::sqrt(delta)) / (2 * A);
    if (t1 < 0 && t2 < 0) return false;
    t1 = (t1 < t2) ? t1 : t2;
    if (t1 < 0) t1 = t2;
    hit.position = ray.origin + ray.direction * t1;
    hit.normal = (hit.position - position).normalized();
    return true;
}
bool Parallelogram::intersect(const Ray &ray, Intersection &hit) {
    Vector3d y = ray.origin - origin;
    Matrix3d A;
    A.col(0) = u;
    A.col(1) = v;
    A.col(2) = -ray.direction;
    Vector3d x = A.colPivHouseholderQr().solve(y);
    if (!(x(0) >= 0 && x(0) <=1 && x(1) >= 0 && x(1) <= 1))
        return false;
    hit.position = ray.origin + x(2) * ray.direction;
    if ((ray.direction).dot(hit.position - ray.origin) < 0)
        return false;
    hit.normal = u.cross(v);
    if ((ray.origin - origin).dot(hit.normal) < 0)
        hit.normal = -1 * hit.normal;
    (hit.normal).normalize();
    return true;
}
////////////////////////////////////////////////////////////////////////////////
// Define ray-tracing functions
////////////////////////////////////////////////////////////////////////////////
// Function declaration here (could be put in a header file)
Vector3d ray_color(const Scene &scene, const Ray &ray, const Object &object, const Intersection &hit, int max_bounce);
Object * find_nearest_object(const Scene &scene, const Ray &ray, Intersection &closest_hit);
bool is_light_visible(const Scene &scene, const Ray &ray, const Light &light);
Vector3d shoot_ray(const Scene &scene, const Ray &ray, int max_bounce);
// -----------------------------------------------------------------------------
Vector3d ray_color(const Scene &scene, const Ray &ray, const Object &obj, const Intersection &hit, int max_bounce) {
    // Material for hit object
    const Material &mat = obj.material;
    // Ambient light contribution
    Vector3d ambient_color = obj.material.ambient_color.array() * scene.ambient_light.array();
    // Punctual lights contribution (direct lighting)
    Vector3d lights_color(0, 0, 0);
    for (const Light &light : scene.lights) {
        Vector3d Li = (light.position - hit.position).normalized();
        Vector3d N = hit.normal;
        // Shoot a shadow ray to determine if the light should affect the intersection point
        Ray shadowRay(hit.position + 10e-7 * Li, Li);
        Intersection shadowHit;
        bool obstacle = false;
        for (const ObjectPtr object : scene.objects) {
            if (object->intersect(shadowRay, shadowHit) && (hit.position - shadowHit.position).dot(light.position - shadowHit.position) < 0) {
                obstacle = true;
                break;
            }
        }
        if (obstacle) continue;
        // Diffuse contribution
        Vector3d diffuse = mat.diffuse_color * std::max(Li.dot(N), 0.0);
        // Specular contribution
        Vector3d h = (Li - ray.direction).normalized();
        Vector3d specular = mat.specular_color * std::pow(std::max(N.dot(h), 0.0), mat.specular_exponent);
        // Attenuate lights according to the squared distance to the lights
        Vector3d D = light.position - hit.position;
        lights_color += (diffuse + specular).cwiseProduct(light.intensity) /  D.squaredNorm();
    }
    // Compute the color of the reflected ray and add its contribution to the current point color.
    Vector3d reflection_color(0, 0, 0);
    Vector3d r = (ray.direction - 2 * (hit.normal.dot(ray.direction)) * hit.normal).normalized();
    Ray reflectionRay(hit.position + 10e-7 * r, r);
    if (max_bounce > 0)
        reflection_color = shoot_ray(scene, reflectionRay, max_bounce - 1).cwiseProduct(obj.material.reflection_color) * 0.5;
    if (reflection_color == scene.background_color)
        reflection_color = Vector3d(0, 0, 0);
    // TODO: Compute the color of the refracted ray and add its contribution to the current point color.
    //       Make sure to check for total internal reflection before shooting a new ray.
    Vector3d refraction_color(0, 0, 0);
    // Rendering equation
    Vector3d C = ambient_color + lights_color + reflection_color + refraction_color;
    return C;
}
// -----------------------------------------------------------------------------
Object * find_nearest_object(const Scene &scene, const Ray &ray, Intersection &closest_hit) {
    int closest_index = -1;
    // Find the object in the scene that intersects the ray first
    // The function must return 'nullptr' if no object is hit, otherwise it must
    // return a pointer to the hit object, and set the parameters of the argument
    // 'hit' to their expected values.
    Intersection hit;
    for (auto it=scene.objects.begin(); it!=scene.objects.end(); it++) {
        if ((*it)->intersect(ray, hit)) {
            // check whether closer
            if (closest_index == -1 || (hit.position - ray.origin).squaredNorm() < (closest_hit.position - ray.origin).squaredNorm()) {
                closest_index = it - scene.objects.begin();
                closest_hit = hit;
            }
        }
    }
    if (closest_index < 0) {
        // Return a NULL pointer
        return nullptr;
    } else {
        // Return a pointer to the hit object. Don't forget to set 'closest_hit' accordingly!
        return scene.objects[closest_index].get();
    }
}
bool is_light_visible(const Scene &scene, const Ray &ray, const Light &light) {
    // Implemented in-place
    return true;
}
Vector3d shoot_ray(const Scene &scene, const Ray &ray, int max_bounce) {
    Intersection hit;
    if (Object * obj = find_nearest_object(scene, ray, hit)) {
        // 'obj' is not null and points to the object of the scene hit by the ray
        return ray_color(scene, ray, *obj, hit, max_bounce);
    } else {
        // 'obj' is null, we must return the background color
        return scene.background_color;
    }
}
////////////////////////////////////////////////////////////////////////////////
void render_scene(const Scene &scene) {
    std::cout << "Simple ray tracer." << std::endl;
    int w = 640;
    int h = 480;
    MatrixXd R = MatrixXd::Zero(w, h);
    MatrixXd G = MatrixXd::Zero(w, h);
    MatrixXd B = MatrixXd::Zero(w, h);
    MatrixXd A = MatrixXd::Zero(w, h); // Store the alpha mask
    // The camera always points in the direction -z
    // The sensor grid is at a distance 'focal_length' from the camera center,
    // and covers an viewing angle given by 'field_of_view'.
    double aspect_ratio = double(w) / double(h);
    double scale_y = scene.camera.focal_length * std::tan(scene.camera.field_of_view / 2); // Stretch the pixel grid by the proper amount here
    double scale_x = aspect_ratio * scale_y; //
    // The pixel grid through which we shoot rays is at a distance 'focal_length'
    // from the sensor, and is scaled from the canonical [-1,1] in order
    // to produce the target field of view.
    Vector3d grid_origin(-scale_x, scale_y, scene.camera.position(2)-scene.camera.focal_length);
    Vector3d x_displacement(2.0/w*scale_x, 0, 0);
    Vector3d y_displacement(0, -2.0/h*scale_y, 0);
    // depth of field by default samples 5 points
    std::vector<Vector3d> apertureCorrection;
    apertureCorrection.push_back(Vector3d(0, 0, 0));
    apertureCorrection.push_back(Vector3d(scene.camera.lens_radius/2, 0, 0));
    apertureCorrection.push_back(Vector3d(0, scene.camera.lens_radius/2, 0));
    apertureCorrection.push_back(Vector3d(-scene.camera.lens_radius/2, 0, 0));
    apertureCorrection.push_back(Vector3d(0, -scene.camera.lens_radius/2, 0));
    int depthOfFieldSampleNumber = 5;

    for (unsigned i = 0; i < w; ++i) {
        for (unsigned j = 0; j < h; ++j) {
            // Implement depth of field
            for (unsigned k = 0; k < depthOfFieldSampleNumber; ++k) {
                Vector3d shift = grid_origin + (i+0.5)*x_displacement + (j+0.5)*y_displacement;
                // Prepare the ray
                Ray ray;
                if (scene.camera.is_perspective) {
                    // Perspective camera
                    ray.origin = scene.camera.position + apertureCorrection[k];
                    ray.direction = (shift - ray.origin).normalized();
                } else {
                    // Orthographic camera
                    ray.origin = scene.camera.position + Vector3d(shift[0], shift[1], 0);
                    ray.direction = Vector3d(0, 0, -1);
                }
                int max_bounce = 3;
                Vector3d C = shoot_ray(scene, ray, max_bounce);
                R(i, j) += C(0) / depthOfFieldSampleNumber;
                G(i, j) += C(1) / depthOfFieldSampleNumber;
                B(i, j) += C(2) / depthOfFieldSampleNumber;
                A(i, j) = 1;
            }
        }
    }
    // Save to png
    const std::string filename("raytrace.png");
    write_matrix_to_png(R, G, B, A, filename);
}
////////////////////////////////////////////////////////////////////////////////
Scene load_scene(const std::string &filename) {
    Scene scene;
    // Load json data from scene file
    json data;
    std::ifstream in(filename);
    in >> data;
    // Helper function to read a Vector3d from a json array
    auto read_vec3 = [] (const json &x) {
        return Vector3d(x[0], x[1], x[2]);
    };
    // Read scene info
    scene.background_color = read_vec3(data["Scene"]["Background"]);
    scene.ambient_light = read_vec3(data["Scene"]["Ambient"]);
    // Read camera info
    scene.camera.is_perspective = data["Camera"]["IsPerspective"];
    scene.camera.position = read_vec3(data["Camera"]["Position"]);
    scene.camera.field_of_view = data["Camera"]["FieldOfView"];
    scene.camera.focal_length = data["Camera"]["FocalLength"];
    scene.camera.lens_radius = data["Camera"]["LensRadius"];
    // Read materials
    for (const auto &entry : data["Materials"]) {
        Material mat;
        mat.ambient_color = read_vec3(entry["Ambient"]);
        mat.diffuse_color = read_vec3(entry["Diffuse"]);
        mat.specular_color = read_vec3(entry["Specular"]);
        mat.reflection_color = read_vec3(entry["Mirror"]);
        mat.refraction_color = read_vec3(entry["Refraction"]);
        mat.refraction_index = entry["RefractionIndex"];
        mat.specular_exponent = entry["Shininess"];
        scene.materials.push_back(mat);
    }
    // Read lights
    for (const auto &entry : data["Lights"]) {
        Light light;
        light.position = read_vec3(entry["Position"]);
        light.intensity = read_vec3(entry["Color"]);
        scene.lights.push_back(light);
    }
    // Read objects
    for (const auto &entry : data["Objects"]) {
        ObjectPtr object;
        if (entry["Type"] == "Sphere") {
            auto sphere = std::make_shared<Sphere>();
            sphere->position = read_vec3(entry["Position"]);
            sphere->radius = entry["Radius"];
            object = sphere;
        } else if (entry["Type"] == "Parallelogram") {
            auto parallelogram = std::make_shared<Parallelogram>();
            parallelogram->origin = read_vec3(entry["Origin"]);
            parallelogram->u = read_vec3(entry["U"]);
            parallelogram->v = read_vec3(entry["V"]);
            object = parallelogram;
        }
        object->material = scene.materials[entry["Material"]];
        scene.objects.push_back(object);
    }
    return scene;
}
////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " scene.json" << std::endl;
        return 1;
    }
    Scene scene = load_scene(argv[1]);
    render_scene(scene);
    return 0;
}

Back to Top