The Scriptable Render Pipeline (SRP) Batcher is a very powerful feature in optimizing game performance, especially in reducing Draw Calls, which are a bottleneck on the CPU side. If the materials use the same shader, even if the parameters are different, they can be drawn all at once by fast switching of the GPU constant buffer, which dramatically reduces the drawing load. However, have you ever faced the problem that even though you are using a shader that supports SRP Batcher for all objects, the moment you place a large number of objects with multiple materials (multi-material meshes), batching becomes invalid and draw calls explode? In this article, we will explain in detail the technical reasons why the inconsistency between multi-material and SRP Batcher occurs, an analogy that is easy for beginners to understand, and the fundamental solution to minimize draw calls.

1. Understanding with an analogy: ``Ultra-fast batch check-in at a hotel'' and ``three separate application forms''

Let's consider the mechanism of this ``batching collapse due to multi-material'' using ``check-in procedure at the front desk of a hotel'' as an analogy.

A group of 100 people (objects to be drawn) from a certain tour company lined up at the front desk of the hotel. Everyone has the same "tour identification card (SRP Batcher compatible shader)".

If the procedure was simple, where each tour guest received "one room card (one material)," the front desk clerk (GPU) could just say, "Yes, we're all in the same group!" and quickly scan the list (constant buffer), and then quickly and quickly complete the check-in (SRP Batcher) by collecting keys for everyone.

However, let's say that each of these tour guests was carrying three completely different configuration forms at the same time: ``I have a blue application form for the bedroom, a white application form for the bathroom, and a woodgrain application form (multi-material) for the balcony.'' Every time a front desk clerk handles one customer, he or she has to interrupt and change his or her work at the counter many times, saying, ``Well, take care of the blue room... (key issue: 1 draw call). Next, switch to the white room... (key issue: 2 draw calls). Finally, proceed with the wood grain room... (key issue: 3 draw calls).'' No matter how many IDs are the same, each person has to go through the process three times, so if there are 100 people in line, there will be a huge traffic jam at the front desk (an explosion of draw calls). This is the reason why the high speed lane (SRP Batcher) is broken due to multi-materials.

2. Root cause: Separation of submesh and render state

Technically speaking, when you attach multiple materials (e.g. Element 0, Element 1, Element 2) to Unity's MeshRenderer, the internal mesh data becomes a "Submesh" structure divided by material. Due to the specifications of graphics APIs (DirectX, Vulkan, Metal, etc.), when drawing areas with different materials (texture binding, drawing settings), you must issue separate drawing API calls such as DrawIndexedInstanced.

SRP Batcher is a mechanism that ``even if the material parameters are different, if the shader rendering pipeline (Render State) is the same, it rewrites only the address of the constant buffer (CBUFFER) while maintaining the binding on the GPU side and draws continuously at ultra-high speed.''

However, if multiple submeshes exist in one game object (MeshRenderer) and different materials are assigned, Unity's internal system gives priority to the drawing order and blending order for each object. As a result, drawing will run in the order Object A submesh 0 ➔ Object A submesh 1 ➔ Object B submesh 0 ➔ Object B submesh 1 , or the batch will be divided into pieces at the material switching boundary. As a result, the premise of SRP Batcher's ``continuous loop playback of data arranged in constant memory'' is broken, and the effect of batch processing is completely canceled out.

3. Three solution approaches that should be adopted in practice

We will introduce specific approaches to avoid this problem and maximize the batch performance of SRP Batcher to 100%.

Solution A: Texture atlasing and material centralization in DCC tools (recommended)

The most essential and recommended approach is to combine multiple materials into one unified material during the 3D model creation stage (Blender, Maya, etc.).

  1. Creating a texture atlas: Combine (pack) the individual texture images used for the roof, walls, and doors into one large image (e.g. 2048x2048) in a puzzle-like manner.
  2. Relocating UV coordinates: Unfolds the 3D model's UV map and rearranges it to fit in the corresponding position of the atlas image.
  3. Consolidate the number of materials: Define only one material on the DCC tool and set the atlas texture to the unified material.
  4. Export to Unity: Export the mesh and place it in Unity. This reduces the number of material slots to one, and even if you copy hundreds of objects, they will all be drawn in one go (minimum draw calls) using SRP Batcher.

Solution B: Automatic atlasing using Unity assets (no code required)

If you have a large number of existing assets and find it difficult to return to Blender etc., use tools such as "Mesh Baker" and "Easy Mesh Combiner" from the Unity Asset Store. These automatically atlas the material (texture) of a group of objects selected in the Unity editor and automatically convert it into a single material mesh that integrates submeshes.

Solution C: Combining meshes at runtime using C# scripts (runtime optimization)

If you want to reduce draw calls by combining multiple objects that are dynamically placed during stage generation or runtime into one, use Mesh.CombineMeshes from C# to physically combine meshes and submeshes into one material.

The following is an example script implementation that combines atlased objects that share the same material into one large mesh at runtime.

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class RuntimeMeshCombiner : MonoBehaviour
{
    void Start()
    {
        // Get all MeshFilters from child objects
 MeshFilter[] meshFilters = GetComponentsInChildren();
 CombineInstance[] combine = new CombineInstance[meshFilters.Length - 1]; // exclude itself

 int combineIndex = 0;
 MeshRenderer parentRenderer = GetComponent();
 Material sharedMaterial = null;

 for (int i = 0; i < meshFilters.Length; i++)
 {
 if (meshFilters[i].gameObject == gameObject) continue; // Skip itself

 // Use the material of the first child object found as the joining material
 if (sharedMaterial == null)
 {
 MeshRenderer childRenderer = meshFilters[i].GetComponent();
 if (childRenderer != null) sharedMaterial = childRenderer.sharedMaterial;
 }

 combine[combineIndex].mesh = meshFilters[i].sharedMesh;
 // Transform and apply the child object's world matrix to this parent object's local coordinate space
 combine[combineIndex].transform = transform.worldToLocalMatrix * meshFilters[i].transform.localToWorldMatrix;
 
 // Hide the combined mesh renderer and exclude it from drawing
 MeshRenderer r = meshFilters[i].GetComponent();
 if (r != null) r.enabled = false;

 combineIndex++;
 }

 // Create a new mesh and execute the combination
 Mesh combinedMesh = new Mesh();
 combinedMesh.name = "Combined_Static_Mesh";
 
 // 1st argument: combined instance array, 2nd argument: merge submeshes into one by setting mergeSubMeshes to true! 
 // *This will draw all meshes in one draw call with one atlased material
 combinedMesh.CombineMeshes(combine, true, true);

 // Assigned to parent MeshFilter and Renderer
 GetComponent().mesh = combinedMesh;
 parentRenderer.sharedMaterial = sharedMaterial;

 Debug.Log($"Completed combining {combineIndex} meshes into one submesh.");
 }
}

4. Practical Checklist for SRP Batcher Optimization

In order to maintain the drawing performance of the entire project, please set the following check items in your asset production workflow.

  • Principle for the number of material slots: Objects that are placed frequently, such as characters and background assets, should be specified so that the number of material slots is limited to ``1''.
  • Using Frame Debugger: Open Window > Analysis > Frame Debugger, check "Why this draw call could not be batched with the previous one" when Draw Call is selected, and constantly monitor for batch disconnections due to multi-material or shader mismatch.
  • Static settings for static objects: For assets that do not move, check Static in the inspector so that Unity's static batching function automatically performs internal mesh joining.