Posted by koonschi on Apr 17th, 2016

Ambient Occlusion

Ambient occlusion (AO) is a powerful tool when it comes to making games visually appealing. Especially for indies, since you can give plain and simple geometry (like Avorion's) a great sense of depth and make it look more three-dimensional.

For those of you who don't know what ambient occlusion is exactly and how your game can benefit from it, have a look at the following screenshot:

This is an animation of a space station with and without ambient occlusion. It's easy to see that one of the scenes looks "better". You get an impression of depth, of plasticity. Ambient occlusion basically means that objects that are near each other "steal" light from each other, so they appear darker.

To calculate ambient occlusion, from each point on your model, you shoot n (random) rays (here 64) with a fixed length in a semisphere formation and check whether you hit other geometry.

The more rays intersect with other geometry, the darker the point in space. There are more sophisticated approaches that take into account the distance of the intersection point to the ray origin, but in our case we'll just say we hit or we miss. We didn't necessarily need a physically correct approach, we only want something that looks decent.

AO Techniques and our Problem

There are various techniques for achieving ambient occlusion:

Usually you can do it in your 3D editor and bake it into textures like lightmaps, but then it won't work when your scene changes. In Avorion, players build their own ships or get them blown apart, so models change all the time and the texture atlas would have to be huge for decent quality.

Then there's SSAO (screen space ambient occlusion). It basically calculates ambient occlusion each frame based on what you see and the depth of your current screen image. SSAO didn't work either, because it largely depends on your current viewport and I wanted dark parts of spaceships to always be dark. Since SSAO is view-based the occlusion tends to change when you move your camera around.

Minecraft does ambient occlusion by stepping one or more blocks in each grid direction, to find out how many blocks there are and calculates a corresponding occlusion. But I can't step in any grid directions because in Avorion there is no grid.

The Solution

Since Avorion allows arbitrarily scaled blocks, there is no way to predict what shape the model is going to have. Plus the model changes all the time when players extend their ships or get them blown apart. So how do we solve this?

I found that the best way to solve this dilemma was vertex-based ambient occlusion. This way it's baked directly into the model and won't change when the camera moves. The great thing about vertex-based ambient occlusion is that you don't have to apply a blur to the result. Instead, the "blur" comes for free via the interpolations the GPU does between vertices!

That means now we'll be shooting rays from each vertex into the model. In order to get the ray intersections fast we used the bounding volume hierarchies that we had already from our collision model. This way every ray cast has a complexity of O(logn), which is pretty much the best we can get. And we needed the bounding volume hierarchy anyways for collision, so there were no additional costs here.

I did a first take on vertex-based occlusion and it turned out better than I had expected:

In this picture I'm using 64 samples per vertex, counting only hits and misses. It shoots fixed length rays depending on the normal of the vertex. 64 samples had the best tradeoff between performance and quality, above 64 there was nearly no difference in the result, but the performance hit got remarkable.

You can see several artifacts of the vertex-based SSAO model on the right side, where the grey block fades from grey to pitch black, and also in the middle of the space ship where there are blocks that are entirely black. That's be cause the vertices of these blocks are actually inside other blocks, so all sampled rays hit and the occlusion is black.

And there was another problem: Blocks can have any size. Even super large block sides only have 4 vertices to shadow, so if you place a small block in the middle of a large block side, it won't have any shadows because its 4 vertices are simply too far away from any geometry that could cast a shadow.

Okay, how do we solve that? Well, we have to increase the resolution of the models. So: Tesselation! Since we needed the tesselation in the model to calculate the ambient occlusion, and some of our target hardware doesn't have tesselation shaders, we couldn't use a shader. Instead I wrote a quick algorithm that tesselates the sides of the of the blocks like this:

Then we generate the ambient occlusion for the tesselated model based on the previous approach:

Looks pretty good already! You can see that there are dark areas on the big cube around the small cube and the quality around the intersections between the small and the big block is a lot higher. There's even a dark area on the top side of the big cube where the main part of the space ship is.

You can also still see the artifacts of the pitch-black vertices, but it's already a lot better and we finally settled for this approach, since we thought it looked good enough.

Then we one last problem: The ambient occlusion algorithm used random ray directions to collect the samples. Every time a user adds a block to a ship, the ambient occlusion would slightly change and create irritating flickering. We solved this by creating a predefined array of 64 fixed ray directions, so there is no more randomness going on.

Vertex-Based Lighting

The best part about this approach: You can use it backwards for vertex-based local ship lighting! We cast rays from light blocks towards all vertices on other blocks to achieve a sweet local ship light effect.

Performance

The calculations for the ships in the pictures took less than half a second with -O3 on gcc. This includes tesselation, ambient occlusion with 64 samples per vertex and lighting calculations. While for these smaller ships it was blazingly fast, stations with thousands of big blocks can easily take 10 - 15 seconds to compute.

But we never had any illusions about the performance of large stations, so we moved the AO calculations into a separate thread. Now, when as the player gets near a station, the AO calculations are started, and while they're running the plain model is rendered.

Wrap Up

Alright, that's it! I'm sure there are plenty who know already about this stuff, but I still hope some of you guys learned something new today.

Cheers

- Koonschi

Koonschi's Twitter

Avorion Website