Introduction to Ray-tracing
By Amine Rehioui, Mar 21, 2019
Ray-tracing is a graphic technique that can produce realistic images, by simulating the path of light and its interactions with the environment.

The idea is inspired by real-life: We see the world thanks to the light that originates from light sources, interacts with the environment, and ends up in our retina.

In practice, it’s not feasible to consider light as originating from light sources, because it would mean wasting time simulating paths that may fall outside of view:

A much better approach is to simulate light paths from the viewer to the light sources. This is called backward tracing. Performance-wise, it’s a win because only objects in the field of view are being processed. Visually speaking, the result can be the same since light propagation is a symmetric process, and the equations work the same in the reverse direction.

Here is a simple implementation of this idea (in Typescript)
Shading
Shading is the process of determining the color at each pixel of the resulting image. In this article, we will use a Diffuse Shading model to simulate how light is absorbed and reflected.

For each pixel, we collect the information needed for shading. Namely, the intersection point (P) between a ray projected from that pixel and the environment, the surface properties (Normal and Light direction) at that location, and the properties of light. The color is calculated as:

- d: The diffuse color at the intersection point
- Li: Light intensity
- Lc: Light color
- θ: The angle between the Normal and the direction towards the light (Ld)
Here is the resulting image, along with sample code:

Reflections
Reflections are a natural byproduct of ray-tracing. When light hits a reflective object, it changes direction and continues traveling until it either doesn’t hit anything, or the maximum amount of bounces is reached.
We refer to the reflected rays as secondary rays, in contrast with the original rays which are called primary rays. Each time a secondary ray is generated, we accumulate the color of the intersection point that created it, using the same shading equation we saw earlier. The final color at the original intersection point is simply the sum of all colors encountered while bouncing secondary rays.

An object reflects light depending on its material properties. In this article, we define a reflectance factor on materials. Implementation-wise, the best practice is to make the ray-tracer a recursive process, so that reflected rays are processed in the exact same way as primary rays. Here is the recursive ray-tracer and the corresponding result:

Shadows
To support shadows, we must determine whether the intersection point at each pixel is reachable by light. If no light is accessible, it must be darkened. We introduce the concept of shadow rays. For each intersection point, we cast a ray towards each light source. If a light is not reachable, we remove its influence from the shading equation by zeroing its intensity.

Here is the new implementation, taking into account shadows:

Smooth Shadows
Sharp shadows happened because we considered light sources as single points in space. In reality, light sources are just like any object in space, with a shape and a volume, they just happen to emit light. When shading, we must test how much of the light source is visible from each pixel of interest, and shade accordingly.

We give light sources a non-zero volume and define a number of sample points on their area that will be used for casting additional shadow rays. The implementation is exactly the same as for sharp shadows, but since we are now casting multiple shadow rays, we must keep track of the number of occluded rays. Then we adjust the light intensity variable to the inverse ratio of occluded rays vs total rays:

Here is the result with smooth shadows:

Optimizations
Most of the time spent by a ray-tracer is in computing ray-object collisions. They need to be done multiple times, for each pixel on the screen, which can be very expensive. A form of Spatial partitioning is needed to ray-trace most worlds in a decent amount of time. This deserves its own article and I will cover it in a future post!
Check out the playable demo based on this blog on spiderengine.io.