Rendering meshes in Unity without MeshRenderers

Overview

Use DrawMesh in situations where you want to draw large amount of meshes, but don’t want the overhead of creating and managing game objects.
DrawMesh draws a mesh for one frame. The mesh will be affected by the lights, can cast and receive shadows, just like if it was part of some game object. It can be drawn for all cameras or just for some specific camera (either by passing a Camera to the call or by calling these methods on a CommandBuffer).

If you go on Unity’s docs you can see many similar methods; Here’s a brief summary:

  • DrawMesh (Docs): the simplest one. Schedules a mesh with a given material to be rendered.
  • DrawMeshInstanced (Docs): means it will render the mesh using GPU instancing. This is useful when you want to render very large numbers of objects that are all the same (with small variations done in shader, like different colors). Receives a Matrix4x4 array to specify where to draw them.
  • DrawMeshInstancedIndirect (Docs): like the previous one, but receives a ComputeBuffer that contains count of instances and other values. Positions and everything else is done in shader. This is usually used in combination with Compute Shaders.
  • DrawMeshInstancedProcedural (Docs): same as DrawMeshInstancedIndirect but for when instance count is known beforehand and can be passed directly from C#.
  • DrawProcedural (Docs): does a draw call on the GPU, without any vertex or index buffers. This is mainly useful on Shader Model 4.5 level hardware where shaders can read arbitrary data from ComputeBuffer buffers. The mesh is generated by a ComputeShader.
  • DrawProceduralIndirect (Docs): same as DrawProcedural but receives the count of instances and other stuff from a ComputeBuffer too.

Instanced means it uses GPU Instancing.
Procedural means no matrices array (for position, rotation, scale), shader has to handle transformations on it’s own from data it receives in ComputeBuffer
Indirect means that instance count is stored inside ComputeBuffer too.
No Mesh in the name means the mesh gets built by a Compute shader.

Some of them also have *Now variants, these are for drawing immediately. The base versions only “submit” the mesh for rendering and it will then be rendered later in the frame as part of normal rendering process.

  • DrawMeshNow (Docs)
  • DrawProceduralIndirectNow (Docs)
  • DrawProceduralNow (Docs)

Some code examples

Let’s start with the easiest one, DrawMesh.

public class ExampleDrawMesh : MonoBehaviour
{
  // assign these in the inspector
  public Mesh mesh;
  public Material material;

  void Update()
  {
    // will make the mesh appear in the Scene at origin position
    Graphics.DrawMesh(mesh, Vector3.zero, Quaternion.identity, material, 0);
  }
}

And DrawMeshNow is very similar. Notice however that the code is not in the Update method but instead in OnPostRender. Like explained before this function draws the mesh immediately so we have to call the code while Unity is in the rendering phase.

// Attach this script to a gameobject with a Camera component, otherwise `OnPostRender` will not be called
public class ExampleDrawMeshNow : MonoBehaviour
{
  // assign these in the inspector
  public Mesh mesh;
  public Material material;

  public void OnPostRender()
  {
    // set first shader pass of the material
    material.SetPass(0);
    // draw mesh at the origin
    Graphics.DrawMeshNow(mesh, Vector3.zero, Quaternion.identity);
  }
}

When switching to the Instanced function things get a bit more complex. We have to give it an array with the positions, rotations and scales of the object we want to draw. This data is all packed into a Matrix4x4 for each object. In the next example we want to draw 10 objects, so we setup an array of that length. In this case, it then gets filled with the positions all in a line on the z axis, no rotation and a scale of 0.3.

public class ExampleDrawMeshInstanced : MonoBehaviour
{
  public Mesh mesh;
  public Material material;
  Matrix4x4[] matrices;

  void OnEnable()
  {
    matrices = new Matrix4x4[10]; // initialize array
    for (int i = 0; i < 10; i++)
    {
      Vector3 position = new Vector3(0, 0, i);
      Quaternion rotation = Quaternion.identity;
      Vector3 scale = new Vector3(0.3f, 0.3f, 0.3f);
      matrices[i] = Matrix4x4.TRS(position, rotation, scale);
    }
  }

  void Update()
  {
    Graphics.DrawMeshInstanced(mesh, 0, material, matrices);
  }
}

Usage examples

You can of course update the positions each frame before rendering too. This code for example makes them move in a wave motion up and down.

void Update()
{
  float t = Time.time;

  Vector3 position = transform.position;
  Vector3 scale = new Vector3(0.3f, 0.3f, 0.3f);
  for (int i = 0; i < 50; i++)
  {
    Vector3 offset = new Vector3(0, Mathf.Sin(t + (i / 3f)), i * 0.3f);
    matrices[i] = Matrix4x4.TRS(position + offset, Quaternion.identity, scale);
  }

  Graphics.DrawMeshInstanced(mesh, 0, material, matrices);
}

And here’s the result with the default cube picked as mesh and a fancy material.

This way of rendering is very flexible, you can use this for anything you like.
For example in my game one of the places where I use it is to render these orbs around the player. The positions are calculated by a very simple boid-like simulation.

DrawMeshInstancedIndirect

DrawMeshInstancedIndirect allows us to offload almost all the work to the GPU. Usually Unity needs to upload the data to the GPU each frame, but with this method it only does it once and then it stays there. It does need a larger amount of code but it seems more difficult than it actually is.

You can use the previous DrawMeshInstanced example as a starting point. We’ll add some variables to store the buffers.

ComputeBuffer argsBuffer;
ComputeBuffer matricesBuffer; // replaces Matrix4x4[] matrices;

Then we set it up (in the Start method for example).
Begin by initializing the buffer with the arguments, the docs have more information on the different values.

argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(new uint[] {
    (uint)mesh.GetIndexCount(0), // triangle indices count per instance
    (uint)instanceCount, // instance count
    (uint)mesh.GetIndexStart(0), // start index location
    (uint)mesh.GetBaseVertex(0), // base vertex location
    0, // start instance location
});

You also need to define the bounds of the rendered meshes, Unity will use this to cull it when outside the camera’s frustum (even if the docs say that it wont). Adjust it based on the amount of space your effect needs, here I’m just using an arbitrary 10x10x10 box.

bounds = new Bounds(Vector3.zero, new Vector3(10.0f, 10.0f, 10.0f));

Setting up the matrix array can be done the same way as before. But this time we store it in a ComputeBuffer that gets then passed to the material.

Matrix4x4[] matrices = new Matrix4x4[instanceCount];
for (int i = 0; i < instanceCount; i++) {
    Vector3 offset = new Vector3(i * 0.3f, Mathf.Sin(i / 3f), 0);
    matrices[i] = Matrix4x4.TRS(transform.position + offset, Quaternion.identity, new Vector3(0.3f, 0.3f, 0.3f));
}
matricesBuffer = new ComputeBuffer(instanceCount, sizeof(float) * 4 * 4);
matricesBuffer.SetData(matrices);
material.SetBuffer("matricesBuffer", matricesBuffer);

Then in the Update method we do the actual call:

void Update() {
    Graphics.DrawMeshInstancedIndirect(mesh, 0, material, bounds, argsBuffer);
}

Make sure to release the buffers when you no longer need them.

void OnDisable()
{
    matricesBuffer?.Release();
    matricesBuffer = null;

    argsBuffer?.Release();
    argsBuffer = null;
}

You can then use compute shaders to do heavy computations for the movement of the meshes. (Compute shaders are out of scope for this tutorial but a small example is included in the Premium file download).

This is the same video as before, since the output is the same. But this time it was executed on the GPU!


Download files

Includes scripts for DrawMesh, DrawMeshInstanced, DrawMeshInstancedIndirect. Two shaders for DrawMeshInstancedIndirect, a basic one and a surface shader with PBR(Standard). An example using a compute shader to move the meshes. And an example scene showing how to to use the scripts.

Preview of the example scene

Enjoying the tutorials? Are they useful? Want more?

If you think these posts have either helped or inspired you, please consider supporting this blog.

Help this blog continue existing

If the knowledge you have gained had a significant impact on your project, a mention in the credits would be very appreciated.

Silo27 Logo
Enrico Monese

All around developer, from games and web to weird commandline utilities. 2D/3D art, with a good care for UI/UX and clean design.

Twitter Twitter Patreon Patreon Mastodon Mastodon Blog Blog   Reddit Reddit Github Github Gitlab Gitlab ArtStation ArtStation Mail Mail