Using Shader Graph 6.6's new functionality to expand the node library
Previously, I wrote a short article on how to get lighting information in Shader Graph, for the purpose of creating toon shaders (like my Zelda-inspired one) and the like. However, with Shader Graph getting out of Preview state (in the Unity 2019.1 lifecycle), the APIs I previously mentioned are no longer available.
Instead, Shader Graph now has a pre-made node called Custom Function, which wraps custom HLSL code and allows it to interact with the rest of the graph. This is a better solution than before since there is no need to create a custom C# class, leading to more reusable HLSL code.
It is found under Utility > Custom Function:
Warning: the Custom Function node is available only as of Shader Graph 6.6. If you are on a previous version, you will not see it in the menu. This post focuses on the Lightweight Render Pipeline, but the same process works for HDRP too.
Assets: To see an example of the setup shown below, you can download this mini reproduction project (provided as-is).
Setting up the node
The node will initially be useless, and have no input or output ports. Configuring it is very simple, and it's composed of two steps. By clicking on the cog in the corner of the node, you will see the dropdown menu that allows you to: 1) define the ports and 2) point the node to either a string, or an external file.
In the screenshot, you can see how I have defined a Vector3 input (ObjPos) and 3 outputs of different types (Direction, Color and ShadowAttenuation):
On the left of this list you can also rename both inputs and outputs, a very important step since the names have to match with the names of variables in the HLSL code.
The second step is to provide the code. You can choose it to be a String, and paste it directly in the text box, or a File, which will allow you to define Name and Source. Name is the name of the function in the HLSL code you will call, and Source is the path to the file itself.
The name of the function has to match the one in the code (see below), and the path starts from Assets and is absolute.
Writing the code
I have no tutorial here since what you write inside the HLSL is up to you, obviously. However, there are a few guidelines. Let's take this code as an example:
void LWRPLightingFunction_float (float3 ObjPos, out float3 Direction, out float3 Color, out float ShadowAttenuation)
//Actual light data from the pipeline
Light light = GetMainLight(GetShadowCoord(GetVertexPositionInputs(ObjPos)));
Direction = light.direction;
Color = light.color;
ShadowAttenuation = light.shadowAttenuation;
//Hardcoded data, used for the preview shader inside the graph
//where light functions are not available
Direction = float3(-0.5, 0.5, -0.5);
Color = float3(1, 1, 1);
ShadowAttenuation = 0.4;
The purpose of this code is to tap into the lighting pipeline and return 3 values (Direction, Color and ShadowAttenuation) by calling some pipeline functions (GetMainLight, GetShadowCoord, etc.).
First, the name of the function and the input/output have to match what you defined in the node. The function name actually incorporates the exit type, so in my case is LWRPLightingFunction_float . The function itself is of type void.
Second, every output parameter has to use the out keyword. You want - at some point - to assign a value to each of your output parameters.
Tapping into the pipeline
And that's it, really. The only thing worth noticing here, is that if your custom HLSL node contains pipeline functions (like mine) it will generally work in the scene view, but it won't compile in the graph itself, leading to something like this:
This is because the preview shader used in the graph has no connection to the whole pipeline, and is not able to access all of its functions. You can see the compilation errors in the Console.
To fix this, you can create branches in the code using #ifdef directives, to provide code that is graph-safe. In my case, I check for a constant called LIGHTWEIGHT_LIGHTING_INCLUDED which is exactly the library I am using for my functions, and - if not present - I output from the node some fake, hardcoded data (see comments). That fixes the issue, and now the custom node works in both scenarios.
Using the Custom Function node is a very powerful tool in your inventory to do something special in your shaders, which in turn will allow your game to obtain the look you really want. It's also one of the easiest ways to start customising the rendering using the SRPs.