Batch your polygons

Continuing my little series on how I created my AppWeek game, let’s talk about triangles, specifically those that make up a level. When working on a 3D game, you’ll more than likely reach a point where you need to optimize your rendering. You’re either making too many state changes or too many draw calls. Generally the solution is to implement some sort of culling mechanism into your pipeline, however there are other approaches to optimize your rendering.

My levels were built using “brushes” in 3D World Studio. A brush could be thought of as a single model. This meant that my draw calls were based on my brush count. My levels had quite a varying number of brushes in them:

Bridge – 33 brushes
Cargo Hold – 86 brushes
Spiral – 179 brushes

This meant that on Spiral with four players I’d be performing at least 716 draw calls (one per brush per camera) just for the world. I say at least because that doesn’t include the possibility of brushes that are using multiple textures or lightmaps, each of which would add to the draw call count. That’s far too many for just the world because I would still need to render the weapon and health pickups, the guns the player’s are holding, and the UI. This had to be fixed.

I decided that instead of optimizing my runtime to cull out geometry, I would instead collapse it at build time into as few actual models as possible. I started by looking at some of the numbers involved with my levels:

Level Brushes Textures Lightmaps Triangles
Bridge 33 12 2 396
Cargo Hold 86 5 2 1032
Spiral 179 5 1 2148

While I had quite a large number of brushes in my larger two levels, I had relatively few possible combinations of textures and lightmaps. What I did was then build up a map that allowed me to relate every triangle to a combination of a texture and lightmap. I then batched up all of these into models such that a given model was all the triangles in the entire level that could be drawn together. When everything was said and done, my resulting model counts were great:

Bridge – 14 models
Cargo Hold – 9 models
Spiral – 5 models

This reduces Spiral’s cost from at least 716 draw calls for four players to just 20. Massive improvement and no runtime overhead. Interestingly we see that while there was savings across the board, the larger levels ended up being cheaper to draw because they used fewer textures which resulted in greater batching.

The lesson of this post is to really think through a problem before you try to fix it. I could have spent a lot of time figuring out portal visibility culling or some other runtime optimization, but by implementing a fix in my pipeline, I was able to significantly reduce my rendering costs without any runtime overhead.