Shader Graph is a useful tool that allows you to easily achieve advanced expressions on a node-based basis, but in order to perform special drawings such as mathematical optimization, complex loop processing, and ray marching, you often use the "Custom Function" node to write HLSL code directly. However, there is a trap that as soon as you write custom HLSL, the material becomes incompatible with SRP Batcher, and the drawing load increases several to tens of times. In this article, we will explain in detail the failure mechanism of this constant buffer declaration and the correct coding technique to integrate custom HLSL while maintaining SRP Batcher's fast batching.

1. SRP Batcher prerequisites: Constant buffer (CBUFFER) mechanism

First, it is important to understand how SRP Batcher reduces draw calls to the utmost. In conventional rendering systems, each time an object is drawn, material parameters (color, tiling values, vectors, etc.) are individually bound and transferred from the CPU to the GPU. This was causing CPU overhead.

In contrast, SRP Batcher takes the approach of saving all material properties in advance in a memory area called a constant buffer (CBUFFER) on the GPU memory. Even when the material is switched, the CPU can continue drawing by simply sending a very light instruction to the GPU: ``Please switch the offset position (address) of CBUFFER for this material.'' This constant buffer must always be defined in the shader code in the following format:

CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;
    float4 _BaseMap_ST;
    float _Smoothness;
CBUFFER_END

The "iron rule" for SRP Batcher to work is that this block name must be "UnityPerMaterial" and that all properties that can be changed by the material are defined in this buffer.

2. Why CBUFFER breaks in Custom Function node

The biggest mistake that many developers make when writing HLSL code in Custom Function node is declaring and using shader property variables directly in the HLSL code.

For example, suppose you have defined a variable called `_Speed` in the Shader Graph properties window. Assume that you have written the following HLSL code inside the Custom Function node.

// Example of incorrect custom HLSL code
void ApplyWaveEffect_float(float3 InPos, out float3 OutPos)
{
 // Trying to directly access and declare shader properties defined on the Shader Graph side
 // Or declare another custom constant buffer yourself
 OutPos = InPos + sin(InPos.y * _Speed) * 0.1f;
}

When you compile this code, Unity's shader compiler detects that a global variable (or local variable) called `_Speed` is referenced in the Custom Function, but it is not properly included in the automatically generated `UnityPerMaterial` constant buffer block. As a result, `_Speed` is declared outside the constant buffer in the "Global Scope".

In this way, the moment a material's parameters are distributed inside and outside of `UnityPerMaterial`, SRP Batcher will mark the shader as "Incompatible" and exclude it from batch processing. The result is that draw batches are torn apart and draw calls are issued on a per-object basis, even if they use the same material.

3. Correct approach: Full value transfer via Inputs port

The solution to prevent this bug is very simple, but must be thorough. The key to this is ``In the HLSL of the Custom Function node, the properties (variables) of the material are not directly referenced, but all are received as arguments (Input ports)''.

Implementation steps:

  1. Preparing properties for the Shader Graph: Define the required variables (e.g. `Speed ​​(_Speed)`) in the properties window as usual.
  2. Configuring ports for the Custom Function node: Select the Custom Function node and open the Inspector settings window.
    • Add a new port to the Inputs list and name it `Speed` (type `Float`).
    • Add the required output ports to the Outputs list.
  3. Connect the node: On the Shader Graph, connect the property node (`Speed`) directly to the `Speed` input port of the Custom Function node with a wire.
  4. HLSL code fixes: In the HLSL code, use the connected argument (`Speed`) as is and completely eliminate writing the property name (`_Speed`) with a prefix of underscore (`_`) directly.
// Example of correct custom HLSL code
void ApplyWaveEffect_float(float3 InPos, float Speed, out float3 OutPos)
{\
    // Use 'Speed' received as argument (not _Speed)
 OutPos = InPos + sin(InPos.y * Speed) * 0.1f;
}

By taking this approach, the declaration of the material variable `_Speed` itself will be automatically and correctly output by the Shader Graph generator into the `UnityPerMaterial` CBUFFER block. The value is simply passed to the Custom Function node as an "argument", allowing for super-fast, fully batched drawing while maintaining 100% constant buffer conformance.