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
| Term | Meaning |
|---|---|
| Draw call | One CPU → GPU command: "draw this mesh with these settings" |
| SetPass call | Changing the shader/material state before drawing. Usually the most expensive part. |
| Batch | A group of objects drawn with one command |
| CPU-bound vs GPU-bound | Whether the CPU (preparing work) or the GPU (drawing pixels) is the bottleneck. Batching only helps when the CPU is the problem. |
Measuring first
- Game view > Stats: shows Batches and SetPass calls for the current frame.
- Window > Analysis > Frame Debugger: step through every draw call in a frame and see why two objects weren't batched together ("Objects have different materials", etc.).
- Profiler > Rendering / CPU: how long the render thread spends preparing draws.
The options
| Technique | What it does | Best for | How 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 path | On by default in the URP asset; use compatible shaders (URP Lit, Shader Graph) |
| Static batching | Combines static meshes sharing a material into bigger meshes | Level geometry that never moves | Tick Static (Batching Static) on the GameObject |
| GPU instancing | Draws many copies of the same mesh + same material in one call, each with its own position/rotation/scale | Lots of identical things: grass, trees, rocks, bullets, crowds | Tick Enable GPU Instancing on the material, or draw from code |
| GPU Resident Drawer (Unity 6, URP/HDRP) | Automatically instances compatible GameObjects using the BatchRendererGroup API | Big scenes with many repeated MeshRenderers | URP asset: GPU Resident Drawer = Instanced Drawing. Requires the Forward+ rendering path and Project Settings > Graphics > BatchRendererGroup Variants = Keep All |
| Dynamic batching | Combines tiny moving meshes on the CPU each frame | Rarely worth it on modern hardware | Built-in / URP asset setting |
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
- Every frame,
RenderMeshInstancedsends the GPU the mesh once, the material once, and a buffer of up to 1023 matrices. - The GPU draws the mesh once per matrix. The shader reads a per-instance index to find which matrix to use.
- 5,000 asteroids become about 5 draw calls instead of 5,000.
- 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
- Share materials. Reading
renderer.materialcreates a copy per object, which breaks batching and instancing. UsesharedMaterialor property blocks. - Texture atlases: pack several textures into one so objects can share a material.
- LOD Groups: swap to simpler meshes at a distance.
- Occlusion culling (Window > Rendering > Occlusion Culling): skip objects hidden behind walls. Good for indoor levels.
- Shadows are drawn in extra passes. Reduce shadow distance and turn off Cast Shadows on small objects.
- Overdraw: many layers of transparent particles or UI covering the screen are expensive on the GPU, especially on mobile. Check with the Scene view's Overdraw mode (Built-in) or the URP Rendering Debugger.
Common Mistakes
- Instancing with different materials. Instancing only groups the same mesh and same material. Two materials with the same settings are still different materials.
- Expecting it to help when GPU-bound. Fewer draw calls saves CPU time. If the GPU is struggling with pixels (resolution, post-processing, overdraw), batching won't fix it.
- Skinned meshes. Animated characters (
SkinnedMeshRenderer) aren't instanced by these features. Crowds need special techniques (e.g. baked vertex animation). - Forgetting worldBounds. Too small, and instances pop out of view when the bounds leave the camera.
- Calling RenderMeshInstanced only once. It draws for one frame; call it every frame.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official Unity documentation.