Kyle Banks

Unity: Working with Custom HLSL Functions in Shader Graph

Written by @kylewbanks on Aug 5, 2020.

Shader Graph is a wonderful tool for authoring shaders without having to get into the complexities of writing shader code yourself. It can really simplify your workflow, so if you’ve been on the fence about it for whatever reason I’d highly recommend checking it out. Here’s a two part tutorial I wrote if you’re not sure where to start, and there are many more out there as well.

As great as Shader Graph is, Unity is obviously unable to pack every function you could ever want or need into their graph. It’s just not practical to expect that the tool is going to be able to solve every use case that developers can come up with, which means at some point you might end up needing to write your own functions.

Another situation which arose for me is that I needed to adapt some of my own Shader Graph logic to be reusable both within my Shader Graph shaders, but also in a legacy shader that wasn’t created with Shader Graph. Rather than maintaining two separate copies of the same core shader logic, or risk rewriting the legacy shader, it made much more sense to rewrite that bit of logic that had previously been composed in Shader Graph into a modular HLSL function, which can be imported to Shader Graph and also called from the legacy shader.

Luckily Unity makes it relatively easy to import custom functions into your graph using the aptly named Custom Function node. This node can be used to bring your own custom HLSL code into Shader Graph in one of two ways:

  1. As a string, where you write or paste the code within the Shader Graph editor.
  2. As a file, where you tell Shader Graph where the file lives and the name of the function you want to use.

For this tutorial we’ll use the second option where we write our function in a .hlsl file and import it into the graph. This is going to be much more maintainable in the long run, and allow us to import the HLSL source into other shaders, so I’d recommend this approach in general.

Writing an HLSL Function for Shader Graph

The function we’ll write is going to be a simple grayscale implementation, which takes a Color or Vector3 as input and outputs a grayscale float. Basically it just sums the R/G/B channels and takes the mean by multiplying by 0.33 (or dividing by 3). The implementation is simplistic and isn’t the best way to create a grayscale shader, but it’s just an example so we can see how this all works.

Here’s what it might look like in Shader Graph:

An example of how you might implement a grayscale function in Shader Graph

Let’s convert this to an HLSL function and import it into Shader Graph using the Custom Function node. There are a couple requirements to be aware of for the functions we write when importing them into Shader Graph:

  1. The name of the function must contain the precision you want to use as a suffix, using the format of FunctionName_precision. The available precision levels and suffixes are _float and _half.
  2. The function must not have a return value. Instead, you define the output values as input parameters to your function using the out keyword.

Let’s start by creating an .hlsl file in a Unity project, somewhere underneath the Assets directory. Unity doesn’t provide this as an option in the editor Create menu, so the quickest way is to just create a new file in your code editor and give it the .hlsl extension.

Creating an HLSL file in VSCode.

Inside let’s start by defining the function, keeping in mind the two requirements above:

#ifndef GRAYSCALE_INCLUDED
#define GRAYSCALE_INCLUDED

void Grayscale_float(float3 input, out float output)
{

}

#endif // GRAYSCALE_INCLUDED

Here we’ve defined a Grayscale function with float precision by naming it Grayscale_float. It takes a float3 as input to represent the R/G/B channels of the color, and uses the out keyword to define a float as output.

Now we can quickly rewrite the implementation before importing back into Shader Graph:

void Grayscale_float(float3 input, out float output)
{
    float r = input[0];
    float g = input[1];
    float b = input[2];
    output = (r + g + b) * 0.33;
}

Again this is a simple demonstration, but we’re just going to grab each of the R/G/B values, sum them up and then take the mean. We assign this to the output variable which the caller, Shader Graph in this case, will be able to access.

Importing HLSL into Shader Graph

Now that we have some HLSL code which satisfies Shader Graph’s requirements we’re able to import this code into a Custom Function node and write it up as we would any other node.

Let’s start by creating or opening a Shader Graph file and adding a Custom Function node. Click the little cogwheel in the top right corner and you’ll see you’re able to define some inputs and outputs, as well as specify the file location and function name.

Go ahead and define an input Vector3 and output Vector1, and locate your HLSL file for the Source field. For the Name field you enter the name without the precision suffix, so in this case we’ll just enter Grayscale:

Creating a custom function node in Shader Graph.

A quick note on the inputs and outputs: the names of the inputs and outputs don’t need to match what you defined in the HLSL function, only the type and order are important. For example consider if we had a function that looked like so:

void Example_float(float in1, float2 in2, float3 in3, out float3 out3, out float out1)

In this case we’d need to define the Inputs in Shader Graph in the same order (Vector1, Vector2, Vector3) as well as the outputs (Vector3, Vector1).

Anyways, now you can wire the node up like any other by dragging the inputs and outputs to connecting nodes:

Using a Custom Function node in Shader Graph

You’ll see that the preview window works as well, and from here on it really is just like any other node. You can continue to make modifications to your HLSL code and see them updated in Shader Graph in real time, add more inputs and outputs, and so on.

You can also integrate your function into other shaders by simply including your HLSL file and calling it like any other function. For example:

#include "Assets/Rendering/Grayscale.hlsl" 

float4 frag(VertextOutput IN) : SV_TARGET
{
	float3 color = _Albedo;

	float grayscale;
	Grayscale_float(color, grayscale);
	return grayscale;
}

Pretty handy! One final tip: the Shader Graph documentation typically includes source code to demonstrate how each node is implemented. If you’re rewriting logic from Shader Graph in HLSL and you’re not quite sure how to go about it, checkout the documentation for each node and you’ll have a solid starting point.

Let me know if this post was helpful on Twitter @kylewbanks or down below!