Skip to content
Harshal Patel
Go back

Ray Tracing — How I Built a Real-Time Raytracing Engine in C++

Table of contents

Open Table of contents

What Is Ray Tracing?

Ray tracing is a rendering technique that simulates how light actually works. Instead of approximating lighting (like rasterization), ray tracing follows individual light rays as they bounce around a scene.

Think of it like this: rasterization draws what you see from your viewpoint. Ray tracing figures out what light does, then shows you the result.

3D rendered scene showing realistic lighting and reflections
Photo by{" "} Dan Meyers{" "} on Unsplash

How Ray Tracing Works

The Basic Idea

For every pixel on screen, cast a ray from the camera through that pixel into the scene. Find what object it hits, then cast more rays from that point to figure out lighting, shadows, and reflections.

Camera

  │  Ray 1
  │  ─────→ [Hits sphere]
  │           │
  │           │ Shadow ray → [Hits light?]
  │           │
  │           │ Reflection ray → [Hits another object]

  │  Ray 2
  │  ─────→ [Hits floor]
  │           │
  │           │ Shadow ray → [Hits light?]

The Math

Every ray is defined by an origin and direction:

struct Ray {
    Vec3 origin;
    Vec3 direction;
};

// Primary ray from camera
Ray primaryRay(int x, int y) {
    Vec3 dir = Vec3(
        (2.0f * x / width - 1.0f) * aspect,
        1.0f - 2.0f * y / height,
        -1.0f  // FOV
    ).normalized();

    return Ray{cameraPosition, dir};
}

Ray-sphere intersection:

bool intersectSphere(Ray ray, Sphere sphere, float& t) {
    Vec3 oc = ray.origin - sphere.center;
    float a = ray.direction.dot(ray.direction);
    float b = 2.0f * oc.dot(ray.direction);
    float c = oc.dot(oc) - sphere.radius * sphere.radius;
    float discriminant = b * b - 4 * a * c;

    if (discriminant < 0) return false;

    t = (-b - sqrt(discriminant)) / (2.0f * a);
    return t > 0;
}

Building the Engine

Project Structure

raytracing/
├── src/
│   ├── main.cpp
│   ├── ray.h
│   ├── vec3.h
│   ├── sphere.h
│   ├── scene.h
│   ├── renderer.h
│   └── gl_utils.h
├── shaders/
│   ├── vertex.glsl
│   └── fragment.glsl
├── CMakeLists.txt
└── README.md

The Renderer

class Renderer {
    GLuint framebuffer;
    GLuint texture;
    GLuint shaderProgram;

    std::vector<Vec3> framebuffer_data;

public:
    void init(int width, int height) {
        // Create framebuffer texture
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, width, height, 0,
                     GL_RGB, GL_FLOAT, nullptr);

        // Create framebuffer
        glGenFramebuffers(1, &framebuffer);
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                              GL_TEXTURE_2D, texture, 0);

        framebuffer_data.resize(width * height);
    }

    void render(const Scene& scene, const Camera& camera) {
        int width = 1920, height = 1080;

        #pragma omp parallel for
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                Ray ray = camera.primaryRay(x, y, width, height);
                framebuffer_data[y * width + x] = trace(ray, scene);
            }
        }

        // Upload to GPU
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height,
                        GL_RGB, GL_FLOAT, framebuffer_data.data());

        // Draw fullscreen quad
        drawQuad();
    }

    Vec3 trace(Ray ray, const Scene& scene, int depth = 0) {
        if (depth > 5) return Vec3(0, 0, 0);  // Recursion limit

        HitInfo hit;
        if (!scene.intersect(ray, hit)) {
            return scene.backgroundColor;  // Miss — return sky color
        }

        Vec3 color = hit.material.emission;
        Vec3 lightDir = (scene.lightPos - hit.point).normalized();

        // Diffuse lighting
        float diffuse = std::max(0.0f, hit.normal.dot(lightDir));
        color += hit.material.albedo * diffuse * scene.lightColor;

        // Shadows
        Ray shadowRay{hit.point + hit.normal * 0.001f, lightDir};
        if (scene.intersectShadow(shadowRay)) {
            color *= 0.2f;  // In shadow — darken
        }

        // Reflections
        if (hit.material.reflectivity > 0) {
            Vec3 reflectDir = ray.direction.reflect(hit.normal);
            Ray reflectRay{hit.point + hit.normal * 0.001f, reflectDir};
            Vec3 reflectColor = trace(reflectRay, scene, depth + 1);
            color = color.lerp(reflectColor, hit.material.reflectivity);
        }

        return color;
    }
};

Optimizations

Bounding Volume Hierarchy (BVH):

struct BVHNode {
    AABB bounds;
    BVHNode* left;
    BVHNode* right;
    std::vector<Triangle>* triangles;  // Only for leaf nodes

    bool intersect(Ray ray, HitInfo& hit) {
        if (!bounds.intersect(ray)) return false;

        if (left == nullptr) {
            // Leaf node — test all triangles
            bool hitSomething = false;
            for (auto& tri : *triangles) {
                if (tri.intersect(ray, hit)) hitSomething = true;
            }
            return hitSomething;
        }

        // Internal node — test children
        bool hitLeft = left->intersect(ray, hit);
        bool hitRight = right->intersect(ray, hit);
        return hitLeft || hitRight;
    }
};

Without BVH, every ray tests every object (O(n)). With BVH, it’s O(log n).

Multi-threading:

#pragma omp parallel for
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        // Each thread handles different rows
    }
}

Results

The engine renders at interactive frame rates for simple scenes:

What I Learned

  1. Math matters: Ray tracing is all linear algebra and geometry
  2. Optimization is key: BVH and multi-threading made the difference between 1 FPS and 30 FPS
  3. GPU is faster: For production, you’d use CUDA or Vulkan compute shaders
  4. Lighting is everything: A scene with good lighting looks 10x better than a complex scene with bad lighting

Next Steps

The code is on GitHub.


Share this post:

Next Post
The Complete Guide to Networking & Communication Protocols