#6.5 3D Optimization

After the last post, I was quite worried about the performance (as you’ve probably noticed), and I literally couldn’t sleep over it. So I took a few days off working on the game to learn about 3D (model) optimization, and the results are really great! I want to explain my findings in this devlog, and hopefully I can save many fellow game developers some headaches.

What causes lag?

Two components work together to run games: the CPU and the GPU.

The CPU is the Central Processing Unit, which handles all your code. Unless you’re creating a heavy strategy game, the code is not the problem. (One thing I did to crash a game once, was implement a really inefficient pathfinding algorithm. It certainly can be done, but it won’t be the bottleneck in most cases.)

The GPU is the Graphical Processing Unit, which handles displaying your game. This is where most problems come from, but this is also where they can be fixed!

The CPU sends “draw calls” to the GPU, every time it needs to draw something (surprising, I know). The more draw calls you’re making per frame, the heavier your game is on the computer, and the more likely it is for lag to occur.

There’s no “ideal number of draw calls”, you just need to keep it as low as possible – always. The average computer will be able to handle anywhere between 500-2000 draw calls per frame, with 60 frames per second. Or so I am told. My hardware is really old and I’m no CPU/GPU expert.


And how was my game doing?

When I was profiling my game, I found that the game was issuing around 900 draw calls. That’s already quite a lot. And it’s way too much for a game that had only 10 or so unique objects, a small level, a player, some packages, and nothing else.

Besides draw calls, I can see the vertex count (how many vertices it had to draw at a certain frame), and material/shader/surface changes. The first one isn’t that big of a deal: yes, drawing more vertices is more work, but it’s very optimized in modern processors. The second one is a big deal: for each change, a new draw call needs to be issued.

For example, say you have modeled a character. This character has five different materials applied to different parts of the object: one for skin color, one for his hair, one for his clothes, etc. For each of these materials, the rendered needs to change the material and change the surface, which results in many extra draw calls.

You guessed it: in my test level, these numbers were higher than I would like. I was drawing nearly a MILLION vertices. There were hundreds of surface changes. How? How?! The level is so simple, and in Blender my models don’t have that many vertices!

Well, as it turns out, there’s a LOT you can do to optimize 3D models, without actually changing the models. Read on, read on!


Optimizing Draw Calls: Meshes

First of all: for each new mesh, the renderer issues a new draw call. We want less draw calls, so combine meshes into one whenever possible.

Let’s say you want to put a house in the game. You might model the walls separately from the roofs, and the windows, and a chimney, and a door, etc. Eventually, your house might be a mix of 10 different meshes. This means that the renderer needs at least 10 draw calls to display the house, and probably much more because every mesh has its own textures.

That’s the wrong way to do it. The right way? Simply combine all these meshes into a single mesh.

In Blender specifically, you can select the meshes in Object mode, and press CTRL+J to Join. I recommend joining meshes only at the END of the whole process, otherwise you can’t modify the individual components anymore, and precise selection becomes a nightmare.)

Once you've joined the meshes, you can simply select all (by pressing A), then press U to UV unwrap the whole thing into a single texture.


Optimizing Draw Calls: Lighting & Shadows

Secondly, I found out that lights add a considerable number of draw calls. I thought this was already optimized, but it turns out that feature will only be available in the next version of Godot. (Where you should be able to batch 8 directional lights into a single call.)

I ran some tests on the model of the player “Headquarters”. (Below is a reminder of how that thing looked. Yes, the texturing is very bad, I drew it quickly and roughly, because I just wanted to test performance.)

Without Light:

  • Vertices = 4692
  • Mat Changes = 8
  • Shader Changes = 4
  • Surface Changes = 14
  • Draw Calls = 14

With a single directional light + default shadows:

  • Vertices = 14076
  • Mat Changes = 12
  • Shader Changes = 4
  • Surface Changes = 42
  • Draw Calls = 42

That’s quite a difference.

My current scene has three directional lights. Maybe … maybe I should just use one if I want to support lower end systems :p

The lesson here is: use as few lights and shadows as possible. Also, if possible, reduce shadow quality.


Optimizing Draw Calls: Material/Surface Changes

The lesson here is quite straightforward.

  • Big objects, even if they have multiple components, should only have a single material.
  • Small objects can even be batched to share the same material. (They can share a so-called “texture atlas”)

How do you do this? In any 3D modeling software, you should be able to UV Unwrap your models onto the same texture space. Even if your model has many different meshes or parts, you should be able to get their UV layout within the same texture.

If you do that, they can all get the exact same material.

Then you can just draw on this UV texture, to give every part the right color (and/or texture).

By doing this, you optimize two things:

  • The material doesn’t need to be changed often, so it reduces draw calls.
  • The surface also doesn’t need to be changed, as the whole object is just a single surface, which too reduces draw calls.

At the end of this article, I’ll share the results from optimizing my “Headquarters” using all the tips I explain here, and you’ll see how powerful this is.

NOTE: I stated, a few devlogs ago, that I baked Ambient Occlusion into my models. I still do this, and with the workflow described above, it has zero impact on the draw calls, which is really nice. All models that share the same material, also share the same AO texture, and I can just apply it everywhere to make things look nicer.

NOTE 2: UV unwrapping was invented by the devil. Seriously, getting a good UV map that allows you to paint the model accurately and efficiently, is the hardest part of optimization. I’m still learning about creating proper UV seams, unwrapping the model before I apply bevels or other effects that mess with the geometry, etc.


Optimizing Draw Calls: Vertex Count

Last but not least, reduce/simplify your models as much as possible. Any faces that will never be seen: delete them. Any overlapping vertices, any detail that isn’t necessary: delete it.

I did the same clean-up for my “Headquarters”, and I was able to remove about 30%-40% of the overall vertex/edge/face count. For example, the boxes on top used to have a backside and a floor. Well, you’ll never see those, so they are gone. The house itself also lost its floor, and underneath the yawning is just a gap, because the yawning occludes it anyway.


Optimizing Draw Calls: Bonus

I really did some rigorous testing: turning all unique elements in my scene on and off, profiling it, writing down the results.

This also gave me some positive information about the engine:

  • Particles are really efficient. I had four instances of the same smoke particle effect, which covered large parts of the screen and had many moving elements, and it only added a single draw call and a handful of other things.
  • Gridmap is more efficient than I thought. Disabling the GridMap greatly reduced the vertex count (which was to be expected, with that many cubes in a terrain), but not much else.
  • Instanced scenes are a little more efficient than unique models, but not by much. If I want to use certain models often throughout the levels, I need to use the MultiMeshInstance, and cannot solely rely on basic instancing.
  • Bonus tip: objects that are small, or will only be seen from further away, can do with a lower resolution texture. Bigger objects will have textures of 2048x2048, smaller ones can be 1024x1024, and even smaller ones could be 512x512.


The results!

Let’s take another look at the profiling results from the “old” Headquarters building.

I placed it in a scene, with a standard directional light (with shadows), and a camera to view the thing in its entirety. These were the results:

Old model  

  • Vertices = 14076
  • Mat Changes = 12
  • Shader Changes = 4
  • Surface Changes = 42
  • Draw Calls = 42

Now, let’s look at the exact same scene, but with the new model, using all tips mentioned above. (The model could be improved even further, I just did a quick clean-up and texture combining exercise.)

New model:

  • Vertices = 9594
  • Mat Changes = 6
  • Shader Changes = 2
  • Surface Changes = 6
  • Draw Calls = 6

Yep. Model looks identical, draw calls went down about 85%. And as a bonus, all other properties were greatly reduced as well. That’s what I discovered this week, and I am very happy I did :p

Sure, I need to completely re-do every model I’ve done so far, but it will be worth it. If I can get roughly the same improvement on each model, my draw calls (for level 1) will drop from 900 to around 150, which is something I can surely work with.


I hope someone reads this and is able to use the right workflow from the start of the project!

One saying that I read somewhere which resonated with me, was: “if you’re asking the GPU to draw something, make it worthwhile” In other words: combine as much as possible into a single call. That helps me remember all of this.

If you’re not interested in game development, I still hope the article was interesting, and you understand why I had to do this and why this means a lot for the game.

Within the next few devlogs, I will probably have updated all the models, and I can report on the numbers again. Let’s all cross our fingers and hope for the best!


That was it for this intermezzo devlog! Next time, I hope I can finally start talking about the cool gadgets in the game: trampolines, catapults, huge rotating construction cranes, and more fun stuff.

Get Package Party

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.