As I’ve mentioned before, I like writing shaders as code. Well, most of the time. Some items are cleaner as code, some are cleaner in a shader graph. Today we’ll be exploring how to combine the two. Meaning, how to write HLSL code, that can be utilized as a custom node within a shader graph. Custom shader graph nodes are incredibly powerful if you are in a team that likes working in graphs. Especially if your creative types are only interested in working in a graph, but need some high-horsepower effects.
Today’s post will cover the two main reasons you might want a custom node, and how to make one. To illustrate the how-to, I walk through the example of making a Polar to Cartesian coordinate converter. As an added bonus, I’ve published that node to the Unity Asset Store here.
This article may contain affiliate links, mistakes, or bad puns. Please read the disclaimer for more information.
Utility Nodes
The most common reason for creating custom shader graph nodes is to create some utility node that your shader graph creators will need to use repeatedly. One example (which we’re going to make today below) is a Cartesian Coordinate node. Which is a node converting Polar Coordinates in to Cartesian. Now, the UVs that you get as inputs to any shader graph are already in Cartesian space, so why would you need this? Because you’ve converted them to polar coordinates to do something mathy to them (twist, fish-eye, etc.) and you need to convert them back to sample your texture. For some reason the shader graph comes with a Polar Coordinate node (Cartesian to Polar converter), but not one to undo that. The example below explores how to remedy that.
Magical Surface Shader Hack
The other reason to create a custom shader graph node is what I affectionately call my “Magical Surface Shader Hack”. As I discuss in my intro to shader graph tutorial, scriptable build pipeline does away with the Surface Shader concept. If you don’t remember what that was (more info here) that’s a shader you could code, that became a stand alone method Unity would call and then do lighting and such on. It was a way to write the color-customization part of the shader, without having to do all the lighting math. With SRP, if you are writing a code shader, you have to write the whole thing (though you can call some Unity helper methods).
So how am I hacking a surface shader in here? Well, within the shader graph, if you create a lit shader graph, the master node is the part that handles all the lighting. That means that your graph feeding into the master node, is essentially a node-based surface shader. By making that a custom node driven by HLSL, you are now able to effectively write an HLSL surface shader, that Unity lights for you. Oh the excitement!
Cartesian Coordinate Example
As mentioned earlier, a converter from Polar Coordinates to Cartesian is a likely real life scenario where you might need a utility node. There are actually two ways to accomplish this. One is to create a node-based sub graph. With this, you do all your logic using shader graph nodes, but do that within an asset that simply defines inputs and outputs arbitrarily. After creating it, you can insert this sub graph within other graphs (sub or top level). The other way, is to utilize a custom node that executes your own HLSL. If you choose the latter (custom node) you’ll generally still wrap it in a sub graph. The reason is that you can’t actually reuse a custom node. You have to create it inside a graph, and it’s a pain to set up. So once made, it’s best to wrap it in a sub graph, so you can reuse it easily.
We’re actually going to do both methods here. You need to utilize sub graphs in either case, and it’s nice to explore the logic of the shader in both settings.
Sub Graph Setup
To start off either plan (HLSL or nodes), we need to create a Sub Graph. This is done by right clicking in your project, or going to Assets->Create, and finding “Sub Graph” within the “Shader” heading. By default, this is created with no inputs, and a single Vector4 output. Step one is to edit the output to be a Vector2 (and I generally rename it from “Out_Vector4” to “Out”). This is done via the gear icon on the output node.
From there we need to add inputs. This is done on the blackboard within the graph. Since this is going to be a reverse of the Polar Coordinate node, we need all the same inputs, with the exception of the coordinates themselves (they take in UV, and spit out Polar, we’ll take in Polar, and spit out UV). Note that each input has a default. This is what will be fed in if no one connects to that node.
- PolarCoords – Vector2, default: (0,0)
- Center – Vector2, default: (0.5, 0.5)
- Radial Scale – Vector1, default: 1
- Length Scale – Vector1, default: 1
Where they came up with the name Length scale, I don’t know. How about “Angle Scale” since it scales the angle? Oh well.
At this point you have the baseline for either node. If you are wanting to follow this tutorial and implement both, I’d recommend hopping over to your file browser (outside unity) and copying this sub graph. You can’t copy/paste inputs to a graph from one to the other, but you can copy the graph on disk.
How To Set Menu Path
Most of what I cover in this sample is just a hands-on version of information covered in the shader graph docs. There is one item, however, you’ll likely want to do that isn’t covered in the docs or the graph UI. That is setting the menu path. The menu path is what controls where your node shows up in the “Create Node” menu. By default, all user-created sub graphs show up in the category “Sub Graphs”. We’re classier than that, so we’ll change it.
To set your sub graphs menu location, you have to open the sub graph asset in a text editor. Near the bottom, you’ll see “m_Path”: “Sub Graphs”. Simply edit the “Sub Graphs” to be whatever you want. In my case, I’m changing it to “UV”. You can add a slash for hierarchy, such as “Artistic/Utility”.
Note that if you start the path with “Hidden/”, then the node will not show up in the “Create Node” menu via search or browsing. This won’t make it unusable, as you can still drag it into a graph.
Node Based Conversion
Let’s walk through the graph that un-does Unity’s Polar Coordinates node. First, we have to take the PolarCoords and do some magic number multiplying because Unity has some magic numbers within it’s generated shader. We multiply the radius by 0.5, and the angle by 2 pi. Then we feed that into a split node to begin our work.
From there we divide both channels by their respective scale factors, and feed the angle into both Sine and Cosine nodes.
We multiply both of those trigonometry outputs by our un-scaled radius, add back our center, and we’re done.
With that, just hit Save Asset (note, you should do this a lot. I’ve frequently been puzzled by a non-working shader only to realize I didn’t click that button. Ctrl+s does not actually save node graphs!). This gives you a Sub Graph you can use any time you need to undo Unity’s Polar Coordinate Node.
Custom HLSL Based Conversion
Now on to the real point of this tutorial. Implementing the above sub graph, but using a custom node with our own custom HLSL.
Start with that same baseline sub graph we created earlier. Now instead of a multiply node being your first added node, create a “Custom Function” node. This is actually the only node you’ll need. Use the gear icon to add inputs and outputs to match the sub graph.
To actually enter your code, you have two options. “String” means you just type the HLSL into a box on the node. “File” means you reference an external HLSL file. 99% of the time you’ll want to do File. The interface to enter a string is fairly hard to use if you’re trying to enter more than one line of code.
The file is any text file. Within unity, there are a couple text files you could create in the “Create” menu, but most come with a lot of junk you’ll have to clear out. I’d recommend creating a basic text file outside Unity, and either keeping it as .txt or changing it to .hlsl.
Note that around version 6.something (which I believe correlates to 2019.2), the changed File from being a path, to being a Unity object. It’s a change for the better, but it means custom nodes are not compatible across this change (old ones won’t work in the new graph).
#ifndef CARTESIAN_INCLUDE
#define CARTESIAN_INCLUDE
void CartesianCoords_float(float2 PolarCoords, float2 Center, float RadialScale, float LengthScale, out float2 UV)
{
float2 adjustedCoord = PolarCoords * float2(0.5, 6.28);
adjustedCoord = adjustedCoord / float2(RadialScale, LengthScale);
float2 result;
result.x = sin(adjustedCoord.y) * adjustedCoord.x;
result.y = cos(adjustedCoord.y) * adjustedCoord.x;
UV = result + Center;
}
#endif //CARTESIAN_INCLUDE
Usage Sample
The tutorial is largely done at this point, but I decided to throw in a simple shader to show you a use case for this Cartesian->Polar->Cartesian logic. Here I’ve taken the output of Polar Coordinates, fed the Y value (the angle) directly through to the Cartesian Coordinates node. But for the X component (the radius), I cubed it.
Now, this isn’t exactly a fisheye shader. Ideally you’d have a bit more complex curve to the curvy part, and a cleaner edge to where the corners return to a normal state. But that’s more expensive and complex to create. Since this is a little add on at the end of the tutorial, I went with the super simple method.
Conclusion
Creating custom shader graph nodes is a great way to up the productivity of whoever in your organization is creating shader graphs. You can put in complex HLSL shader snippets, or simply wrap something you’ll reuse a lot in a sub graph.
Do something. Make progress. Have fun.