Optimizing Bricks for Alpha 5, Part 2
We've doubled the frame rate on large builds. But how is that possible?
As promised in the last post, we're going on a deep dive into the rendering optimizations we've done for Alpha 5. This will be even more technical than the previous post, so strap yourselves in!
Rendering Millions of Bricks
Before we start throwing around lots of big words that need context, let's talk a bit about how Brickadia renders your fancy builds. Say you've placed two million bricks in the world. How can we get those on screen without a mid range computer exploding?
If you've played Minecraft or other voxel based games, you might be familiar with the concept of chunks. Basically, the world would be divided into chunks of some fixed size, and rather than attempting to render each individual block, you create combined meshes to represent each chunk and then render only those.
In Brickadia, we're making use of a similar concept that we call brick clusters. Since bricks are not just voxels on a grid, but individual shapes that can be positioned and oriented arbitrarily on a relatively fine grid, we needed to go a step further. If you place a lot of small bricks in an area, the cluster will be subdivided into smaller ones, so we're never stuck building a single crazy large combined mesh.
For example, let's look at the Castle MoorDread build by Facechild:
If we split this build into clusters normally, and then pull them apart, it looks about like this. You can see how highly detailed areas with many bricks are split into more small clusters:
Here's a closeup of an individual cluster:
Another example, let's look at Brickadia City by Sylvanor:
Pulling the clusters apart again:
An individual cluster from the city:
For each of these clusters, we can now generate a combined mesh that represents all bricks in it. If you place a new brick or modify one, only the cluster containing it has to be rebuilt, keeping update costs very low. Generating a new mesh takes only milliseconds, and we can do it asynchronously on worker threads so your game will never hitch from this.
Texture Arrays
Drawing a brick currently requires multiple textures for different faces. For example, here are the individual textures required to draw the resizable ramp brick:
Previously, we would collect all the faces from bricks in the clusters, group them by texture, and then split the cluster into multiple draws - one for each used texture. This works, though it generates unnecessary CPU overhead. We're requiring the engine to manage 4 times as many draws as we have clusters.
In Alpha 5, we've made use of the texture arrays feature that's recently become available in Unreal Engine. With texture arrays, a single draw can render the entire cluster, and the shader will dynamically select the correct texture for each face. The reduced CPU usage from this change resulted in higher frame rates when viewing large builds from a distance.
However, it wasn't nearly enough. Therefore...
Level of Detail for Bricks
Bricks generate a massive amount of geometry. For example, Castle MoorDread generates 4.5 million vertices. Brickadia City generates over 54 million vertices. These numbers are far too high if we want to be able to render the builds on mid range hardware!
For Alpha 5, we've implement a dynamic level of detail system for bricks, which replaces your builds with simplified versions at a distance where the fine details are no longer visible. This was quite a lot more complicated than initially expected, so let's get started.
The first thing we tried to do is simply take the combined meshes generated for the clusters and run them through some industry standard reduction algorithms. After all, if this worked, there would be no need to do anything more. For example, let's try this section:
One of the algorithms we tried to use to create a reduced detail version is vertex clustering. In theory, it's very fast and works on all kinds of meshes. However, the result of simplifying brick clusters with vertex clustering looks like garbage, so this is basically a non starter:
Another common algorithm is quadric-based simplification. This algorithm tends to provide high quality results and has been used nearly everywhere. For example, this is what Unreal Engine uses to automatically generate level of detail meshes for static meshes you import. The result for brick clusters also looks pretty good:
There's one small problem: it hasn't actually done anything. It failed to reduce anything at all. So this is also a non starter.
With standard reduction algorithms failing to produce useful results, we've had no choice but to start over from scratch and create our own level of detail system just for bricks.
The most obvious thing we can do if we want to reduce the detail of a brick build is to simplify the individual bricks by themselves. For example, at a distance you can't see any of the edges, whether ramps have lips, or whether a round is actually round:
Replacing all bricks with low detailed versions already has a noticeable effect on the amount of geometry we need to render. For example, look at this small (3700 bricks) segment from the outer wall of Castle MoorDread, rendered at high detail:
And the same segment rendered in low detail, with half of the geometry removed:
If we look at the wireframe of this low detail version though, we can see that it still has lots of useless geometry left over from being constructed out of many individual shapes:
That's where our new mesh reduction algorithm comes in, which we designed specifically to process this low detail mesh generated from bricks. It's able to quickly remove most of the unnecessary geometry while producing a mesh that looks exactly the same as before:
The wireframe looks a lot cleaner aswell:
In case that example was a bit too convoluted, let's take a step back and look at an even smaller example: a single brick wall. You've probably built one before.
In the top row below, you can see the mesh we get from combining all the individual low detail bricks. In the bottom row, you can see the mesh we get from our reduction algorithm, with all the unnecessary geometry removed:
The reduction algorithm takes only milliseconds to process tens of thousands of bricks, so it will always be active as you play. For example, here's how the low detail version changes as you place new bricks. This video is just for demonstration though, since you would see the high detail version at this distance - but another player watching you might see this!
Applying the level of detail reduction to real builds, such as Brickadia City, gives us pretty good results. For example, compare this image of the build rendered using only the original high detail meshes:
To this image, rendered with reduced low detail meshes, at a 82% reduction in vertices:
Of course drawing a frame involves many more steps than processing vertices, so this does not mean that the game will now magically run 5 times faster. To understand how much of an improvement you can really expect, we set up a camera flight around this build. Then, we measured the times taken to render a frame with the system disabled/enabled:
Lower frame times are better. You can see that the whole frame became around two times faster to render - that means you'll get around double FPS while viewing this build.
And that's it for this blog post! Stay tuned for the next one, where we'll show improvements we've made to various building tools for Alpha 5, and some of the new UI designs we've been cooking up.
If you have any questions or ideas, feel free to find us on Discord, Twitter, or e-mail!