CPU raytracer

CPU-only raytracer with interactive X-window or image output. Supports basic geometric primitives, lighting scenarios, and ambient occlusion approximation.

As presumably most of computer science students, at a certain point in time, there will be the ambition to come up with an own CPU ray-tracer/-caster implementation. While there should already be enough options to make use of in the wild, the main purpose of this project was to get a glimpse on the actual algebra behind.

Learnings from this project lead to the more recent maze light-tracer game implementation.

Apart from calculus related to spatial primitives, there also are other interesting aspects of this topic. These include for example techniques such as ambient occlusion, using Xlib, or the evaluation of possible optimizations (whether exact, numeric, or approximating). Currently, the supported features comprise:

A simple example scene consisting of 20 objects and 7 light sources can be seen below.

This video has been created by combining 20 frames rendered to separate images. A live screencast of animated X-window output would have been possible as well.

Scene description

A scene description file defines all primitives to draw, several general configuration options, and if/how the camera should move to produce several frames. We’ll assume these datatypes in the following:

vertex
A 3d coordinate vector written as (100,700,300).
unsigned/percentage/bool
Positive integer value, number between 0 and 100, or 0/1, respectively.
float
Signed float/double number such as 1.23.
color
An RGB color in hex notation, e.g. 0xffffff.
texture
An image/color texture definition, such as foo.ppm or 0xffffff. Images must be in PPM format and can be suffixed by :(0-3)[xy] – indicating a number of clockwise 90° rotations and whether it should be mirrored along the x- or y-axis. If prefixed with percentage+, this determines the object’s “radiosity”, i.e. the object’s intrinsic brightness.

Currently supported drawing primitives and light sources are:

sphere texture texture vertex center unsigned radius:
A sphere with a given position and radius.
plane texture texture vertex point vertex direction vertex direction:
A general plane given by a position and two direction vectors, thus actually being a parallelogram.
axisplane texture texture vertex point vertex direction:
A better optimizable rectangle that follows axis coordinates. Defined by a point and a diagonal that has to contain a single 0.
cylinder texture texture vertex start vertex end unsigned radius:
Creates a hollow cylinder between the two given points and a radius.
globallight percentage brightness:
Global illumination value. Will be added to all pixels unconditionally, thus creating minimal lighting.
raylight percentage brightness:
Virtual lighting added from the camera viewport itself. Visible objects will be illuminated depending on their surface’s normal (a straight look at 90° yields its full value).
pointlight vertex point color color percentage brightness unsigned len:
Creates a single colored light at the given point with a maximum length for dropoff.
arealight vertex point vertex direction percentage brightness unsigned len:
Defines an illuminated surface (or an approximation thereof), starting at a given point with a given diagonal (similar to axisplane). Also requires a brightness percentage and a maximum distance for dropoff.

Some experimental shaders are also available as work in progress:

General per-scene configuration options are:

set ao_len float
Ambient occlusion ray length: How far along the surface’s normal and for which radius to check for colliding objects. Feature disabled for 0.
set ao_lin bool
Linear ambient occlusion, which converges more slowly towards 1. Otherwise, this yields less difference between just-a-bit-occluded and just-not-occluded-anymore.
set ao_os|ao_fac float
Ambient occlusion weight offset and factor. Both should add up to 1, so for example each one 0.5.
set alight_samples unsigned
Number of samples per arealight dimension. Giving 3 here will thus trace 9 individual lights.
set supersample unsigned
Supersamples the final result in a postprocessing step. Smoothes the image and helps with aliasing artifacts. 0 disables this feature and e.g. 2 will half the image’s dimensions by computing 4 pixel averages.

Camera viewport and output control:

camera vertex origin vertex direction unsigned raster-width unsigned raster-height:
Defines the camera’s (starting) position and viewing direction. The length of the direction vector yields the distance from the camera to the rasterization plane and thus controls the field of view (FOV). The raster dimensions effectively control the output’s width and height.
pan unsigned frames vertex camera-movement vertex camera-pan:
Enables multi-frame output by defining camera movement. The given number of frames will be shown and/or written, with the given vectors applied to the camera’s position and viewing direction. Afterwards, when controlling the X11 window by keys, the camera vectors’ lengths will be used for the movement and viewport step size, respectively.

Raytracer usage

After building using make, run the resulting binary on a scene description file:

./crt [-j threads] scene.conf [outdir/]
-j
Process separate image parts in parallel, e.g. 4 will start a thread for each corner, 1 will disable threading. Rounded down to a power of 2, default 1. Requires the USE_THREADS build flag.
scene.conf
The description of the scene to draw, see above for syntax and details.
outdir
Where to place the output image(s) in PPM format. Optional, as an X window for interactive tracing will be used if available (requires the USE_X build flag).

When tracing the scene as defined by the input file, the result will be:

After the first result, remaining frames are drawn according to the pan setting. Between each frame (if more than 1), the camera position and viewport direction will be moved accordingly.

If the output window is available, the camera can then be moved through the scene interactively:

The resulting output images can be combined and rendered into a video by using for example ffmpeg -r 25 -i '%d.ppm' -preset veryslow out.mp4.

Build options

There are several build-time options available, which can be added or removed as -D CFLAG in the provided Makefile.

USE_X
Enables X11 support for “realtime” rendering with interactive controls. Disable this for an improved performance during bitmap image output. Enabled per default. Requires the -lX11 linker flag, library, and headers.
IMAGE_LIN_AVG
When supersampling, use linear RGB interpolation instead of squared component distance (faster).
IMAGE_INTERPOLATE
Enable 2D linear interpolation for texture pixels. Otherwise the nearest neighbor is chosen.
TRACE_ALL
Before tracing, the scene will be preprocessed – objects that won’t be visible or are occluded under certain circumstances are then skipped at runtime. This flag disables this feature.
STATIC_LIGHT
Enable static lighting. In a (lengthy but parallelized) preprocessing step, every object’s surface gets traversed and lighting (and occlusion) information is collected. Improves runtime performance.
STATIC_LIGHT_RES
When using static lighting, use this resolution in scene units. For texture images, their very own resolution in pixels is taken. The default is 1.0.
LIGHTMAP_INTERPOLATE
Produce softer shadows by linear lightmap interpolation.
PLIGHT_SQ_DROPOFF
Squared instead of linear dropoff for pointlights (faster).
NO_CLIP
When moving due to keyboard input, don’t check for clipping.
NO_OBJ_LRU
Enables several LRU caches for objects hit or occluded by the last ray or light source.
WRITE_PLAIN_PPM
Writes PPM images in ASCII instead of in binary format. Will be slower but might be needed for compatibility.
STATS
Collect timing statistics and counters for debugging purposes. Slows everything down significantly.
USE_SIMD
Use floating-point SIMD intructions for vertex calculus. Can provide a performance benefit.
USE_THREADS
Support multithreaded shadowmaps generation and rendering using the -j argument. If disabled, only a single process is used instead. Enabled per default. Requires the -lpthread linker flag, library, and headers.
KEY_REPEAT
Honor repeated keypresses. Otherwise, only a single keystroke after rendering the current frame gets processed.
NDEBUG
Disables assertions and additional checks (faster). Enabled per default.

Code & Download