GPU Instancing & Batching

Every time the CPU asks the GPU to draw something, there's overhead. With thousands of objects (trees, grass, rocks, crowds), that overhead, not the drawing itself, often limits your frame rate. Batching and GPU instancing reduce how many requests the CPU has to make.

Shipping 1,000 identical chairs. You could send 1,000 trucks, each with one chair and its own paperwork (1,000 draw calls). Or you can send one truck with one set of paperwork that says "1,000 of this chair, here's the list of addresses" (one instanced draw call). The chairs are the same work to unload, but the paperwork is where the time was going.

Key terms

TermMeaning
Draw callOne CPU → GPU command: "draw this mesh with these settings"
SetPass callChanging the shader/material state before drawing. Usually the most expensive part.
BatchA group of objects drawn with one command
CPU-bound vs GPU-boundWhether the CPU (preparing work) or the GPU (drawing pixels) is the bottleneck. Batching only helps when the CPU is the problem.
Without instancingCPUGPUWith GPU instancingCPUGPUdraw tree #1one draw call per tree1,000 trees = 1,000 draw calls1 instanced draw callmesh + material +1,000 matricesthe GPU loops over the matrices
The drawing work on the GPU is similar either way. What instancing removes is the CPU overhead of preparing and sending a separate command for every copy.

Measuring first

The options

TechniqueWhat it doesBest forHow to enable
SRP Batcher (URP/HDRP)Makes switching between objects with the same shader very cheap, even with different materials. Doesn't reduce draw calls, but reduces their cost.Most scenes in URP/HDRP: the default pathOn by default in the URP asset; use compatible shaders (URP Lit, Shader Graph)
Static batchingCombines static meshes sharing a material into bigger meshesLevel geometry that never movesTick Static (Batching Static) on the GameObject
GPU instancingDraws many copies of the same mesh + same material in one call, each with its own position/rotation/scaleLots of identical things: grass, trees, rocks, bullets, crowdsTick Enable GPU Instancing on the material, or draw from code
GPU Resident Drawer (Unity 6, URP/HDRP)Automatically instances compatible GameObjects using the BatchRendererGroup APIBig scenes with many repeated MeshRenderersURP asset: GPU Resident Drawer = Instanced Drawing. Requires the Forward+ rendering path and Project Settings > Graphics > BatchRendererGroup Variants = Keep All
Dynamic batchingCombines tiny moving meshes on the CPU each frameRarely worth it on modern hardwareBuilt-in / URP asset setting
SRP Batcher vs. instancing on GameObjectsIn URP/HDRP, when a GameObject is compatible with the SRP Batcher, the SRP Batcher is used instead of GPU instancing for it. Ticking "Enable GPU Instancing" on the material then has no effect for those objects. Instancing still matters when you draw from code (below), and for shaders or renderers that aren't SRP Batcher compatible.

Instancing from code: thousands of objects without GameObjects

For huge counts (a field of grass, an asteroid belt), skip GameObjects entirely and draw the mesh directly. No Transforms, no components, no Update calls.

using UnityEngine;

public class AsteroidField : MonoBehaviour
{
    [SerializeField] private Mesh mesh;
    [SerializeField] private Material material;   // "Enable GPU Instancing" ticked
    [SerializeField] private int count = 5000;
    [SerializeField] private float radius = 100f;

    private Matrix4x4[] matrices;
    private RenderParams renderParams;
    private const int BatchSize = 1023; // classic per-call limit; stay under it

    void Start()
    {
        matrices = new Matrix4x4[count];
        for (int i = 0; i < count; i++)
        {
            Vector3 pos = Random.insideUnitSphere * radius;
            Quaternion rot = Random.rotation;
            Vector3 scale = Vector3.one * Random.Range(0.5f, 3f);
            matrices[i] = Matrix4x4.TRS(pos, rot, scale); // position + rotation + scale in one
        }

        renderParams = new RenderParams(material)
        {
            // Bounds used for culling: must contain every instance
            worldBounds = new Bounds(transform.position, Vector3.one * radius * 2f)
        };
    }

    void Update()
    {
        // Must be called every frame: this draws for the current frame only
        for (int start = 0; start < count; start += BatchSize)
        {
            int n = Mathf.Min(BatchSize, count - start);
            Graphics.RenderMeshInstanced(renderParams, mesh, 0, matrices, n, start);
        }
    }
}

What happens at runtime

  1. Every frame, RenderMeshInstanced sends the GPU the mesh once, the material once, and a buffer of up to 1023 matrices.
  2. The GPU draws the mesh once per matrix. The shader reads a per-instance index to find which matrix to use.
  3. 5,000 asteroids become about 5 draw calls instead of 5,000.
  4. These instances have no colliders and aren't GameObjects. For interaction, keep your own data (positions) and do the math yourself, or use a few real GameObjects near the player.

Per-instance colors

MaterialPropertyBlock lets renderers share one material but override properties such as color, without creating material copies.

private static readonly int ColorId = Shader.PropertyToID("_BaseColor"); // "_Color" in Built-in shaders
private MaterialPropertyBlock block;

void Awake()
{
    block = new MaterialPropertyBlock();
    GetComponent<Renderer>().GetPropertyBlock(block);
    block.SetColor(ColorId, Random.ColorHSV());
    GetComponent<Renderer>().SetPropertyBlock(block);
}

With GPU instancing, a shader must declare the property as per-instance for different values to stay in one batch (Shader Graph and URP's built-in shaders handle the common cases). Note: in URP, a renderer with a MaterialPropertyBlock is no longer SRP Batcher compatible.

Other big rendering wins

Common Mistakes

See also