Unity VR Optimization Draw Calls

Let’s talk about optimization, this time let’s go over draw calls! 

What is a Draw Call?

Whenever we need to an object to appear on the screen, we need to make a call to the GPU to draw out this object to the screen. This call to the GPU is what the Draw Call is. As we can imagine, having hundreds of objects in a scene would also require hundreds of draw calls as well. Too many draw calls and we’ll run into performance issues. 

Keep in mind, it isn’t as simple as one game object = one draw call. A draw call will be made for every material on an object. That means if we have an object with 4 different materials to help give it more depth, then it will have 4 different draw calls when rendering it to the screen.

One thing to point out is that Unity has named it’s draw calls differently and instead uses the term Batching. This is because Unity tries to optimize performance by grouping draw calls together so they can be drawn together instead of individually. 

Now that we know some basic terminology, let’s discuss how to optimize draw calls.  

What's a Good Metric for Drawcalls?

If we’re going to optimize something, it always helps to have a target. Again, we look towards Meta’s metrics as they have the highest market saturation with the Meta/Oculus Quest 2. According to Meta, there is actually a range of draw calls that we should aim for based upon what is currently happening in our scenes. 

According to their website, found here, we should aim for about 80-600 draw calls depending on if our scene is performing busy or light simulations. So what does is mean to have busy or light simulations? Well luckily, Meta answers that for us too.

So there we have it! Multiplayer games with lots of actions fall under the busy simulation category, single-player fps games under medium and finally escape room and puzzle games are light simulations.

With that in mind, most developers should aim for between 200-600 draw calls unless they are creating a very busy multiplayer experience.

Visulization with Frame Debugger

it always helps to be able to see the problem and luckily, with the frame debugger, we can do that! 

  • Go to Window -> Analyisis -> Frame Debugger 
This will bring up the Frame Debugger window. If we click the Enable button in the top left corner, we can actually walk through how the frame is being drawn step-by-step and watch as it constructs the seen. Even better, we can press the play mode button and then click enable whenever we want to analyze a specific frame. This is incredibly helpful if there is a certain section in our scenes that seems laggy. We can play our camera there during play mode and walk through all the draw calls to see if that specific area causes a ton of draw calls. 

So now that we can visualize the problem, let’s start looking at ways to reduce draw calls.

Shadow Ban

Depending on how much we have going on in a scene, we may want to consider just eliminating shadows. If we look at our frame debugger, we can see that shadows actually account for a decent amount of draw calls. Depending on our application, we might be able to get away with turning off shadows without ruining too much of the user experience.

  •  Go to Edit -> Project Settings -> Quality
    • Locate Shadows and select Disable Shadows from the drop menu
 

With that small change, we can see that we went from 57 frames down to 20. That is a difference of almost 65% less frames! Although it isn’t ideal to lose shadows in our game, if they aren’t too important to the experience, it can be a massive improvement.

We can also simulate shadows by baking light ahead of time, but I’ll save that for when we go over light optimization in future videos.

Let’s move on to static batching!

Static Batching

One of the first things we can implement is a optimization method called Static Batching. Like the name implies, this method focuses on batching objects in the seem that are not intended to move. The Unity game engine can group these stationary objects together and render them by making it into a single mesh. Since they’re all grouped up into a single mesh, it will then only need a single draw call in order to produce it.

We’ll start by making sure static batching is turned on.

  •  Go to Edit -> Project Settings -> Player
    • Other Settings
      • Static Batching = True
Then we need to set objects in our scene that won’t move as static so they can be static batched.
 
  • Select an object in your scene that will be stationary and that you wish to mark as static
    • In the top right of the Inspector
      • Static = True
Now to see the real benefits of static batching, make sure to press play mode to see how many draw calls are being made. Turn off play mode, set objects to static and test again.

 

 With my small test scene, I ended up losing 50 draw calls due to static batching!

So what about batching objects that are moving?

Dynamic Batching

Dynamic batching will allow us to batch draw calls together if they meet a certain criteria. Those criteria are as follows.

  • 300 vertices for the object being rendered.
  • Have the same material as other objects.
  • Have the exact same scale as other objects.
Now these are some interesting requirements, but luckily for us, we won’t need to identify any of these items in the scene. As long as we set up our project to perform dynamic batching, it will identify objects like this for us!
  •  Go to Edit -> Project Settings -> Player
    • Other Settings
      • Dynamic Batching = True

There are drawbacks to using dynamic batching. Since Unity performs the batching for us, it can be unpredictable. There is a way to manually dynamically batch objects, but it’s more pain than it’s worth. The other drawback comes from the tradeoff with CPU usage. Instead of putting the load of the draw call on the GPU, it instead puts the workload on the CPU.
 
Ultimately, this an option to use sparingly and on a case by case basis, but it is a useful tool to keep in mind if we’re struggling with draw calls.

GPU Instancing

GPU Instancing will allow us to perform draw calls on objects that are in motion, but our objects that are moving will need to have a few things in common before they can be considered for GPU Instancing.

  • Objects must have the same Mesh
  • Objects must have the same Material
If we have both these qualities, then we’ll be able to perform GPU Instancing. To implement GPU Instancing, just do the following.
  • Select an object that has multiple appearances in the scene (Same mesh used multiple times)
  • Make sure all of these objects also share the same mesh
  • Expand the mesh that is being shared by all objects
    •  GPU Instancing = True

Now if we start up the scene, we can see that our batch calls have been reduced! 

The way GPU instancing works is by gathering up a list of vertices and instance transforms. With all the positions, rotations and scales of all the copies of the same object, it then sends it in a single draw call to all be drawn. 

Conclusion

So there we are! A few new tools in the toolbelt to help us reduce our draw calls and make our games even more efficient than before.  

My shrimp brain is tired! I’ll see you in the next one!