DrawCall性能优化

原文地址 https://thegamedev.guru/unity-performance/draw-call-optimization/#wait-whats-a-draw-call

什么是DrawCall

简单说,DrawCall就是CPU请求GPU画一些东西。

Simply put, a draw call is your CPU asking your GPU to draw something.

The issue is, preparing unity draw calls steals a huge portion of your CPU time and energy.

Unity has to convert your scene content into a format your GPU understands. And a very expensive part of this process is to set the correct render parameters, such as textures, shaders, meshes, etc…

Setting rendering parameters manually is tedious. That’s why game developers introduced the concept of materials.

A material is a data structure with information about how to draw an object. It contains a shader with all its parameters, plus information about how to set the GPU render state.

And every material you add to your scene increases the complexity of your rendering pipeline. Each material adds at least one SetPass (this sets rendering parameters). And you really want to minimize those if you want your game to perform well.

Does this mean we cannot draw too many objects at once?

Not necessarily.

Game developers use batching to group the rendering of similar objects into the same draw call. That way, the CPU pays the price of a single draw call to render multiple objects.

With batching, we ask the gpu once to draw three chairs here, there and behind instead of asking three different times.

In batching, it all comes down to using the same material across different objects. Really. If you get this done, you achieved the most complicated step.

With the help of the Unity Frame Debugger(Window->Analysis->Frame Debugger), you can see below the sequence for 4 draw calls: 3 for the furniture and 1 for the floor.

Unity Draw Calls: No Batching

That was expensive. But batching will help us ​reduce these draw calls. This, in turn, will reduce the CPU load of your players. Having more resources lets you add more gameplay features or just keep it that way to reduce the energy consumption of the device.

Batches vs SetPasses

There’s a difference between the Batches and SetPasses metrics you see in the profiler and stats window.

But this difference has a huge impact.

Batches are what we usually describe as draw calls. Those are plain draw commands, e.g. draw this object here and then this other one there. This is mostly about drawing an object with the current global render state. Same shader, similar parameters.

SetPasses, however, describe a more expensive operation: material changes. Changing a material is expensive because we have to set a new render state. This includes shader parameters and pipeline settings, such as alpha blending, Z testing, Z writing, etc…
这里为什么SetPasses开销更大,猜测是因为GPU有很多核心,在切换渲染状态(SetPass)的时候,这些GPU cores都要等待切换完成,相当于没干活。

Let’s consider we have 3 chairs sharing the same mesh.



We’ll now explore three scenarios with different batching and material setup. Each scenario will result in different batches and SetPasses. Check the following table.

Scenario ”You’re Screwed”Scenario ”You’re Still Screwed”Scenario”Getting Better”Scenario ”Kicking Ass”
Batching SettingDisabledEnabledDisabledEnabled
Material SetupIndividual (x3)Individual (x3)Shared (x1)Shared (x1)
Draw Events1. SetPass (chair 1)

2. Draw call (chair 1)

3. SetPass (chair 2)

4. Draw call (chair 2)

5. SetPass (chair 3)

6. Draw call (chair 3)

1. SetPass (chair 1)

2. Draw call (chair 1)

3. SetPass (chair 2)

4. Draw call (chair 2)

5. SetPass (chair 3)

6. Draw call (chair 3)

1. SetPass (chair mat)

2. Draw call (chair 1)

3. Draw call (chair 2)

4. Draw call (chair 3)

1. SetPass (chair mat)

2. Draw call(chair 1+2+3)

SetPasses3311
Batches (D.C.)3331
PerformanceWorstWorstGoodBest

The first and second scenarios are similar: different materials skyrocket our SetPass count. And those have the worst performance hit in the render thread. Batching is not possible, as batching requires using identical materials.

However, we see a hint of light with the third scenario. Sharing materials makes all the difference. Having a unique material reduces the SetPass count to 1, which gives you an incredible performance boost. Sure, we still have three draw calls, but those are very cheap.

Finally, if you really want to kick ass, then the fourth scenario is for you. Here, we enable batching. And batching loves unique materials. Enabling batching reduces the Draw Call count to 1. Here we have the perfect output: ❤️ 1 SetPass, 1 Batch ❤️️

Counting Unity Draw Calls

Before we dig into fighting draw calls, we first need the proper tools to measure them. There are many tools available for this, such as RenderDoc , but we will stick to the simplest: the stats window and the Unity Frame Debugger.

You can access the stats window any time by clicking on the Stats button on the top-right corner of the game view. This panel shows you metrics for the current game view. Expect these numbers to evolve if your screen contents change (which should if you’re serious about game development​).

Fight the Battle: Batching Unity Draw Calls

Instead of drawing one object 10 times, we draw 10 objects once.

That’s the power of batching.

The main requirement to batch draw calls is to get the objects to use the same drawing properties (material). When that happens, Unity can then merge the different meshes into a single chunk that uses the common material.

As we said, most assets will use different materials by default. But worry no further, we’ll see several ways to merge materials into a single one.

Below there’s a flowchart diagram that summarizes the options you have for batching in Unity.

Your entry point is to find out if the objects you want to batch share the same material.

Sharing materials is a precondition for batching to work. Different materials have different drawing settings that change the global GPU render state.

If these objects don’t use the exact same material but they’re similar enough, then you must merge them into a single one. This usually involves creating shared texture atlases and updating the individual objects’ UV coordinates to point at the new correct locations. There are tools to help you out here.

Requirement: Merging Unity Materials

The first requirement to merge materials is this:
The materials for the objects you want to batch must use the same shader

Changing the current shader is one of the most expensive operations you can do. This slows down rendering significantly.

If you can merge two shaders that look alike into the same one, you’ll get huge wins in performance.

So the first step is to remove shaders from your project, whenever you can. Chances are, you can get many original materials to look similarly under a common shader.

Once your target objects use the same shader, the next step is to merge their materials. That is probably complicated, as they probably had different material parameters such as:

  • Textures: each material often has one or more textures that are not shared with other materials. One way to use the same texture across different materials is by creating bigger textures that contain all individual textures. Those textures are called atlases.
  • Decimal values: such as metallic, specular and other parameters. To merge those, you can either find a common average value that suits them all or create a texture atlas containing that value in a specific channel. You can re-purpose the 3 or 4 texture channels for different parameters, e.g. storing the metallic value in the red channel.

Now, if you have several objects with the same material but they must have different parameters, you can give MaterialPropertyBlock a shot. Instead of creating individual material instances, you can create a MaterialPropertyBlock for each renderer that needs custom parameters. You can then set your individual parameters in each of these blocks. This will not reduce your draw calls but it will make rendering much cheaper, as you’re explicitly telling Unity what’s different about each object.

Technique 1: Unity Static Batching

Static batching is enabled by default (and I suggest you to keep it that way).

Unity applies this technique automatically to all static objects in the scene that share a material. If we start with the following draw calls:

  • Draw static chair 1
  • Draw static chair 2
  • Draw static table

Then static batching creates a single draw call out of them:

  • Draw the static dining furniture (containing 2 chairs and a table).

More precisely, Unity will look for objects whose batching static flag is enabled. Then, Unity will attempt to merge those that share a material.

Unity static batching works by creating a huge mesh containing the individual meshes. But Unity doesn’t discard the individual meshes. Instead, Unity keeps the original meshes intact so we are still able to render them individually. We need this for frustum culling to work. This way, we can draw only the objects that lie within the visible field of view and discard those who aren’t.

The main limit to static batching is the amount of vertices and indices each batch can have. This is often 64k for each, but check the updated requirements here.

The downside to static batching is an increased memory usage. If you have 100 stones and each stone model takes 1MB, then you can expect memory usage to be around 100MB+. This happens because the huge batched mesh contains all stones together as a single mesh.

Technique 2: Unity GPU Instancing

GPU instancing is one of my favorite batching techniques because it works with non-static objects.

If we have these draw calls:

  • Draw dynamic stone 1
  • Draw dynamic stone 100

Then with GPU instancing we convert them to a single draw call:

  • Draw 100 dynamic stones here and there and there…

GPU instancing 底层使用了 DirectX的 Mesh Instancing,相对于静态合批,它还起到了节省内存的作用,因为只需要发送一个mesh data即可。缺点是只能作用于同一个mesh。

GPU instancing lets you draw the same mesh several times very efficiently. Unity does it by passing a list of transforms​. After all, each stone had its own position, rotation and scale.

This is a powerful technique because it does not skyrocket the memory usage and it doesn’t require the objects to be static, compared to static batching.

Technique 3: Unity Dynamic Batching

If you cannot meet the requirements of static batching and GPU instancing, you still have hope.

You can still batch dynamic objects that use different meshes with dynamic batching.

However, bear in mind that Unity dynamic batching is heavily limited. You can apply it only to meshes that have less than 300 vertices and 900 vertex attributes (colors, UVs, etc). The material should use a single-pass shader as well. Make sure to check the full list of requirements here.

The reason for this limit is the CPU performance cost of creating these batches in run-time. Above 300 vertices it becomes hard to justify the batching CPU cost compared to issuing the draw calls individually.

Technique 4: Unity Run-Time Batching API

For all special cases where you want to have finer control over batching, you can just do it manually.

Don’t worry, you won’t have to deal with vertices yourself.

Let’s say you’re driving a car. In the interior you see several elements such as the seats, the handles, the windshield and all the coffee mugs you accumulated over time. You customize these elements before the race starts.

Once you made your choice and start the race, these elements become kind of static within your car. Let me explain this…

The car itself is dynamic. After all, you’re driving it .

But all its non-moving inner parts? They can be considered static relative to the car object. The windshield will always remain at the same place within the car.

Yet, Unity considers all these pieces to be dynamic. That’s why static batching won’t work in this situation.

Still, we can profit from the static batching APIs to create these batches manually.

The easy way is to use StaticBatchingUtility.Combine. This function takes a root game object and will iterate over all its children and merge their geometry into a big single chunk. One requirement that is easy to forget is that the import settings of all sub-meshes to batch must allow CPU read/write.

The second way is by using Mesh.CombineMeshes. This function indirectly takes a list of meshes and creates a combined mesh. You can then assign that mesh to a mesh filter and you’re good to go.

posted @ 2024-02-18 13:35  dewxin  阅读(52)  评论(0编辑  收藏  举报