# 4. Indirect Draws with GraphicsBuffer
Part of [[Compute Shaders and GPU Instancing]]. Prev: [[3. Compute Shader Fundamentals]]. Next: [[5. Worked Example Compute Swarm]].
## 4.1 What "indirect" means
With `RenderMeshInstanced` the CPU says "draw this mesh N times with these matrices". With `RenderMeshIndirect` the CPU says "draw this mesh; **read how many and where from this GPU buffer**". The count lives in an **args buffer** (`GraphicsBuffer.Target.IndirectArguments`) laid out as `GraphicsBuffer.IndirectDrawIndexedArgs`:
| Field | Meaning |
|---|---|
| `indexCountPerInstance` | indices in the submesh (`mesh.GetIndexCount(0)`) |
| `instanceCount` | how many instances (the GPU can overwrite this, part 6) |
| `startIndex` | first index (`mesh.GetIndexStart(0)`) |
| `baseVertexIndex` | base vertex (`mesh.GetBaseVertex(0)`) |
| `startInstance` | first instance ID (usually 0) |
The reference says to use this struct rather than raw ints "as the layout of this structure can change depending on the platform", requires compute-shader support, and tells you to set `RenderParams.worldBounds` because Unity culls the whole draw as one entity.
The instance data (positions etc.) is *your* `StructuredBuffer`; Unity does not supply matrices. Your shader reads it with the instance ID.
## 4.2 The shared agent struct
Used by this part, the swarm (part 5) and culling (part 6). C# and HLSL must match byte-for-byte.
```csharp
using System.Runtime.InteropServices;
using UnityEngine;
/// <summary>One swarm agent. 24 bytes: matches `struct Agent { float3 position; float3 velocity; }` in HLSL.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct SwarmAgent
{
public Vector3 position;
public Vector3 velocity;
public const int Stride = sizeof(float) * 6;
}
```
## 4.3 `SwarmIndirectURP.shader`
```hlsl
Shader "Tutorial/SwarmIndirectURP"
{
Properties
{
_BaseColor ("Base Color", Color) = (0.9, 0.6, 0.2, 1)
_FastColor ("Fast Color", Color) = (1, 0.1, 0.4, 1)
_Scale ("Scale", Float) = 0.15
_MaxSpeed ("Speed for Fast Color", Float) = 8
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" }
HLSLINCLUDE
#pragma target 4.5
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// Unity's indirect helpers (documented on Graphics.RenderMeshIndirect).
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#include "UnityIndirect.cginc"
struct Agent { float3 position; float3 velocity; };
StructuredBuffer<Agent> _Agents;
StructuredBuffer<uint> _VisibleIndices; // part 6
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _FastColor;
float _Scale;
float _MaxSpeed;
CBUFFER_END
// Rows = right, up, forward. Use mul(rowVector, M) to go object -> world.
float3x3 LookRotation(float3 forward)
{
float3 up = abs(forward.y) > 0.99 ? float3(1, 0, 0) : float3(0, 1, 0);
float3 right = normalize(cross(up, forward));
float3 newUp = cross(forward, right);
return float3x3(right, newUp, forward);
}
uint ResolveAgentIndex(uint svInstanceID)
{
InitIndirectDrawArgs(0);
uint i = GetIndirectInstanceID(svInstanceID);
#if defined(USE_VISIBLE_LIST)
i = _VisibleIndices[i];
#endif
return i;
}
float3 AgentWorldPos(float3 positionOS, Agent a, out float3x3 rot)
{
float speed = length(a.velocity);
float3 fwd = speed > 1e-4 ? a.velocity / speed : float3(0, 0, 1);
rot = LookRotation(fwd);
return mul(positionOS * _Scale, rot) + a.position;
}
ENDHLSL
Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_local _ USE_VISIBLE_LIST
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; };
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float3 color : TEXCOORD1;
};
Varyings vert(Attributes v, uint svInstanceID : SV_InstanceID)
{
Varyings o;
Agent a = _Agents[ResolveAgentIndex(svInstanceID)];
float3x3 rot;
float3 posWS = AgentWorldPos(v.positionOS.xyz, a, rot);
o.positionCS = TransformWorldToHClip(posWS);
o.normalWS = normalize(mul(v.normalOS, rot));
float t = saturate(length(a.velocity) / max(_MaxSpeed, 1e-3));
o.color = lerp(_BaseColor.rgb, _FastColor.rgb, t);
return o;
}
half4 frag(Varyings i) : SV_Target
{
Light mainLight = GetMainLight();
half ndl = saturate(dot(normalize(i.normalWS), mainLight.direction));
half3 lit = i.color * (0.25h + ndl * mainLight.color);
return half4(lit, 1);
}
ENDHLSL
}
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "DepthOnly" }
ColorMask R
HLSLPROGRAM
#pragma vertex vertDepth
#pragma fragment fragDepth
#pragma multi_compile_local _ USE_VISIBLE_LIST
struct AttributesD { float4 positionOS : POSITION; };
struct VaryingsD { float4 positionCS : SV_POSITION; };
VaryingsD vertDepth(AttributesD v, uint svInstanceID : SV_InstanceID)
{
VaryingsD o;
Agent a = _Agents[ResolveAgentIndex(svInstanceID)];
float3x3 rot;
o.positionCS = TransformWorldToHClip(AgentWorldPos(v.positionOS.xyz, a, rot));
return o;
}
half fragDepth(VaryingsD i) : SV_Target { return 0; }
ENDHLSL
}
}
}
```
### Reading the shader
- **`HLSLINCLUDE`** holds code shared by both passes: the agent struct, buffers, material constants and helpers.
- **`UNITY_INDIRECT_DRAW_ARGS` + `UnityIndirect.cginc` + `InitIndirectDrawArgs(0)` + `GetIndirectInstanceID(svInstanceID)`** are the documented way to get the correct instance index for indirect draws (it accounts for `startInstance` and multi-command buffers).
- **`USE_VISIBLE_LIST`** is a local keyword: off = draw agents `0..instanceCount-1`; on = the instance index is looked up through the culled list from part 6.
- **`DepthOnly` pass**: URP renders a depth prepass in some configurations (SSAO, depth priming, depth texture). Without it, the swarm can be missing from the depth texture. A `ShadowCaster` pass is left as an exercise (copy `DepthOnly`, use `LightMode = ShadowCaster`, and apply URP's shadow bias helpers).
- **`CBUFFER_START(UnityPerMaterial)`** keeps the material layout SRP-Batcher-style; harmless here and good habit.
- **Lighting** is a simple Lambert from the main light. Swap in URP's full PBR `UniversalFragmentPBR` once it works (see [[URP Shaders and Shader Graph]]).
> [!warning] If `UnityIndirect.cginc` will not compile in URP
> I did not verify that this built-in include works inside a URP `HLSLPROGRAM`. If it errors, delete the `#define`/`#include` and the `InitIndirectDrawArgs`/`GetIndirectInstanceID` calls, and use `svInstanceID` directly. That is correct as long as you issue **one command** with `startInstance = 0`, which is all this series does.
## 4.4 `IndirectInstanceRenderer.cs`
```csharp
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>
/// Draws `mesh` once per agent in a GPU buffer using RenderMeshIndirect.
/// Other components provide the agent buffer (SetSource) and, optionally, a culled index list.
/// NOT compiled.
/// </summary>
public sealed class IndirectInstanceRenderer : MonoBehaviour
{
[SerializeField] Mesh mesh;
[SerializeField] Material material; // Tutorial/SwarmIndirectURP
[SerializeField] Vector3 boundsSize = new(200, 200, 200);
[SerializeField] ShadowCastingMode shadows = ShadowCastingMode.Off;
static readonly int AgentsId = Shader.PropertyToID("_Agents");
static readonly int VisibleId = Shader.PropertyToID("_VisibleIndices");
public GraphicsBuffer Args { get; private set; }
readonly GraphicsBuffer.IndirectDrawIndexedArgs[] _argsData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
Material _runtimeMaterial; // per-component copy so keywords do not leak into the asset
MaterialPropertyBlock _mpb;
RenderParams _rp;
void OnEnable()
{
Args = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
_argsData[0].indexCountPerInstance = mesh.GetIndexCount(0);
_argsData[0].startIndex = mesh.GetIndexStart(0);
_argsData[0].baseVertexIndex = mesh.GetBaseVertex(0);
_argsData[0].startInstance = 0;
_argsData[0].instanceCount = 0;
Args.SetData(_argsData);
_runtimeMaterial = new Material(material);
_mpb = new MaterialPropertyBlock();
_rp = new RenderParams(_runtimeMaterial)
{
matProps = _mpb,
shadowCastingMode = shadows,
receiveShadows = false,
layer = gameObject.layer
};
}
void OnDisable()
{
Args?.Release();
Args = null;
if (_runtimeMaterial != null) Destroy(_runtimeMaterial);
}
/// <summary>Point the renderer at an agent buffer and set the CPU-known instance count.</summary>
public void SetSource(GraphicsBuffer agents, int count)
{
_mpb.SetBuffer(AgentsId, agents);
_argsData[0].instanceCount = (uint)count;
Args.SetData(_argsData);
}
/// <summary>Use a culled index list (part 6). The GPU writes instanceCount via CopyCount.</summary>
public void SetVisibleList(GraphicsBuffer visibleIndices)
{
if (visibleIndices != null)
{
_mpb.SetBuffer(VisibleId, visibleIndices);
_runtimeMaterial.EnableKeyword("USE_VISIBLE_LIST");
}
else _runtimeMaterial.DisableKeyword("USE_VISIBLE_LIST");
}
void LateUpdate()
{
if (Args == null) return;
_rp.worldBounds = new Bounds(transform.position, boundsSize);
Graphics.RenderMeshIndirect(_rp, mesh, Args);
}
}
```
### Why `LateUpdate`?
The swarm simulation dispatches in `Update`; drawing in `LateUpdate` guarantees the draw is queued after this frame's dispatch on the same command stream, so you draw this frame's positions. The GPU executes them in order; the CPU never waits.
### Bounds
`worldBounds` must contain every instance or Unity culls the whole draw when the box leaves the view (and shadow casting misjudges). Make it generous, or have the simulation keep agents inside a known sphere (part 5 does).
## 4.5 Quick test without a compute shader
```csharp
using UnityEngine;
/// <summary>Fills an agent buffer once from the CPU to prove the renderer works. NOT compiled.</summary>
public sealed class StaticAgentsTest : MonoBehaviour
{
[SerializeField] IndirectInstanceRenderer target;
[SerializeField] int count = 10000;
[SerializeField] float radius = 40f;
GraphicsBuffer _agents;
void Start()
{
var data = new SwarmAgent[count];
var rng = new System.Random(7);
for (int i = 0; i < count; i++)
{
Vector3 p = Random.insideUnitSphere * radius;
data[i] = new SwarmAgent { position = transform.position + p, velocity = Vector3.Cross(p, Vector3.up).normalized * (float)rng.NextDouble() * 8f };
}
_agents = new GraphicsBuffer(GraphicsBuffer.Target.Structured, count, SwarmAgent.Stride);
_agents.SetData(data);
target.SetSource(_agents, count);
}
void OnDestroy() => _agents?.Release();
}
```
You should see 10,000 oriented, speed-coloured meshes (use a small cone or arrow mesh) in **one** draw in the Frame Debugger.
## Sources
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Graphics.RenderMeshIndirect.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/GraphicsBuffer.IndirectDrawIndexedArgs.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/MaterialPropertyBlock.SetBuffer.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Mesh.GetIndexStart.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Mesh.GetBaseVertex.html
- URP shader authoring (Manual): https://docs.unity3d.com/6000.5/Documentation/Manual/urp/writing-custom-shaders-urp.html