# 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