Building a Basic Ray Tracer in C++ - A Step-by-Step Guide Learn to build a basic ray tracer with C++, from setup to rendering a simple scene, exploring the core concepts of computer graphics.
Ray tracing is a technique for generating an image by tracing the path of light through pixels in an image plane and simulating the effects of its encounters with virtual objects. Let's build a basic ray tracer from scratch using C++ and CMake.
C++17 compatible compiler (GCC, Clang, etc.)
CMake (version 3.10 or higher)
A code editor or IDE of your choice
mkdir RayTracer
cd RayTracer
mkdir src include build
In the build
directory:
cmake ..
Create a CMakeLists.txt
in the project root:
cmake_minimum_required ( VERSION 3.10)
project (RayTracer)
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_STANDARD_REQUIRED True )
add_subdirectory (src)
Create include/vector.h
:
#ifndef VECTOR_H
#define VECTOR_H
#include <cmath>
class Vector3 {
public:
double x, y, z;
Vector3 ( double x_ = 0 , double y_ = 0 , double z_ = 0 ) : x (x_), y (y_), z (z_) {}
Vector3 operator + ( const Vector3 & v ) const {
return Vector3 (x + v.x, y + v.y, z + v.z);
}
Vector3 operator * ( double t ) const { return Vector3 (x * t, y * t, z * t); }
Vector3 &operator *= ( double t ) {
x *= t;
y *= t;
z *= t;
return * this ;
}
double length () const { return std :: sqrt (x * x + y * y + z * z); }
Vector3 normalize () const { return * this * ( 1 / this -> length ()); }
};
Vector3 operator * ( double t , const Vector3 & v ) { return v * t; }
#endif // VECTOR_H
Create include/ray.h
:
#ifndef RAY_H
#define RAY_H
#include "vector.h"
class Ray {
public:
Vector3 origin, direction;
Ray ( const Vector3 & o , const Vector3 & d ) : origin (o), direction (d. normalize ()) {}
};
#endif // RAY_H
Create include/sphere.h
:
#ifndef SPHERE_H
#define SPHERE_H
#include "vector.h"
#include "ray.h"
class Sphere {
public:
Vector3 center;
double radius;
Sphere ( const Vector3 & c , double r ) : center (c), radius (r) {}
bool intersect ( const Ray & ray , double& t ) const {
Vector3 oc = ray.origin - center;
auto a = dot (ray.direction, ray.direction);
auto b = 2.0 * dot (oc, ray.direction);
auto c = dot (oc, oc) - radius * radius;
auto discriminant = b * b - 4 * a * c;
if (discriminant < 0 ) return false ;
t = ( - b - std :: sqrt (discriminant)) / ( 2.0 * a);
return t > 0 ;
}
private:
double dot ( const Vector3 & a , const Vector3 & b ) const { return a.x * b.x + a.y * b.y + a.z * b.z; }
};
#endif // SPHERE_H
Create include/scene.h
:
#ifndef SCENE_H
#define SCENE_H
#include <vector>
#include "ray.h"
#include "sphere.h"
class Scene {
public:
std ::vector < Sphere > objects;
void addSphere ( const Sphere & s ) { objects. push_back (s); }
Vector3 ray_color ( const Ray & r ) {
double t;
for ( const auto& obj : objects) {
if (obj. intersect (r, t)) {
return Vector3 ( 0.5 , 0.5 , 0.5 ); // Simple shading
}
}
Vector3 unit_direction = r.direction. normalize ();
t = 0.5 * (unit_direction.y + 1.0 );
return ( 1.0 - t) * Vector3 ( 1.0 , 1.0 , 1.0 ) + t * Vector3 ( 0.5 , 0.7 , 1.0 ); // Sky color
}
};
#endif // SCENE_H
Create src/main.cpp
:
#include <iostream>
#include <fstream>
#include "vector.h"
#include "scene.h"
void writeColor ( std :: ostream & out , Vector3 pixel_color ) {
out << static_cast<int> ( 255.999 * pixel_color.x) << ' '
<< static_cast<int> ( 255.999 * pixel_color.y) << ' '
<< static_cast<int> ( 255.999 * pixel_color.z) << ' \n ' ;
}
int main () {
const int image_width = 200 ;
const int image_height = 100 ;
std ::ofstream outfile ( "./image.ppm" );
outfile << "P3 \n " << image_width << ' ' << image_height << " \n 255 \n " ;
Scene scene;
scene. addSphere ( Sphere ( Vector3 ( 0 , 0 , - 1 ), 0.5 ));
for ( int j = image_height - 1 ; j >= 0 ; -- j) {
for ( int i = 0 ; i < image_width; ++ i) {
auto u = double (i) / (image_width - 1 );
auto v = double (j) / (image_height - 1 );
Ray ray ( Vector3 ( 0 , 0 , 0 ), Vector3 (u - 0.5 , v - 0.5 , - 1 ));
Vector3 pixel_color = scene. ray_color (ray);
writeColor (outfile, pixel_color);
}
}
outfile. close ();
std ::cout << "Image generated in image.ppm \n " ;
return 0 ;
}
add_executable (RayTracer main.cpp)
target_include_directories (RayTracer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} /../include)
Back in the build
directory:
cmake ..
make
./RayTracer
This will create an image.ppm
file in your project directory, which you can view with an image viewer capable of reading PPM files.
Congratulations! You've now built a basic ray tracer. This simple project can be expanded with more complex scenes, lighting, textures, reflections, and refractions for more realistic images. Remember, ray tracing in the real world involves a lot more optimizations and complex algorithms, but this gives you a starting point to explore and understand the fundamentals of how light interacts with virtual worlds.
Note : This guide provides a starting framework. For a production-ready ray tracer, you would need to handle many edge cases, optimize for performance, and possibly implement parallel processing or GPU acceleration.