Ray Tracing Adventures

Doruk B.
12 min readOct 31, 2022

--

In computer graphics, ray tracing is a rendering —generating a digital image— technique that can realistically simulate how light interacts with objects. After shooting out from the light source, light will bounce off, get absorbed, get reflected or even get refracted by the objects it interacts with. More or less, this is the behaviour that we will try to simulate by code, in C++. The rendering will be performed in CPU and off-line, meaning it won’t be an interactive real-time application just yet. In later chapters we will implement several optimizations such as acceleration structures to get close to a real-time performance.

In this first chapter of the adventure, we will take off by first figuring out how to setup a basic non-interactive camera and an image plane. Then the rays will be generated from the camera and will “travel” in the scene, looking for intersections with the objects. Following this, after we find the appropriate interesection point, we will perform not only basic shading such as diffuse, ambient and specular, but also some more interesting ones like reflection and refraction.

Here is what we can achieve at the end of this post:

METU Science Tree Model with a Glass Material

OK, let’s move on to the actual stuff before losing that motivation.

Camera and Image Plane

A simple 3D camera can be specified by a position e, and an orientation which is defined by the three vectors: u,v and w. A lecture slide can illustrate this better.

image credits: Prof. Ahmet Oguz Akyuz, CENG 795

The image plane is defined with respect to the camera and is typically
centered and orthogonal to the gaze direction.

image credits: Prof. Ahmet Oguz Akyuz, CENG 795

You can think of the image plane as the blank canvas that we are about the paint with colors, to “render” our final image.

Casting Rays and Intersections

Now that we have the camera and image plane setup, we can actually start casting rays from the camera or “eye’.

Here is how the process looks for a single pixel, analogous to a fragment/pixel shader in a forward rendering pipeline.

Main process that calculates color for a given pixel.

Coordinates are basically the pixel coordinates on the image plane. For instance, to create a 800x600 image, first pixel or the top-left corner, would be (0,0) and the last one or bottom right corner, would be (799,599).

Generating the ray is then just computing the vector from the camera position to that pixel, normalizing it and setting it as the direction of the ray. Origin of the ray is simply the location of the “eye”, camera in this case but mind that this could change for example while we are doing reflections. These rays that are originating from the camera are also called “primary rays”.

Next, we perform intersection tests for each of the objects in our scene. Current implementation supports: perfect mathematical spheres, triangles and 3D Mesh structure which consist of bunch of triangles at the end.

Detecting the intersection with each of the listed structures is math heavy and there are lots of good sources out there so I will skip those details, but for the record this raytracer is using the barycentric coordinates method for the triangle intersection. Mesh intersection is actually just triangle intersection, repeatedly.

On the other hand, mathematical spheres can be defined by an implicit and thus concise equation that allows us to compute the intersection easily.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

In the above equation, everything except t is known, so solving the resulting quadratic equation and then plugging t into the ray equation is enough to find the intersection point.

In the case where we have multiple intersections detected, we look for the closest one relative to the ray origin, we can think of it as the object closest is in “front” of the other one and is occluding everything behind it. Even in the case of translucent materials such as dielectrics, this rule will apply, however we will cast secondary rays to handle that “seeing through” effect.

Shading

We have found the intersection point on an object, so what?

Remember that we were sending rays for each of the pixels that will form out final image, so the final goal is to compute a color value, RGB in our case, for each of the pixels. To do that, we need to perform shading operations at the point. There are multiple parts to this, and depending on the type of the object, or the “material” it has, we have differents things to do.

Diffuse

The most basic component is the diffuse shading which is useful to render matte objects.

Diffuse rendering of two spheres

How did we control the color?

Well, this is a topic in itself, but here is a physically correct explanation.

Objects attain color depending on how much they reflect a given wavelength of light. Simplified, if an object is reflecting the wavelength of light that corresponds to the red color more, then it looks reddish.

In our renderer, we will assume “reflectance” of an object is the same at every point, not a curve as is shown below. Simulating that variety in fact creates lot more realistic images and is called Spectral Ray tracing.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

So, back to our question. We have simply set the reflectance of each sphere to reflect more red light in the case of the red sphere, and more blue light for the blue sphere!

Material properties as seen in our custom Scene.xml

Let’s add two more pieces to our shading: Ambient and Specular.

Ambient and Specular

Diffuse + Ambient + Specular

Ambient shading is a very crude approximation to the concept of Global Illumination and is responsible for lighting up the perfectly dark spots from before. With diffuse shading, any part of the object that did not directly face the light was completely dark, which is not physically correct. The reason for that is light is not getting fully absorbed in each hit, but rather getting scattered around and thus reaching to places that are not facing the light directly. Ambient shading simply adds a constant value to all parts equally. Here you might notice the greenish tint on the red sphere, that’s because the ambient reflectance for that sphere is setup to reflect more green, which doesn’t make much sense as the object is reflecting red color in general, but still goes to show that it is possible and also to clearly show how ambient shading is effecting the whole picture.

Specular shading on the other hand, is dependent on the view position, and simulates shiny surfaces.

Shadows

What about shadows? You probably already noticed that in our example two sphere scene, the light is coming from the top right side, and you would expect to see some shadows on the red sphere, so let’s make that happen.

Shadows added

It is not the prettiest shadow, its quite darker than it should be and is also jagged on the edges, but will do for now.

To find out if a point is in shadow, we do the intutive thing: cast a ray from the intersection point towards the light source and check if it intersects with any of the objects in the way. If it does, then the point is in shadow.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

We are done with basics and can move on to more interesting stuff like reflections and refractions, but before we go let’s render some other scenes with the same methods.

More spheres!
Scene with rather complex meshes.
The classic bunny scene

Back Face Culling

To make testing easier, let’s first implement a simply optimization called back face culling or BFC for short. We will not test for intersection for triangles that are facing away from our ray. This is a valid operation if the object is not transparent and thus the closest intersections, if any, will occur on a front facing triangle.

Implementation is simple. A face is actually a triangle with precalculated normal information.

Here are some benchmarks on different scenes with the only difference being back face culling enabled or not:

BFC Disabled bunny scene
BFC Enabled bunny scene

Observed decrease in rendering time is roughly 27% for the bunny scene which has around 2500 triangles.

For the science tree scene:

BFC Disabled
BFC Enabled

Observed decrease in rendering time is around 18% for the science tree scene.

It’s safe to say the amount of improvement is heavily dependent on the scene setup but considering how simple the implementation is, the speed up is quite nice whenever we can get it. Unfortunately we will have to disable BFC for dielectric materials as they behave as though they have doubly-faced triangles and so BFC will lead to wrong renders.

Perfect Mirrors

So far we have no way of rendering reflections and that is unacceptable for a ray tracer!

We will start correcting that by implementing perfect mirrors which are assumed to perfectly smooth surfaces so as to reflect the incoming light in a symmetrical manner relative to the surface normal.

The main idea is to calculate the direction the ray will follow after being reflected at the intersection point, and then cast it into the scene once again, this time with a different direction and an origin.

Origin is simply the intersection point, slightly offset in the direction of the surface normal so as to avoid self intersection issues.

Reflection direction is simply calculated as explained below.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

One question remains: for how long we will keep reflecting the ray?

It is not hard to imagine a scenario where the ray is just ping pong’in between two mirrors, indefinitely. To avoid this infinite looping and also taking forever rendering a scene, we will simply introduce a maximum number of bounces or reflections, per ray.

Here is how it looks:

First, with a single bounce/reflection. We can see spheres are being reflected onto the ground plane and also onto the center sphere which has mirror material.

With single bounce allowed (MaxRecursionDepth = 1)

This time we do one more reflection, we can see that the reflection of the sphere on the ground is reflected on itself.

With two bounces allowed (MaxRecursionDepth = 2)

Finally, with 6 bounces.

With six bounces allowed (MaxRecursionDepth = 6)

Dielectric Materials

Mirrors are nice and all, but only reflection is not that interesting. This class of materials simulate transparency and will help us render objects such as glass which both reflect and refract rays.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

This part is quite a bit more math heavy and I will resort to the lecture slides, but one can also check the amazing and free PBRT book.

Reflected ray computation is the same as in mirrors, again assuming that the surface of the object is perfectly smooth.

Aside: Microfacet Models

I have came across other more physically realistic methods such as Microfacet Models while researching this topic.

In reality, objects are never going to have fully smooth surfaces but instead varying degrees of smoothness. The less smooth a surface, or equivalently the more rough a surface, the more random and disturbed the resulting reflection and/or scattering direction.

Microfacet model as explained in Physically Based Rendering: From Theory To Implementation, © 2004–2021 Matt Pharr, Wenzel

Back to reality though, we are implementing a much simpler ray tracer for now and will just assume that surfaces are perfectly smooth.

The basic information we need to compute the refracted ray then is the refractive indices of both mediums, which are obtained experimentally, the incoming ray direction and the surface normal at this point.

Credits: Professor Ahmet Oguz Akyuz, CENG 795
Credits: Professor Ahmet Oguz Akyuz, CENG 795

When the term inside the square root, here annotated in a red circle, is negative that would mean we have no solution for the refracted angle. This is the case where the light is totally reflected towards the medium it’s already in. This phenomenon is also called “total internal reflection”.

To find out the ratio between reflection and refraction, we use the Fresnel Reflectance formulas.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

Now let us take a look how it looks in a scene with a conductor sphere (we will talk about conductors shortly) on the left hand side of the screen, and a dielectric one on the RHS, closer to the camera.

Dielectric sphere has no color reflectance by itself, it is not even reflecting back the ambient light, but only performing the reflection and refraction computations as explained above and thus “blending into” the scene around it.

Vanilla cornell box scene with conductor and dielectric material

I went ahead and took few different shots in this scene by simply moving the camera around.

A top-down view from the side
A top-down view from the front

We can observe that the dielectric material looks quite different depending on viewing angle.

Well, all seems to be working however the shadows, being quite basic, are not really correct for the dielectric sphere. Transparent objects such as glass do of course cast shadows, but they are more of a patched nature, are brighter compared to matte object shadows and depending on the angle, can let light pass through and converge at a point, and make it brighter.

Let me share some real life footage which led me to think about this issue.

Real-life indoor shadows, glass versus matte objects.

Here is an experiment with shadows completely removed, from the same point of view.

No shadowcasting at all, top-down view from the front.

This is a limitation of the current implementation and one that we might address in the future, as our adventure continue.

Moving on, we can now render that science tree scene we have seen at the start!

The tree object has a dielectric material while the ground plane has mirror properties.

METU Science Tree Model with a Glass Material

Let’s see what happens if we change the material to diamond by changing the refractive index to 2.42.

Same scene, with Diamond material

Attenuation

One detail we have glossed over so far is attenuation of the light. As light travels inside a medium other than vacuum, it loses energy. We did not consider it so far since we assumed our rays were moving in a vacuum. However, in the case of refraction at the surface of a dieletric object, a ray might enter an object and travel inside that object and in those cases we should apply attenuation as described by the Beer’s Law.

Beer’s Law
Credits: Professor Ahmet Oguz Akyuz, CENG 795

We’re almost through. The last thing we haven’t talked about is the conductor materials. We already have seen them in the two spheres scene with a dielectric one, but let’s take a look how they are implemented.

Conductors

This class of materials can simulate metallic objects. They are opaque and reflect back a a significant portion of the illumination. As mentioned in the PBRT book, they do indeed transmit a very small amount of light energy into the object, but it is almost instantly absorbed, and thus for any practical purposes, refraction amount is considered to be zero for conductors.

However, they are still governed by the Fresnel equations and we will use them to calculate how much of the light is absorbed and how much is reflected back.

Credits: Professor Ahmet Oguz Akyuz, CENG 795

Reflection direction is calculated in the same way as we did for perfect mirrors.

Here is the scene from before, but this time both of the spheres are conductors.

Both spheres are conductors with different reflectance and refractive properties

And this concludes the first chapter of our ray tracing adventures.

Thank you for reading thus far and hope to see you in the next chapter.

Until then take care!

Doruk Bildibay

References

--

--