From screen space to voxel space
There has been quite a few changes to my rendering pipeline over the last couple of months. The biggest being that I now do full raytracing in voxel space instead of the screen space counterpart.
This may sound like a major rewrite, but a lot of the pipeline actually stays the same. I simply trace rays in a huge 3D texture instead of screen space. This obviously has a number of benefits, like real ambient occlusion, long shadows, specular occlusion and no screens space artefacts, but it also comes with a number of drawbacks. The biggest one probably being memory consumption. I chose a texture resolution of 5 cm and combined with a world size of 100x100x25 meters this gives two billion voxels, or two gigabytes if storing one voxel per byte.
Since each object has it’s own transform and can be freely moved and rotated, I have to rasterize each object into the big world texture continuously. This is done on the cpu and the relevant parts of the texture is updated with glTexSubImage3D
. For large objects, this can be rather slow, so the technique is not for everyone. I’ve been surprised by how well it works in practice though, since most dynamic objects are usually rather small. If there are several objects moving at the same time, I cluster them and send updates in larger chunks where they are needed.
Voxel storage
Note that the big world space texture, the shadow texture, only requires one bit per voxel. Using a whole byte means wasting eight times the memory we really need. Therefore I store eight neighboring voxels per byte in an octree fashion, so each bit represents one octant in a 10 cm cube. If the byte is zero is means there are no voxels in any octant, and this can be exploited later to speed up raytracing. In addition to the base level 10 cm resolution (or 5 cm if you count the octants) I also store two mip levels, one for 20 cm and one for 40 cm. This gives a total of four mip levels, including the octant bits. This gives 256 + 32 + 4 = 292 Mb for the shadow texture instead of the two gigabytes, including two mip levels which can be used to speed up raytracing.
Raytracing the shadow texture
Raytracing in voxel space is actually much simpler than in screen space. Just start at the camera and walk the ray direction in voxels space until hitting something that isn’t zero. Note that walking the voxel space the same direction in fixed steps will not give a water tight result. Light can leak through voxels in certain scenarios as described in this paper, making implementation a little trickier. The paper also presents a solution to the problem, an algorithm sometimes referred to as “supercover” traversal.
I actually use both supercover and the cheaper fixed step tracing depending on the use case. Ambient occlusion, for instance, or volumetric fog, doesn’t require exact tracing. This is were voxel raytracing really shines. In triangle raytracing each ray has a fixed cost, while in voxel raytracing you can choose which fidelity you need for each particular ray and even change fidelity while walking the ray.
To further speed up raytracing I also utilize the other mip levels, starting in the lowest mip level and if it hits, switch to the larger ones progressively. The base mip level requires some bit masking to find out if it really hits, but the general algorithm is the same.
Here is test scene without shadows and ambient occlusion. It’s a bit unfortunate that these are voxel objects themselves. It’s not really necessary, since the shadow volume rasterization would work on any closed mesh, but I don’t have it in my code yet, since I’m currently working on a game with voxel graphics.
Let’s add ambient occlusion with five rays per pixel in the most naive way possible. Just walk from the pixel in a semi-random direction on the hemisphere.
As seen in the image, this won’t work very well, bacause the shadow volume is not perfectly aligned with the object surface. You can think of this as an extreme version of shadow acne found in regular shadow mapping. To overcome this problem, I don’t start tracing at the pixel position, but offset the ray origin a safe distance, based on the normal and ray direction. This will prevent the ray from hitting the shadow voxel that comes from the pixel surface.
While capturing the overall occlusion very well, you might notice that it lacks fine detail. This is an artefact from the ray origin offset.
Combining with screen space
To capture finer detail, we need a backup method for the distance from the pixel out to the new ray origin. Fortuantely there is another technique that works really well at short distances – screen space raytracing! It turns out all my previous work in screen space raytracing now come in handy. Here is the contribution from a short distance of screen space raytracing:
And finally combining the two by starting in screen space, trace the safe distance and unless it hits something switching over to voxel raytracing and continue through the world:
Light sources
The exact same techinque can also be used for light sources. Here is the scene with a spotlight with zero radius. One ray per pixel is shot from the pixel towards the light source, starting in screen space and then moving over to voxel space.
As you might notice, the voxel grid becomes pretty noticable with sharp shadows and particularly so with light coming in at a sharp angle. However, since we are now raytracing the shadows, it’s really easy to make soft shadows by just jittering the light position. This will effectively hide artefacts from the voxel grid, at the same time producing accuracte soft shadows “for free”. Here is the same light but with a 30 cm radius:
I also use the same raytracing technique for reflections and volumetric fog (god rays). For reflections, I use screen space reflections for the reflected image where available. Where it’s not available I fade to black, simply because I don’t have any information about the hit surface. The shadow volume is binary and knows nothing about materials. However, and more importantly, since I do the ray tracing in voxel space, I get specular occlusion for everything, not just what’s visible on screen. Blurry reflections generally look better for the same reason that soft shadows look better than sharp – because they hide blocky artefacts better.
Performance
Voxel raytracing performance depends largely on the length of the ray, the resolution and the desired quality (step length, mip map, etc), but it is generally really, really fast compared to polygon ray tracing. I don’t have any exact measures of how many rays per pixel I shoot, but for comparison I do all ambient occlusion, lighting, fog and reflections in this scene in about 9 ms, including denoising. The resolution is full HD and the scene contains about ten light sources, all with volumetric fog, soft shadows and no precomputed lighting. Timings taken on a GTX 1080.