You render multiple meshes with RenderMeshIndirect by supplying a single “mega-mesh”/“mesh-atlas” which contains all the meshes you want to render. You then pick the correct mesh from the mega-mesh by supplying the correct offsets to the graphicsBuffer and setting commandCount to the amount of different meshes you want to render.
So basically bind all the mesh data once and then pick the individual mesh via the GraphicsBuffer and commandCount supplied to RenderMeshIndirect.
Quick and dirty example for that:
multiDrawCommandsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, meshes.Count, GraphicsBuffer.IndirectDrawIndexedArgs.size);
multiDrawCommands = new GraphicsBuffer.IndirectDrawIndexedArgs[meshes.Count];
//Create merged mesh
if (mergedMesh != null)
{
UnityEngine.Object.Destroy(mergedMesh);
}
mergedMesh = new Mesh();
int vertexCount = 0;
int indexCount = 0;
foreach (Mesh m in meshes)
{
vertexCount += m.vertexCount;
indexCount += (int)m.triangles.Length;
}
//Create merged mesh and multDrawCommands
Vector3[] vertices = new Vector3[vertexCount];
int[] indices = new int[indexCount];
Vector3[] normals = new Vector3[vertexCount];
int currentVertexCount = 0;
int currentIndexCount = 0;
for (int i = 0; i < meshes.Count; i++)
{
Mesh m = meshes[i];
Array.Copy(m.vertices, 0, vertices, currentVertexCount, m.vertexCount);
Array.Copy(m.triangles, 0, indices, currentIndexCount, m.triangles.Length);
Array.Copy(m.normals, 0, normals, currentVertexCount, m.vertexCount);
multiDrawCommands[i].baseVertexIndex = (uint)currentVertexCount;
multiDrawCommands[i].indexCountPerInstance = (uint)m.triangles.Length;
multiDrawCommands[i].instanceCount = instanceCount;
multiDrawCommands[i].startIndex = (uint)currentIndexCount;
multiDrawCommands[i].startInstance = (uint)(i * instanceCount);
currentVertexCount += m.vertexCount;
currentIndexCount += (int)m.triangles.Length;
}
//mergedMesh.SetVertices(vertices);
//mergedMesh.SetIndices(indices, MeshTopology.Triangles, 0);
//mergedMesh.SetNormals(normals);
mergedMesh.vertices = vertices;
mergedMesh.triangles = indices;
mergedMesh.normals = normals;
mergedMesh.RecalculateTangents();
multiDrawCommandsBuffer.SetData(multiDrawCommands);
You can also fill the GraphicsBuffer on the GPU via a ComputeShader if you want to do culling on the GPU.
And then call this somewhere else to actually render everything in one go:
RenderParams rp = new RenderParams(material);
rp.worldBounds = new Bounds(Vector3.zero, 10000 * Vector3.one); // use tighter bounds for better FOV culling
rp.matProps = new MaterialPropertyBlock();
Graphics.RenderMeshIndirect(rp, mergedMesh, multiDrawCommandsBuffer, meshes.Count);
And a quick and dirty test shader to render everything, which is basically the one from the documentation. Please note that this is still using the old CG syntax and not the “new” HLSL syntax which should be used for the SRP:
Shader "Custom/UberTweaked"
{
Properties
{
}
SubShader
{
Tags {
"RenderType" = "Opaque"
"LightMode" = "SRPDefaultUnlit"
}
Pass
{
CGPROGRAM
#pragma target 4.5
#pragma vertex vert
#pragma fragment frag
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#include "UnityIndirect.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normals : NORMAL;
uint svInstanceID : SV_InstanceID;
//uint svVertexID : SV_VertexID;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float3 worldNormal : TEXCOORD0;
};
v2f vert(appdata v)
{
InitIndirectDrawArgs(0);
v2f o;
uint cmdID = GetCommandID(0);
uint instanceID = GetIndirectInstanceID(v.svInstanceID);
float4 wpos = mul(unity_ObjectToWorld, v.vertex + float4( (instanceID%10) * 15, cmdID * 8, (int)(instanceID / 10) * 15, 0));
o.pos = mul(UNITY_MATRIX_VP, wpos);
o.color = v.vertex / 10;// v.svInstanceID;// float4(cmdID & 1 ? 0.0f : 1.0f, cmdID & 1 ? 1.0f : 0.0f, instanceID), 0.0f);
o.worldNormal = mul((float3x3)unity_ObjectToWorld, v.normals.xyz).xyz;
return o;
}
float4 frag(v2f i) : SV_Target
{
float4 color = 1;
color.xyz = dot(normalize(i.worldNormal.xyz), normalize(float3(1, 1, 0)));
return color;
}
ENDCG
}
}
}
I also had a look at the documentation again and chances are pretty high that it uses MDI under the hood on platforms that support it.
From the documentation: