Sebastian Kümper

MS Informatics: Games-Engineering

Real-Time Soft Shadows

Overview

This project was my Master's Thesis. The topic was to test whether and how soft shadows can be created using a voxel representation of the scene. In my final result, I use temporal jittered rays to create the soft shadows, which works quite well.

I built the whole project from scratch. I built a rudimentary engine to use at my Master's Thesis beforehand and programmed every algorithm on my own. The complete source code can be viewed at Github. An executable can be found here. It only includes a very basic scene due to the memory size of larger scenes.


XYZ-Dragon voxelized in 16k. Requires 100MB of storage.

Data Layout

The first thing I implemented was a voxelizer since the method should work with arbitrary scenes and without special input formats. I used a binary voxelization since color can be omitted for shadows and I only support opaque objects. This means every voxel is either set or not set. I also use only a surface voxelization because it isn't certain that the scenes are watertight and designed in with the thought of a solid voxelization. The voxelizer only outputs a list of the set voxels or more precisely elements with a size of 4x4 voxels to limit the memory requirement. The position is encoded into a 32-bit integer using Morton Code. After sorting and combining elements with equal positions, I can build a tree. The tree is required for raycasting, which is used for the shadow calculation.

The Morton Code ordering resembles a tree structure. I exploit this for a fast tree building by iterating through the tree levels and voxel elements list and marking the set areas. I use a 4^3-tree instead of a normal octree. This means that every node in the tree has 4*4=64 children instead of the eight children of a normal octree. This reduces the tree height, thus it reduces the number of traversal steps and improves the performance during the raycasting. A 4^3-tree requires a different strategy for storing the pointers for each node. For each node, I only store one single pointer instead of 64. Additionally, I store a 64-bit mask that represents which children are present. The position of the corresponding child node is given by the common pointer plus index among set children. The index among the set children can be easily calculated using bit operations and intrinsic functions.

The produced tree requires much less storage than storing the voxelization in a 3D texture, but it can be further reduced. Every scene has reoccurring structures in a voxel representation. This can be used to reduce the memory consumption of the voxel representation. I combine equal parts of the tree. I start with the leave nodes by combining similar leaves and altering the pointers that point to these leaves. The same is done level by level up until the root. This reduces the memory consumption by 70% to 90% depending on the complexity of the scene. I also tried to further reduce the memory consumption by combining similar instead of equal leaves, which further reduced the memory consumption by 10% to 20%.

Shadow Creation

The soft shadow creation is done via temporal jittered ray casting. First I render the scene normally: I use the normal pipeline with a vertex shader, rasterizer and pixel shader to obtain a shaded representation of the scene. I use the depth values of the rasterization to obtain the world position for every pixel. This world position is the starting position of the shadow ray. For normal hard shadows, I simply trace this shadow ray. If I hit a voxel I know that the starting point is in the shade. If didn't hit anything I know that the starting position is completely lit. For soft shadows, I jitter the ray over time. Each frame, the direction of the shadow ray is different. The direction is limited by the position of the light source and its size. Each frame on its own is the same simple hit or no-hit test as it was for the hard shadows. I combine the results of several frames with an exponential moving average to obtain smooth results. This emulates the testing multiple rays in a single frame for the computation cost of only a single ray. For a final touch, I use a screen space bilateral Gaussian Blur to obtain real smooth shadows and remove some minor artifacts.

The result is really promising. The shadows are realistic since I directly check the visibility of the light source with raycasting without any hard approximations. The shadows are also very smooth without any big artifacts. I also verified my result by using 100 shadow rays per frame. There is no significant difference to my method. The method runs in real-time at around 40 fps for a complex scene with a voxel resolution of 16k^3 on a nvidia GTX 970.

This project was my biggest project so far. I had to design classes, design algorithms and efficiently implement everything. I had to solve many problems and find and resolve many bugs. I think this project helped to become a better developer.