Before starting this article make sure you are familiar with the visualization tools and vectors, we’re going to use them extensively in this raytracing series!

In this article we’re going to do a couple of things:

  • Define structures for a ray and sphere
  • Create a camera model
  • Intersect rays with the scene and output a color

Rays

A ray can be defined as two vectors: one to define the starting position (also known as the origin) and one for the direction it’s traversing in. In code this looks as follows:

1struct Ray
2{
3    Float3 Position;
4    Float3 Direction;
5};

In order to traverse along the ray we can calculate a new position where the ray ends up by adding a variable amount of the direction. We’ll call this variable amount $t$. Using $t$ we can now see that any point on the ray can be shown as follows:

$t$ can even be negative, meaning we trace the ray backwards!

Sphere

We use spheres as they are the simplest object to define and create an intersection test for. A perfect sphere can be defined pretty easily using a vector and a scalar. Similar to the ray, we have to define the position of the sphere using a vector. The scalar is used to say how large the sphere is by defining the radius of the sphere:

1struct Sphere
2{
3    Float3 Position;
4    float Radius;
5};

To visualize these spheres we have to see if a ray hits the sphere. Therefore we are going to define a ray-sphere intersection. The ray-sphere intersection will be as simple as possible, we are only going to detect if the ray hits the sphere.

The ray-sphere intersection requires a little bit of linear algebra to solve. Before we set up the formula let’s define what parameters we have to work with:

  • $O$ = ray position (origin)
  • $D$ = ray direction
  • $t$ = parameter for the amount of ray direction
  • $C$ = position of the center of the sphere
  • $R$ = sphere radius

The idea is to find a point on the ray that is exactly on the surface of the sphere. We know that any point on the surface of the sphere is exactly $R$ away from the center of the sphere, this is how the radius is defined. Let’s now assume our Sphere is exactly at the world origin $(0, 0, 0)$. Using that we can define the sphere as follows:

$$x^2 + y^2 + z^2 = R^2$$

Which means any point $(x, y, z)$ is on the sphere if the distance from the origin is exactly equal to the radius. Using this we can compare any point on the ray to any point on the sphere:

$$|O + tD|^2 - R^2 = 0$$

This equation tells us that we are looking for any point on the ray (using $t$) which is exactly on the sphere. So if we can solve this equation for $t$ we know at what distance we intersect the sphere. Solving this requires us to rewrite the equation by expanding the brackets (the $||$ brackets indicate length). After expanding the brackets we’ll end up with this:

$$t^2D^2 + 2OtD + O^2 - R^2 = 0$$

This equation is in the form of a quadratic function:

$$ax^2 + bx + c = 0$$

When we translate to this standard form, remember that we are solving for $t$, so we use $t$ instead of $x$ and then we can translate all the other terms to fill in $a$, $b$ and $c$. We will end up with the following terms:

  • $a$ = $D^2$
  • $b$ = $2OD$
  • $c$ = $O^2 - R^2$

The quadratic function has a known solution which is defined as follows:

$$x = \dfrac{-b \pm \sqrt{b^2-4ac}}{2a}$$

For now we are only interested in the discriminant which indicates if we have any intersections at all. The discriminant is this part of the equation: $b^2 - 4ac$. If the solution is 0, there is one solution meaning the ray hits the sphere. If it’s larger than 0 there are two solutions meaning the ray passes through the sphere.

There is one additional term we’ve ignored until now which is the sphere position. Adding that to the mix we get the initial equation:

$$|O + tD - C|^2 - R^2 = 0$$

Rewriting that is left as an exercise, but the results are quite similar. Translating all that to code we get the following:

1bool IntersectSphere(const Ray &ray, const Sphere &sphere)
2{
3    Float3 difference = ray.Position - sphere.Position;
4    auto a = Dot(ray.Direction, ray.Direction);
5    auto b = 2.0f * Dot(difference, ray.Direction);
6    auto c = Dot(difference, difference) - sphere.Radius * sphere.Radius;
7    auto discriminant = b * b - 4 * a * c;
8    return discriminant > 0.0f;
9}

If none of this made sense or you’d like to read it in other words, I can highly recommend following the ray-sphere intersection tutorial on Scratchapixel.

A simple camera model

We are going to make a simple camera through which we view the world. The camera model itself is no more complex than a ray, we only store the screen resolution in addition to the position and direction:

1class SimpleCamera
2{
3public:
4    Float3 Position;
5    Float3 Direction;
6    Float2 Resolution;
7};

To create a ray for every pixel on the screen, we are going to create a viewport: a virtual representation of our screen in front of the camera. Once we have this viewport in place we can interpolate over the x and y axis similar to what we do when setting a color for every pixel in an image. The idea is to create a plane in front of the camera spanning a range between -1 and 1 (so a total width and height of 2 per axis).

Before we dive into coordinates we have to define a coordinate system. For our camera we are going to use the z-axis as the up axis, this means that if we move in the positive direction of the z-axis we are going up. In short this is called z-up. Different programs use different conventions here, also commonly seen is y-up which means the y-axis would move you up.

If the camera would be perfectly on the world origin (0, 0, 0) and looking in the positive y-axis direction, the plane coordinates are as follows:

1TopLeft         = (-1, 1,  1)
2TopRight        = ( 1, 1,  1)
3BottomLeft      = (-1, 1, -1)
4BottomRight     = ( 1, 1, -1)

To interpolate between these coordinates we don’t have to know all of them. We can calculate the top-left coordinate and two vectors to move over the x-axis and y-axis respectively. This requires a bit of vector math to calculate:

 1void UpdateViewport()
 2{
 3	// Calculate orthonormal basis: 3 vectors that are all perpendicular to each other
 4	Float3 w = Direction;
 5	Float3 u = Normalize(Cross(w, Float3(0, 0, 1)));
 6	Float3 v = Cross(w, u);
 7
 8	// Aspectratio is the ratio between the x and y length of the viewport
 9	float aspectRatio = Resolution.x / Resolution.y;
10	// We describe the viewport in 3 variables:
11	// TopLeft: top left position of the viewport
12	// Horizontal: TopLeft + horizontal moves over the x-axis of the viewport
13	// Vertical: TopLeft + vertical moves over the y-axis of the viewport
14	Horizontal = u * 2 * aspectRatio;
15	Vertical = v * 2;
16	TopLeft = Position - Horizontal / 2 - Vertical / 2 + w;
17}

Scene

Our scene will be really simple with just a list of spheres for now. In the scene we define a function to traverse the ray through the scene. This traversal function will do an intersection test with all the spheres from the list to see if the ray hit them.

To sum up everything we will do in the code below:

  • For each pixel, calculate a ray using camera parameters
  • For each ray calculate intersections with each sphere
  • If we hit a sphere we return a red color
  • Otherwise we return a blue sky color