← Back

Ray Tracing with Vanilla C++

2021

Computer graphics is the study of generating images with software. I studied it in Introduction to Computer Graphics at Boğaziçi University, taught by Lale Akarun. Over three homework assignments, we wrote a ray tracer in plain C++, with no libraries.

At the lowest level, an image is just a rectangular table of colors. A 1920×1080 image is simply 2,073,600 color values, one for each pixel. If a program chooses an RGB color for every pixel in that table, it has drawn an HD image.

The question is how to compute a color for every pixel. There are two main approaches: rasterization and ray tracing. This class used ray tracing, which produces very realistic images but is much slower than rasterization.

The core idea is simple: cast a ray from a hypothetical eye point through each pixel of an image plane. If the ray hits an object, write that object’s color into the pixel.

The eye casts a ray through every pixel on the image plane; gold rays are the ones that hit the sphere, while the darker rays miss and continue into the scene.

Everything beyond that builds on the same trick with more rays. Where is the light source? Can the hit point see it, or is another object in the way, so the point is in shadow? Does the light arrive head-on or at a grazing angle? Is the surface shiny, so the ray should bounce off it and continue into the scene?

Homework 1

The first program rendered only spheres, lit by one point light. Each pixel got one ray, and the nearest sphere intersection decided the pixel’s flat color. From that hit point, a second ray was cast toward the light: if it hit any sphere, the point was treated as shadowed and its color was multiplied by 0.1.

The first renderer, re-run with a transparent background: flat colors, and a shadow ray that darkens blocked points to 10%. The blue sphere's shadow falls on the gold one.
The first renderer, re-run with a transparent background: flat colors, and a shadow ray that darkens blocked points to 10%. The blue sphere's shadow falls on the gold one.

Homework 2

The second assignment added a ground plane and replaced flat colors with a simple illumination model. Every object still has one RGB color; what changes is how strongly that color shows up at each hit point. The final RGB is the sum of three contributions:

  • Ambient light: the object’s color multiplied by a small constant, the same everywhere, so nothing in the scene is ever pitch black.
  • Diffuse light: the object’s color multiplied by a brightness factor. The factor is the cosine of the angle between the surface normal and the direction to the light, so head-on light is bright, grazing light is dim, and light from behind contributes nothing. It also weakens with the square of the distance to the light.
  • Specular light: pure white multiplied by how well the light’s mirror reflection lines up with the viewing direction, raised to a high power so it collapses into a small sharp highlight.

The three are summed channel by channel and clamped to the displayable range.

The diffuse and specular contributions are dropped whenever the shadow ray from the surface point hits another object, leaving only ambient.

Reflections were implemented by continuing the ray after a hit. The renderer bounced each ray this way up to a maximum depth, shaded every hit along the path with the model above, and blended the bounces into one color. It is an educational reflection model rather than a physically based energy model, but it is enough to make objects appear in shiny surfaces.

Twenty diffuse spheres on a gray ground plane with hard shadows and a black sky. The same twenty spheres rendered with reflections, so every sphere shows mirrored copies of the scene around it.
The same scene traced without reflection rays on the left and with them on the right. Reflection rays make nearby objects visible inside shiny surfaces.

Homework 3

The final assignment added triangles. Triangles matter because almost any surface can be approximated by enough of them. For example, the Utah teapot is a combination of 28 cubic Bézier surface patches, each controlled by a 4×4 grid of points PijP_{ij} and evaluated by

B(u,v)=i=03j=03(3i)ui(1u)3i(3j)vj(1v)3jPij,0u,v1.B(u,v) = \sum_{i=0}^{3}\sum_{j=0}^{3} \binom{3}{i}u^i(1-u)^{3-i} \binom{3}{j}v^j(1-v)^{3-j} P_{ij}, \qquad 0 \le u,v \le 1.

To render those smooth patches with the triangle ray tracer, the program sampled each patch on an m×mm \times m grid and split each grid cell into two triangles, so the whole teapot becomes

282(m1)228 \cdot 2(m - 1)^2

triangles. Increasing mm makes the silhouette and highlights smoother, but it also gives every ray more triangles to test.

A very coarse, angular Utah teapot rendered with two by two samples per Bézier patch. A recognizable Utah teapot rendered with five by five samples per Bézier patch. A smooth Utah teapot rendered with nine by nine samples per Bézier patch.
Approximating Bézier patches with triangles. From left to right: 2×22 \times 2, 5×55 \times 5, and 9×99 \times 9 samples per patch.

The final scene combined all the pieces: spheres, a ground plane and three walls, the triangulated teapot, ambient/diffuse/specular shading, hard shadows, reflections, anti-aliasing, and BMP output. The body of the teapot and its rim, handle, spout, and lid got different material colors, assigned by patch index.

The final render: a triangulated Bézier teapot in a simple room. The spheres in front reflect in the teapot's body.
The final render: a triangulated Bézier teapot in a simple room. The spheres in front reflect in the teapot's body.

Read the original homework reports: HW1 PDF, HW2 PDF, HW3 PDF

Check out the source code on GitHub: github.com/sinaatalay/RayTracing

I’m always interested in discussing business and technology. I also have plenty of ambitious project ideas in mind if you’re looking for one.

Feel free to reach out at dev@atalay.biz.

Follow me at: LinkedIn, X, GitHub, YouTube, Reddit