Optimizing graphics performance
Modeling Characters for Optimal Performance

Draw Call Batching

To draw an object on the screen, the engine has to issue a draw call to the graphics API (e.g. OpenGL or Direct3D). Draw calls are often expensive, with the graphics API doing significant work for every draw call, causing performance overhead on the CPU side. This is mostly caused by the state changes done between the draw calls (e.g. switching to a different material), which causes expensive validation and translation steps in the graphics driver.

Unity uses several techniques to address this:

  • Static Batching: combine static (i.e. not moving) objects into big meshes, and render them in a faster way.
  • Dynamic Batching: for small enough meshes, transform their vertices on the CPU, group many similar ones together, and draw in one go.

Built-in batching has several benefits compared to manually merging objects together (most notably, the objects can still be culled individually). But it also has some downsides too (static batching incurs memory and storage overhead; and dynamic batching incurs some CPU overhead).

Material Setup For Batching

Only objects sharing the same material can be batched together. Therefore, if you want to achieve good batching, you need to share as many materials among different objects as possible.

If you have two identical materials which differ only in textures, you can combine those textures into a single big texture - a process often called texture atlasing. Once textures are in the same atlas, you can use a single material instead.

If you need to access shared material properties from the scripts, then it is important to note that modifying Renderer.material will create a copy of the material. Instead, you should use Renderer.sharedMaterial to keep material shared.

While rendering shadow casters, they can often be batched together even if their materials are different. Shadow casters in Unity can use dynamic batching even with different materials, as long as the values in materials needed by the shadow pass are the same. For example, many crates could use materials with different textures on them, but for shadow caster rendering the textures are not relevant – in that case they can be batched together.

Dynamic Batching

Unity can automatically batch moving objects into the same draw call if they share the same material and fulfill other criteria. Dynamic batching is done automatically and does not require any additional effort on your side.

  • Batching dynamic objects has certain overhead per vertex, so batching is applied only to meshes containing less than 900 vertex attributes in total.
    • If your shader is using Vertex Position, Normal and single UV, then you can batch up to 300 verts; whereas if your shader is using Vertex Position, Normal, UV0, UV1 and Tangent, then only 180 verts.
    • Note: attribute count limit might be changed in future.
  • Objects will not be batched if they contain mirroring on the transform, for example object A with +1 scale and object B with –1 scale can not be batched together.
  • Using different material instances - even if they are essentially the same - will make objects not batch together. The exception is shadow caster rendering.
  • Objects with lightmaps have additional renderer parameter: lightmap index and offset/scale into the lightmap. So generally dynamic lightmapped objects should point to exactly the same lightmap location to be batched.
  • Multi-pass shaders will break batching.
    • Almost all unity shaders supports several lights in forward rendering, effectively doing additional pass for them. The draw calls for “additional per-pixel lights” will not be batched.
    • Legacy Deferred (light pre-pass) rendering path has dynamic batching disabled, because it has to draw objects twice.

Since it works by transforming all object vertices into world space on the CPU, it is only a win if that work is smaller than doing a “draw call”. How exactly expensive is a draw call depends on many factors, primarily on the graphics API used. For example, on consoles or modern APIs like Apple Metal the draw call overhead is generally much lower, and often dynamic batching can not be a win at all.

Static Batching

Static batching allows the engine to reduce draw calls for geometry of any size (provided it does not move and shares the same material). It is often more efficient than dynamic batching (it does not transform vertices on the CPU), but it uses more memory.

In order to take advantage of static batching, you need explicitly specify that certain objects are static and will not move, rotate or scale in the game. To do so, you can mark objects as static using the Static checkbox in the Inspector:

Using static batching will require additional memory for storing the combined geometry. If several objects shared the same geometry before static batching, then a copy of geometry will be created for each object, either in the Editor or at runtime. This might not always be a good idea - sometimes you will have to sacrifice rendering performance by avoiding static batching for some objects to keep a smaller memory footprint. For example, marking trees as static in a dense forest level can have serious memory impact.

Internally, static batching works by transforming the static objects into world space and building a big vertex + index buffer for them. Then for visible objects in the same batch, a series of “cheap” draw calls are done, with almost no state changes in between. So technically it does not save “3D API draw calls”, but it saves on state changes done between them (which is the expensive part).

Other Batching Related Tips

Currently, only Mesh Renderers are batched. This means that skinned meshes, cloth, trail renderers and other types of rendering components are not batched.

Semitransparent shaders most often require objects to be rendered in back-to-front order for transparency to work. Unity first orders objects in this order, and then tries to batch them - but because the order must be strictly satisfied, this often means less batching can be achieved than with opaque objects.

Manually combining objects that are close to each other might be a very good alternative to draw call batching. For example, a static cupboard with lots of drawers often makes sense to just combine into a single mesh, either in a 3D modeling application or using Mesh.CombineMeshes.

Optimizing graphics performance
Modeling Characters for Optimal Performance