UNITY & C#
EXPERT DEVELOPER

Implementing GPU Instancing in Unity

By Eduardo Martinelli | May 19, 2025

In this post, I will walk you through the concepts of this technique and how to build a GPU Instancing component that you will use to render thousands of objects per batch. Using Unity Engine 2022.3.15.f1 and the Universal Rendering Pipeline (URP).

What we'll cover:

  • Getting Started
  • URP Implementation
  • Batching Efficiently
  • Benchmarking Results

Rendering Many Objects

Performance bottlenecks often emerge from scale in game development. Different techniques to batch and reduce draw calls are key to the success of countless game titles that render thousands of identical meshes to be simultaneously. Yet, some major game engines don't do it by default. Unity Engine being one of them.

First we should understand that Unity does handle most of the batching itself, most notably with static batching and the SRP batcher. But we can go further as it is not always efficient and somewhat limited. Often individual GameObjects and MeshRenderers create separate draw calls even though they share the same mesh or material data. In large game worlds this can overwhelm the CPU and cause frame rate drops.

GPU instancing is a technique that leverages parallel processing capabilities of modern graphics hardware and gives you control over the batching of mesh renderers. We will build a prototype and gradually approach the core concepts of this implementation in Unity.

The video below shows a direct comparison of GPU Instancing and Non-GPU Instancing impacts on performance. Pay extra attention to the number of draw calls. GPU Instancing make draw calls significantly less often than its counterpart — increasing performance severely.

Understanding the Core Concept

GPU instancing is elegant in its simplicity. Rather than instructing the GPU to draw each individual mesh separately, we provide a single mesh and a collection of transformation matrices that tell the GPU where to place each instance of that mesh. This approach allows the graphics hardware to handle the repetitive work in parallel, dramatically reducing CPU overhead and draw calls.

Think of it like the difference between placing individual bricks on a wall, versus placing a thousand bricks at once using a blueprint. The performance gain is from rendering in bulk through fewer batches.

Prototype: Direct Rendering

Go on, create an empty Unity project and attach the upcoming scripts to objects in your scene.

The first approach we’ll explore is interfacing directly with Unity’s rendering pipeline through the OnRenderObject callback:

using UnityEngine;

public class GPUInstancing : MonoBehaviour
{
    public Mesh mesh; // Mesh rendered
    public Material material; // Material Rendered

    void OnRenderObject()
    {
        if (mesh == null || material == null) return;

        material.SetPass(0);
        Graphics.DrawMeshNow(mesh, transform.localToWorldMatrix);
    }
}

This implementation works by hooking into Unity’s rendering process after the standard rendering pass. OnRenderObject provides a direct interface to the rendering pipeline, allowing us to issue immediate drawing commands through Graphics.DrawMeshNow.

Why Transform.localToWorldMatrix? It gives the full transformation matrix from your GameObject’s local space to world space, including translation (position), rotation, and scale. When passed to Graphics.DrawMeshNow, it ensures that the mesh is rendered in the correct position and orientation in the world.

While this approach successfully renders meshes, it has two significant limitations: it doesn’t properly support Unity’s Universal Render Pipeline (URP) lighting system, and more importantly, it doesn’t actually leverage instancing yet. Each object would still require a separate call to DrawMeshNow, which doesn’t solve our fundamental problem.

A single rendered mesh lacking proper lighting or shadows

URP Compatibility: Adapting the Approach

To address the compatibility issue with Unity’s Universal Render Pipeline, let's revise the implementation to Graphics.DrawMesh using DrawMeshNow instead:

using UnityEngine;

public class GPUInstancing : MonoBehaviour
{
    public Mesh mesh;
    public Material material;

    void Update()
    {
        if (mesh == null || material == null) return;

        // Properly queues the mesh for URP rendering with lighting and shadows
        Graphics.DrawMesh(mesh, transform.localToWorldMatrix, material, 0, Camera.main, 0, null, false, false, false);
    }
}

This modification properly integrates with URP’s rendering pipeline, ensuring that lighting, shadows, and other visual features work correctly. Graphics.DrawMesh() differs from DrawMeshNow() — rather than rendering immediately, it queues the mesh for rendering during the appropriate pass in the pipeline.

A single rendered mesh with proper URP lighting and shadows A single rendered mesh with proper URP lighting and shadows

Now the URP compatibility issue is fixed. But we are still effectively rendering one object at a time, leaving our core performance problem unsolved. Let’s fix that.

True GPU Instancing: Drawing Many Meshes

Our solution comes from Graphics.DrawMeshInstanced() — Unity’s dedicated API for GPU instancing. This function allows us to submit an entire array of transformation matrices at once, saving up on function calls and enabling the GPU to render many instances of the same mesh in a single batch:

using UnityEngine;

/// <summary>
/// Handles GPU instanced rendering of a single mesh with a shared material.
/// Call Setup to initialize the rendering data.
/// </summary>
public class GPUInstancing : MonoBehaviour
{
    private Mesh _mesh;           
    private Material _material;    
    private Matrix4x4[] _matrices; 

    private const int batchSize = 1023; /// Unity's limit for DrawMeshInstanced is 1023 instances per call.

    /// <summary>
    /// Initializes the GPU instancing system with the mesh, material, and transforms.
    /// </summary>
    /// <param name="mesh">The mesh to be drawn using instancing.</param>
    /// <param name="material">The shared material to apply to each instance.</param>
    /// <param name="matrices">An array of transforms (as Matrix4x4) representing instance positions and orientations.</param>
    public void Setup(Mesh mesh, Material material, Matrix4x4[] matrices)
    {
        _mesh = mesh;
        _material = material;
        _matrices = matrices;
    }

    private void Update()
    {
        if (_mesh == null || _material == null || _matrices == null) return;

        // Render in batches of 1023 (Unity's limit per instancing call)
        for (int i = 0; i < _matrices.Length; i += batchSize)
        {
            int count = Mathf.Min(batchSize, _matrices.Length - i);

            // Draw the current batch of instances
            Graphics.DrawMeshInstanced(
                _mesh,
                0,
                _material,
                _matrices,
                count,
                null,
                UnityEngine.Rendering.ShadowCastingMode.On,
                true,
                0,
                null
            );
        }
    }
}

Why do we group a maximum of 1023 mesh per batch? Unity can only process 1023 instances per DrawMeshInstanced call. By batching our instances in groups of 1023, we work around this constraint while still achieving great performance.

The real magic happens in the matrices array, where each Matrix4x4 contains the position, rotation, and scale of each individual instance. This information is passed to the GPU in batches, allowing it to efficiently render thousands of objects with minimal overhead, looking just like any object rendered on the CPU.

We need a way of using this class set’s code a simple test script:

using UnityEngine;

/// <summary>
/// Test class to initialize a GPU instancing system by generating a number of
/// instance transforms and passing them to a <see cref="GPUInstancing"/> component.
/// </summary>
public class GPUInstancingTester : MonoBehaviour
{
    [SerializeField] private Mesh _mesh;                    /// The mesh to render via instancing
    [SerializeField] private Material _material;            /// The material to apply
    [SerializeField] private int _objCount = 100;           /// Number of instances to generate
    [SerializeField] private GPUInstancing _gpuInstancing;  /// Reference to the GPUInstancing handler

    public void Start()
    {
        var matrices = new Matrix4x4[_objCount];

        /// Generate random positions and create transform matrices
        for (int i = 0; i < _objCount; i++)
        {
            Vector3 pos = new Vector3(
                Random.Range(-10f, 10f),
                Random.Range(0f, 5f),
                Random.Range(-10f, 10f)
            );

            /// Create a transform matrix at the random position, with no rotation and uniform scale
            matrices[i] = Matrix4x4.TRS(pos, Quaternion.identity, Vector3.one);
        }
        
        /// Send the data to the GPUInstancing component
        _gpuInstancing.Setup(_mesh, _material, matrices);
    }
}

Multiple meshes rendered directly on the GPU Multiple meshes rendered directly on the GPU

At this point we covered most fundamentals of Unity's GPU Instancing API and you should be ready to expand this to fit your project's needs.

Benchmarking With Multiple Meshes & Materials

I’ve spent some time building a more robust version, and a counterpart that does the same thing but on the CPU. This new version supports multiple different meshes, and even meshes with multiple materials each. It became my default for rendering in all of my projects, as the results showed a significant advantage for GPU Instancing and it's really easy to use.

You can get this package from the Asset Store down below: Ultimate GPU Instancing - Global Rendering Performance

For this benchmark I did two runs: a run with 40,000 GameObjects in-editor and a run with 80,000 GameObjects in-build.

Metric GPU Instancing Min GPU Instancing Max Non-GPU Instancing Min Non-GPU Instancing Max
FPS 29.13 30.88 7.67 16.17
Draw Calls 263 611 80,031 200,031
Batches 263 611 80,031 200,031
Triangles 33,840,460 84,600,460 33,840,480 84,600,480
Vertices 23,540,750 58,850,760 23,540,780 58,850,790
SetPass Calls 36 39 61 105
Shadow Casters 40 160 40,000 160,000
Instanced Batches 80,000 200,000 0 0

Results:

Performance Comparison Table

  • GPU Instancing yields significantly higher FPS, indicating much better runtime performance.
  • GPU Instancing drastically reduces draw calls and batches, lowering the burden on the graphics API.
  • GPU Instancing requires fewer SetPass calls, reducing CPU overhead.
  • Triangle and vertex counts remain nearly identical, confirming that the geometry load is equivalent.
  • Both methods render the same number of objects casting shadows (up to 160,000), but GPU Instancing does so with much less performance cost.

Performance Comparison Table

Conclusion: A Brief View of the Rendering Potential

GPU instancing represents a shift in how you may approach rendering at scale. With this technique, we’ve seen how thousands of objects can be rendered with minimal performance impact, transforming what would have been frame staggers into smooth gameplay.

This approach gives you direct control over the rendering process, allowing you to optimize specifically for your game’s needs rather than relying on Unity’s automatic batching, which often falls short with complex scenes.

Where to Go From Here

While this implementation provides a solid foundation, there are several avenues for further optimization and expansion:

  1. Add instance property variation: Using material property blocks to give each instance unique colors or properties without breaking batching.
  2. Implement culling strategies: Only render objects within the camera’s view frustum to further reduce GPU workload.
  3. Dynamic instance management: Add and remove instances at runtime without rebuilding the entire matrix array.
  4. LOD (Level of Detail) system integration: Combine instancing with LOD techniques for objects at different distances.

Remember that GPU instancing works best with objects sharing the same mesh and material. For games with diverse asset types, you’ll want to organize your instancing system by these shared characteristics.

By letting hardware do the heavy lifting through GPU instancing, you open the door to much richer, more densely populated game worlds without sacrificing performance. Create vast forests, crowded cities, or particle-rich space battles, master this technique and you have the tools to leverage the most performance out of your assets and engine.

#unity#rendering#performance#game-dev#gpu

EDUARDO MARTINELLI

© 2026 • Fullstack Software Engineer

Thank you for checking out my work. Shoot me a message about interesting projects and collaborations!