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.

captionless image

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.

captionless image

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:

captionless image

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.

Backward tracing of light paths

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.

captionless image

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:

captionless image

  • 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:

captionless image

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.

captionless image

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:

captionless image

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.

captionless image

Here is the new implementation, taking into account shadows:

captionless image

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.

Some light samples not reachable, the point is partially shaded

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:

captionless image

Here is the result with smooth shadows:

captionless image

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.

← Back to Articles