Instanced Drawing With Unity, Part 1

Back in 2017, during the Unite Austin presentation, Unity revealed a fully performance oriented approach to programming, namely the Entity Component System (ECS). In there, the show case demonstrated that they could place 100,000 units on screen with logic and graphics and run the code at over 30 FPS. This was really impressive, but what was more impressive was that they started open sourcing a lot of it.

ECS at Unity Austin 2017

Having looked into the ECS samples I noticed that the way the boids demo was being rendered was using Graphics.DrawMeshInstanced() as you can see down here:

This was really interesting as it showed that a large number of meshes could be drawn with very little overhead compared to having each one be an individual object.

What is Instanced Drawing

Generally speaking, if you wanted to draw a large forest or a number of buildings, this could be a way. You draw the same mesh multiple times, in a single command, with some differing parameters to reduce repetition (changing color, or even animation frames for meshes).

Unity introduced this in Unity 5.4 as a very welcome feature!

There’s a few samples out there for how to use Graphics.DrawMeshInstanced(), but I’d like to try and present a minimal code sample for what you need in case you want to start looking into Instanced Drawing.

Instanced Shader Properties

First, you’ll need a shader that’s set up to work with instancing.

This is achieved by adding a number of instancing specific properties to it, namely:

#pragma multi_compile_instancing
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
UNITY_ACCESS_INSTANCED_PROP(arrayName, color)

From the Unity Documentation on GPU Instancing:

#pragma multi_compile_instancing

Use this to instruct Unity to generate instancing variants. It is not necessary for surface Shaders.

UNITY_VERTEX_INPUT_INSTANCE_ID

Use this in the vertex Shader input/output structure to define an instance ID. See SV_InstanceID for more information.

UNITY_SETUP_INSTANCE_ID(v);

Use this to make the instance ID accessible to Shader functions. It must be used at the very beginning of a vertex Shader, and is optional for fragment Shaders.

UNITY_INSTANCING_BUFFER_START(name)
UNITY_INSTANCING_BUFFER_END(name)

Every per-instance property must be defined in a specially named constant buffer. Use this pair of macros to wrap the properties you want to be made unique to each instance.

UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

Use this to define a per-instance Shader property with a type and a name. In this example, the _Color property is unique.

UNITY_ACCESS_INSTANCED_PROP(arrayName, color)

Use this to access a per-instance Shader property declared in an instancing constant buffer. It uses an instance ID to index into the instance data array. The arrayName in the macro must match the one in UNITY_INSTANCING_BUFFER_END(name) macro.

Now that the minimum amount of properties needed to have a shader that is compatible with instancing have been identified, its time to put them to use in a shader!

Creating the shader

Here is a shader that uses the properties mentioned above. Its also available on GitHub under the MinimalInstancing project

MinimalInstanced Shader

Shader "Custom/MinimalInstancedShader"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
            UNITY_INSTANCING_BUFFER_END(Props)

            v2f vert(appdata v)
            {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);            
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float4 color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
                return color;
            }
            ENDCG
        }
    }
}

Graphics.DrawMeshInstanced Script

Next, we’ll need a script that uses the shader combined with Graphics.DrawInstanced() to really show off just how much geometry you can draw with this API!

We need to use a material that uses the shader we created above, and we also need a model to draw, in this case a prefab I created with ProBuilder.

Draw Mesh Instanced gotchas

When it comes to actually invoking Graphics.DrawMeshInstanced() there are a couple things to point out:

    1. An Array of Matrix4x4 are required. These matrices represent where the Meshes are to be drawn (translation, rotation & scale).

    2. The Matrix4x4 array is limited to 1023 entries, so as the number of entities goes up, we’ll need to create more batches which could hinder performance, though 40,000 instances runs without a hitch.

    3. The desired length of the Array is also required as a separate variable, but it does not need to match the 1023 limit as you can see in the code below.

using UnityEngine;

public class DrawInstancedScript : MonoBehaviour
{
    const float BATCH_MAX_FLOAT = 1023f;
    const int BATCH_MAX = 1023;

    public GameObject prefab;
    public Material meshMaterial;
    public int width;
    public int depth;    
    public float spacing;
   
    private MeshFilter mMeshFilter;
    private MeshRenderer mMeshRenderer;
    private Matrix4x4[] matrices;

    void Start ()
    {
        mMeshFilter = prefab.GetComponent<MeshFilter>();
        mMeshRenderer = prefab.GetComponent<MeshRenderer>();
       
        InitData();
    }

    private void InitData()
    {
        int count = width * depth;

        matrices = new Matrix4x4[count];
        Vector3 pos = new Vector3();
        Vector3 scale = new Vector3(1, 1, 1);

        for (int i = 0; i < width; ++i)
        {
            for (int j = 0; j < depth; ++j)
            {
                int idx = i * depth + j;

                matrices[idx] = Matrix4x4.identity;

                pos.x = i * spacing;
                pos.y = 0;
                pos.z = j * spacing;

                matrices[idx].SetTRS(pos, Quaternion.identity, scale);
            }
        }
    }

    void Update ()
    {
        int total = width * depth;
        int batches = Mathf.CeilToInt(total / BATCH_MAX_FLOAT);

        for (int i = 0; i < batches; ++i)
        {
            int batchCount = Mathf.Min(BATCH_MAX, total - (BATCH_MAX * i));
            int start = Mathf.Max(0, (i - 1) * BATCH_MAX);

            Matrix4x4[] batchedMatrices = GetBatchedMatrices(start, batchCount);
            Graphics.DrawMeshInstanced(mMeshFilter.sharedMesh, 0, meshMaterial, batchedMatrices, batchCount);
        }
    }

    private Matrix4x4[] GetBatchedMatrices(int offset, int batchCount)
    {
        Matrix4x4[] batchedMatrices = new Matrix4x4[batchCount];

        for(int i = 0; i < batchCount; ++i)
        {
            batchedMatrices[i] = matrices[i + offset];
        }

        return batchedMatrices;
    }
}

DrawInstanced Script setup

Just one more step before we can see the result, which is assigning values to the DrawInstancedScript

For this demo, I went with a 200×200 sized grid for a total of 40,000 prisms being drawn on screen, with these settings:

With just enough spacing to be able to tell that the prisms are individual

Once  we have this all set up, this is what we can see

Batching 40,000 prisms

This project (as well as the upcoming Part02) are available on GitHub:

https://github.com/JavDevGames/MinimalInstancing

In Part 2, I’ll show you how to use the Material Property Block combined with DrawMeshInstanced in order to create a scene with more variation, and here’s a sneak peak into it:

Batching 40,000 prisms

You can always follow me on Twitter @JavDev