Indirect & procedural rendering with Shader Graph

Hi fellow Shader Graph users!

In this thread I want to touch on DrawIndirect/DrawProcedural, a topic we’ve received a lot of requests about.

Although it requires custom HLSL for defines, includes and types unsupported in Shader Graph such as StructuredBuffers, we shall see there’s nothing really complicated about making shader graphs work with Indirect and/or Procedural Rendering.

I’m going to split this into several posts to make it easier to reference them later, and use this first post as a table of content.

Note about GPU Instancing

I’d like to differentiate two ways of using GPU Instancing.

Automatic Instancing

Automatic Instancing is when you have mesh renderers in the scene with ‘GPU Instancing enabled’ materials, or when using Graphics.RenderMeshInstanced().
In that case, Shader Graph shaders already account for per-instance ObjectToWorld matrix and RenderingLayer. They will just work.

Instrumented Instancing

“Instrumented” Instancing is when you want to customize how each instance is treated in the shader, such as fetching a color from a global StructuredBuffer. Shader Graph shaders then need to implement the specifics about per-instance data.

Note about #pragma instancing_options

There are pros and cons about using #pragma instancing_options procedural:<func> to handle per instance setup. I’ll touch on this in more detail later in the thread. For now let’s just say it is not required, as we shall see in the examples.

Table of Content

Please use this thread to ask questions or give feedback on how we can make this any better.

13 Likes

God :face_in_clouds:
Procedural rendering (or even rendering Shuriken Particles with mesh type) is the biggest headache ever.
For which version of Shader Graph will this be available?
Is it possible to add this feature to earlier versions of Shader Graph, or it’s a part of the Unity 6.1+ update?

While the documentation examples don’t provide real use cases – since you would usually want to write to graphics buffers from a Compute shader and not from a C# script – they are great as unit tests to validate how one can use Shader Graph with lower level graphics APIs.

First of all, Graphics.Draw__() methods were deprecated and replaced with Graphics.Render__().

This post will detail how the latter can be used with Shader Graph shaders, but it applies to former deprecated methods the same.

Also, since those examples use an Unlit material and only translate positions, I’m using the most straightforward approach to handle it. But I’ll follow up on dealing with Lit materials and per-instance rotation.

Compatibility & Requirements

Render Pipelines

With the exception of RenderMeshInstanced that requires a material that enables GPU Instancing, the following examples will work with both Unlit and Lit Shader Graphs, with Built-in, Universal and High Definition Render Pipelines.

The following examples were tested using:

  • Built-in Render Pipeline (Forward, Deferred)
  • Universal Render Pipeline (Forward, Deferred, Forward+, Deferred+)
  • High-Definition Render Pipeline (Forward Only, Deferred Only, Both)
  • Unlit and Lit targets

Frame Debugger

If you look in the Frame Debugger, it will appear like it’s not using Draw Procedural, but a deeper look in RenderDoc will display the same results as a handwritten shader.

Documentation Examples

RenderMesh

Renders a mesh with given rendering parameters.

There are no specific requirements for this example. It will work with any Shader Graph shader.

RenderMeshInstanced

Renders multiple instances of a mesh using GPU instancing.

  • This requires GPU Instancing to be enabled on the Material.
  • The InstanceID Node will return the unique index of the instance.

Note: Shader Graph shaders do not support GPU Instancing with the Built-in Render Pipeline.
Although this can be forced with mat.enableInstancing = true;, it will only work with an Unlit shader.

As mentioned in this other post, note that ‘GPU Instancing’ is referring to “legacy” GPU Instancing, and not the more modern approach using the GPU Resident Drawer.

RenderMeshIndirect

Renders multiple instances of a mesh using GPU instancing and rendering command arguments from commandBuffer.
This function only works on platforms that support compute shaders.

See → SystemInfo.supportsIndirectArgumentsBuffer

This requires the following define and include:

#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#include "UnityIndirect.cginc"

These can be put in an HLSL file and referenced from a Custom Function Node.

GetCommandID and GetIndirectInstanceCount can both be accessed with a Custom Function Node.

They both need InitIndirectDrawArgs(0) to be called before. It’s then better to fetch them both from the same function as to call InitIndirectDrawArgs only once.

uniform float4x4 _ObjectToWorld;
This can either be declared in the HLSL file, and accessed with another Custom Function Node, or simply set as a Per Material Matrix 4x4 Property in the Blackboard.

The InstanceID Node will return the unique index of the instance.

The following HLSL file can be used with Custom Function Nodes in a Shader Graph to recreate the example.

#ifndef UNITY_INDIRECT_DRAW_ARGS
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#include "UnityIndirect.cginc"
#endif

#ifndef RENDER_MESH_INDIRECT_EXAMPLE
#define RENDER_MESH_INDIRECT_EXAMPLE

void CommandID_IndirectInstanceCount_float(out uint CommandID, out uint IndirectInstanceCount)
{
#ifndef SHADERGRAPH_PREVIEW
    InitIndirectDrawArgs(0);
    CommandID = GetCommandID(0);
    IndirectInstanceCount = GetIndirectInstanceCount();
#else
    CommandID = 0;
    IndirectInstanceCount = 1;
#endif
}
#endif

Using the Custom Function above, the following graph does the same thing as the shader in the documentation example.

Note: a Custom Interpolator allows setting the color in vertex stage, then getting the interpolated value in fragment stage.

RenderMeshPrimitives

Renders multiple instances of a Mesh using GPU instancing and a custom shader.

This example requires no custom HLSL.

The InstanceID Node will return the unique index of the instance.

uniform float4x4 _ObjectToWorld;
uniform float _NumInstances;

These can be set as Per Material Properties in the Blackboard.

The following graph does the same thing as the shader in the documentation example.

RenderPrimitives

Renders non-indexed primitives with GPU instancing and a custom shader.

This example requires custom HLSL to declare and access the StructuredBuffer and uint uniforms.

StructuredBuffer<int> _Triangles;
StructuredBuffer<float3> _Positions;
uniform uint _StartIndex;
uniform uint _BaseVertexIndex;

void Position_float(uint vertexID, out float3 Out)
{
    Out = _Positions[_Triangles[vertexID + _StartIndex] + _BaseVertexIndex];
}

Using the Custom Function above, the following graph does the same thing as the shader in the documentation example.

Note: a Predefined Keyword SHADERGRAPH_PREVIEW, with the preview value set to true, allows bypassing the Position fed by the Custom Function Node, as to make the Shader Graph Main Preview work.

RenderPrimitivesIndirect

Renders primitives with GPU instancing and a custom shader using rendering command arguments from commandBuffer.
This function only works on platforms that support compute shaders.

Requirements are similar to those of RenderMeshIndirect and RenderPrimitives altogether, except we need to use IndirectDrawArgs and not IndirectDrawIndexedArgs.

#ifndef UNITY_INDIRECT_DRAW_ARGS
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawArgs
#include "UnityIndirect.cginc"
#endif

#ifndef RENDER_PRIMITIVES_INDIRECT_EXAMPLE
#define RENDER_PRIMITIVES_INDIRECT_EXAMPLE

StructuredBuffer<int> _Triangles;
StructuredBuffer<float3> _Positions;
uniform uint _StartIndex;
uniform uint _BaseVertexIndex;
void Position_float(uint vertexID, out float3 Out)
{
    Out = _Positions[_Triangles[vertexID + _StartIndex] + _BaseVertexIndex];
}

void CommandID_IndirectInstanceCount_float(out uint CommandID, out uint IndirectInstanceCount)
{
#ifndef SHADERGRAPH_PREVIEW
    InitIndirectDrawArgs(0);
    CommandID = GetCommandID(0);
    IndirectInstanceCount = GetIndirectInstanceCount();
#else
    CommandID = 0;
    IndirectInstanceCount = 1;
#endif
}

#endif

Using the Custom Function above, the following graph does the same thing as the shader in the documentation example.

RenderPrimitivesIndexed

Renders indexed primitives with GPU instancing and a custom shader.

Requirements are similar to those of RenderPrimitives.

#ifndef RENDER_PRIMITIVES_INDEXED_EXAMPLE
#define RENDER_PRIMITIVES_INDEXED_EXAMPLE

StructuredBuffer<float3> _Positions;
uniform uint _BaseVertexIndex;

void Position_float(uint vertexID, out float3 Out)
{
    Out = _Positions[vertexID + _BaseVertexIndex];
}

#endif

Using the Custom Function above, the following graph does the same thing as the shader in the documentation example.

RenderPrimitivesIndexedIndirect

Renders indexed primitives with GPU instancing and a custom shader with rendering command arguments from commandBuffer.
This function only works on platforms that support compute shaders.

Similar to RenderMeshIndirect and RenderPrimitives altogether.

#ifndef UNITY_INDIRECT_DRAW_ARGS
#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
#include "UnityIndirect.cginc"
#endif

#ifndef RENDER_PRIMITIVES_INDEXED_INDIRECT_EXAMPLE
#define RENDER_PRIMITIVES_INDEXED_INDIRECT_EXAMPLE

StructuredBuffer<float3> _Positions;

void Position_float(uint vertexID, out float3 Out)
{
    Out = _Positions[vertexID];
}

void CommandID_IndirectInstanceCount_float(out uint CommandID, out uint IndirectInstanceCount)
{
#ifndef SHADERGRAPH_PREVIEW
    InitIndirectDrawArgs(0);
    CommandID = GetCommandID(0);
    IndirectInstanceCount = GetIndirectInstanceCount();
#else
    CommandID = 0;
    IndirectInstanceCount = 1;
#endif
}

#endif

Using the Custom Function above, the following graph does the same thing as the shader in the documentation example.


I hope this helps and will follow up with more examples.

2 Likes

Lit Materials

ObjectToWorld & WorldToObject

As we could see in the documentation examples above, we were applying transformations to vertex positions in Object Space and ultimately multiplying them by _ObjectToWorld.

Shader Graph Vertex Stage Functions expect everything in ObjectSpace.

Further transformations, from Object to World Space, and ultimately to Camera (Clip) Space, are handled by the Shader Target, using unity_ObjectToWorld and unity_WorldToObject, automatically set when a Renderer is in the Scene.

When using low level API methods such as RenderMeshIndirect, there is no gameObject and no transform component, thus Object Space is World Space.

As such, we need to handle transformations from Local To World Space, and that can be done in several ways.

  • Use #pragma instancing_options procedural:<func> to modify unity_ObjectToWorld and unity_WorldToObject – or any other custom changes – at the beginning of the vertex function.
  • Use a Custom Function Node to do the same, but without the pragma.
  • Handle vertex data (position, normal and tangent) ourselves in the graph.

#pragma instancing_options procedural:<func>

This pragma is documented here: GPU instancing shader reference for the Built-In Render Pipeline.

Generates an additional variant for use with Graphics.RenderMeshIndirect. At the beginning of the vertex shader stage, Unity calls the function specified after the colon. To set up the instance data manually, add per-instance data to this function in the same way you would normally add per-instance data to a shader. Unity also calls this function at the beginning of a fragment shader if any of the fetched instance properties are included in the fragment shader.

So when writing a shader, one can easily add this pragma and then add the following to fetch a position or whole matrix for each instance.

#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
StructuredBuffer<float4x4> _Matrices;
#endif

void ProceduralSetup()
{
#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
    unity_ObjectToWorld = _Matrices[unity_InstanceID];
    unity_WorldToObject = transpose(unity_ObjectToWorld); // assume no scaling
#endif
}

But since this pragma needs to be in the main shader and cannot be set in an include, even when using #include_with_pragmas, to be used in a shader graph, this requires adding two Custom Function Nodes, one to reference the function, and another in String mode like this:

#pragma instancing_options procedural:ProceduralSetup
Out = In;

So not only is this quite convoluted, but looking at the description, there are several side effects:

It adds a variant, and calls the function in both the Vertex and Fragment stages, which seems suboptimal.

Custom ‘Setup’ Function Node

Since the pragma function is to be called at the beginning of the vertex function, one may just as well use a Custom Function Node and do necessary modifications.

#if !defined(SHADERGRAPH_PREVIEW) && !defined(SHADERGRAPH_PREVIEW_MAIN)
unity_ObjectToWorld = ObjectToWorld;
unity_WorldToObject = transpose(unity_ObjectToWorld);
#endif

PositionOut = PositionIn;

Note that in the example above, I use transpose(unity_ObjectToWorld) to set unity_WorldToObject which works given there’s no scaling. If we need to handle scaling, then a proper inversed matrix is required, which is expensive to do in HLSL. In such case it’s better to provide an inversed matrix from C#, or from a Compute shader, as to do it only one per instance.

Also, unity_ObjectToWorld and unity_WorldToObject are not accessible when using HDRP.

Two reasons in favor of the 3rd option described below.

Custom ‘Fetch’ Function Node & VertexTransforms SubGraph

Since we usually author a specific shader for use with DrawProcedural, I’d rather suggest using a simple Custom Function Node, in File mode, with something as simple as this:

StructuredBuffer<float4x4> _Matrices;

void InstanceData_float(uint instanceID, out float4x4 localToWorld)
{
    localToWorld = _Matrices[instanceID];
}

We can then pass it the InstanceID, and put its result behind a Predefined Keyword for the sake of making things work nicely in Shader Graph previews.

This also allows us to specifically carry instance data through Custom Interpolators, and should we really want to make a shader that adds a variant to handle data differently when applied to a renderer, we can also add the Keyword to the shader.

In other words, this moves things out of custom HLSL and into the graph itself. And this can also be nested in a Subgraph.

This is the technique I’ve been using in the examples.

Now to make things work properly with Lit materials, we also need to transform vertex normals, and should we use normal maps, vertex tangents.

This can be done with a Subgraph, like this:

Position

Normal

Tangent

Procedural

Note the documentation examples set things up for Unlit materials. Should you want to use a Lit material with any of the RenderPrimitives__() methods (except RenderMeshPrimitives), you will need to set vertex normals as well as positions.

This is also true for any other vertex data required, such as UVs for texture sampling, and tangents for normal maps.

In the HLSL include, you can add another buffer and method to get the normals.

StructuredBuffer<float3> _Normals;

void Normal_float(uint vertexID, out float3 Out)
{
    Out = _Normals[vertexID];
}

In the example C#, you add a buffer:

GraphicsBuffer meshNormals;

Initialize it with the mesh normals.

meshNormals = new GraphicsBuffer(GraphicsBuffer.Target.Structured, mesh.vertices.Length, 3 * sizeof(float));
meshNormals.SetData(mesh.normals);

And set the buffer on the render params material property block.

rp.matProps.SetBuffer("_Normals", meshNormals);

Then use a Custom Function Node to fetch the Normal.

I hope this already answered many of your questions.

I’ll follow up with more practical examples.

3 Likes

Hi @kripto289, thanks for your input.
Good news is, it’s already available.

There’s been confusion in the past, in the midst of misunderstanding around GPU Instancing with BiRP and other related topics, but the most important issues – like InstanceID and VertexID – have been addressed long ago.

The only thing related to those use cases that’s left to be added is the ability to disable interpolation.

If after reading this thread there are things you think are not possible or still complicated doing, I’m happy to hear about your use cases.

The URP Cookbook (Unity 6 edition) features a couple of real use cases, such as grass instancing or rendering compute based particles.

It’s also full of other great examples.

You can get the project here.

Instanced Grass Shader

Now that we’ve seen how we can easily offset vertex data without using the pragma, page 31, you can simplify the shader graph / custom function:

Replacing the following:

#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
StructuredBuffer<float2> PositionsBuffer;
#endif

float2 position;

void ConfigureProcedural()
{
#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED)
    position = PositionsBuffer[unity_InstanceID];
#endif
}

void ShaderGraphFunction_float(out float2 PositionOut)
{
    PositionOut = position;
}

with:

StructuredBuffer<float2> PositionsBuffer;

void ShaderGraphFunction_float(uint instanceID, out float2 PositionOut)
{
    PositionOut = PositionsBuffer[instanceID];
}

And change the shader graph like this:

ParticleFun

In a similar fashion, the shader used in the Particle Fun chapter can be recreated as a shader graph.

URP Cookbook: Compute shaders - Part 1: Particle fun

ParticleFun.hlsl

#ifndef PARTICLE_FUN
#define PARTICLE_FUN

struct Particle
{
    float3 position;
    float3 velocity;
    float life;
};

StructuredBuffer<Particle> particleBuffer;

void ParticleAttributes_float(uint instanceID, out float3 position, out float3 velocity, out float life)
{
    Particle particle = particleBuffer[instanceID];
    position = particle.position;
    velocity = particle.velocity;
    life = particle.life;
}

#endif

ParticleFun.shadergraph

That is all for now. I’ll follow up later.

4 Likes

Well done Fred! Very interesting paper :wink:

1 Like

Hi @kripto289,

thanks for your feedback.

I started a new thread to address the specifics on Shuriken Particles with Shader Graph.

I hope this will help.

1 Like

Being able to disable interpolation on custom interpolators is also something we’ve had requests about and is somehow related.

When generating procedural geometry such as strips, you may want to store some values in vertex data that you need uninterpolated in the fragment stage.

As announced here, starting with Unity 6000.3.0a1 you can now disable interpolation on Custom Interpolators.

5 Likes

Hi Fred,

Sorry for jumping on this thread, but I’ve been following your insights on Shader Graph and GPU instancing, and I was hoping you might be able to shed some light on a related issue I’ve been struggling with.

I’m currently trying to get Graphics.DrawMeshInstancedIndirect working within HDRP but I’ve hit a brick wall. The official Unity scripting example works fine in Built-in RP, but when applied to an HDRP project, the draw call executes without any errors or warnings yet nothing is rendered. I’ve double-checked my compute buffers, argument buffers, and shader setup, but HDRP seems to require something more which isn’t covered in the documentation.

I’ve searched through forums, discussions, and community posts but couldn’t find any HDRP-specific guidance for this. Since you’ve been involved in discussions about Shader Graph and GPU instancing, I was wondering if you might have any advice on how DrawMeshInstancedIndirect should be correctly set up for HDRP**?

1 Like

Hi,

are you using the same shader provided in the documentation?

If so, here’s what doesn’t with HDRP:

It’s using this pragma, which adds the keyword PROCEDURAL_INSTANCING_ON to the shader, and will automatically call the setup() function at the beginning of the vertex stage.

#pragma instancing_options procedural:setup

And in this function, you can see it’s fetching positions from a StructuredBuffer to set the unity_ObjectToWorld and unity_WorldToObject matrices.

void setup()
{
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
    float4 data = positionBuffer[unity_InstanceID];

    float rotation = data.w * data.w * _Time.y * 0.5f;
    rotate2D(data.xz, rotation);

    unity_ObjectToWorld._11_21_31_41 = float4(data.w, 0, 0, 0);
    unity_ObjectToWorld._12_22_32_42 = float4(0, data.w, 0, 0);
    unity_ObjectToWorld._13_23_33_43 = float4(0, 0, data.w, 0);
    unity_ObjectToWorld._14_24_34_44 = float4(data.xyz, 1);
    unity_WorldToObject = unity_ObjectToWorld;
    unity_WorldToObject._14_24_34 *= -1;
    unity_WorldToObject._11_22_33 = 1.0f / unity_WorldToObject._11_22_33;
#endif
}

In HDRP, you can access those matrices with Macros but they are read only.

So, what I’d suggest instead of overwriting those matrices, is to transform vertices from local to world.

It’s something I shall cover in more depth in a future post in the Shuriken Particles & Shader Graph thread.

1 Like

After reading URP cookbook, I have been testing interactive grass shader.
So I’m exciting about simplified custom function.
I’ve refactored my shader graph with new hlsl, thanks!

1 Like

I want to share a work around described in this thread.

I shall follow up later with more details from engineering on why this is not recommended, the HDRP team did this for a reason, and better options.

Hi @FredMoreau, you seem like a good candidate for answering this related question about ShaderGraph, procedural rendering, and single pass VR. Any chance you can have a look at it?

Hey, @FredMoreau, very helpful thread.

Is there a reason procedural rendering is only enabled for Graphics.RenderMeshIndirect()? I recently found that Graphics.RenderPrimitivesIndexedIndirect() does not call my procedural setup function. I do not need a mesh vertex buffer, so Graphics.RenderMeshIndirect() is wasteful for me, but I still require per-instance setup.

Hi,
can you provide more details?
Which Unity version and Render Pipeline you are using, is it a handwritten shader or shader graph, and do you have the #pragma in the shader?

#pragma instancing_options procedural:ProceduralSetup

And is the keyword INSTANCING_ON enabled on the material instance?

@FredMoreau Yes, I tested with a ShaderGraph shader in URP 2022.3.62 and 6000.2.9. I do have the pragma written in the shader (in a string custom function node). I have “Enable GPU Instancing” selected on the material in the inspector. However, neither INSTANCING_ON nor PROCEDURAL_INSTANCING_ON is present in material.shaderKeywords.

Worth noting, procedural instancing works perfectly fine for me by simply switching from Graphics.RenderPrimitivesIndexedIndirect() back to Graphics.RenderMeshIndirect(). The documentation you linked above seems to suggest procedural rendering only works for Graphics.RenderMeshIndirect().

Your comment led me to try something, though. Graphics.RenderPrimitivesIndexedIndirect() seems to work without issue after calling material.EnableKeyword("PROCEDURAL_INSTANCING_ON"); manually before rendering.

I also wanted to ask:
ShaderGraph adds:

float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float4 uv1 : TEXCOORD1;
float4 uv2 : TEXCOORD2;

to the vertex attributes, even though my vertex shader doesn’t use these (As far as I know, no vertex buffer is bound when using Graphics.RenderPrimitivesIndexedIndirect()). Is there a penalty for having these attributes when they’re redundant? Could these be removed?

As mentioned in this other post, note that ‘GPU Instancing’ is referring to “legacy” GPU Instancing, and not the more modern approach using the GPU Resident Drawer.

Hi, May I ask that ,how Shader Graph know how many instances to run? Cause in the shader graph ,I only see that the instenceCount is used as a paramter to decide the color. If I use mesh renderer and mesh filter, the instanceCount is determined by the mesh in the scene but not how many meshes are in the ComputeBuffer.

If you use MeshRenderers in the scene, you can’t tell from the shader how many instances are rendered. Also, as said in the doc:

When Unity uses dynamic instancing, instance IDs might not be consistent across multiple frames.

1 Like