Search Unity

Shader Graph Custom Node API: Using the Code Function Node

March 27, 2018 in Engine & platform | 6 min. read
Share

Is this article helpful for you?

Thank you for your feedback!

With the recent addition of Shader Graph, it’s now easier than ever to create custom shaders in Unity. However, regardless of how many different Nodes we offer by default, we can’t possibly cater for everything you might want to make. For this reason, we have developed a Custom Node API for you to use to make new Nodes in C#. This allows you to extend Shader Graph to suit your needs.

In this blog post, we will take a look at one of the ways you can do this in Unity 2018.1 beta. The simplest way to create custom Nodes that create shader functions is the Code Function Node. Let’s take a look at how to create a new Node using this method.

Let’s start by creating a new C# script. For this example, I have named the script MyCustomNode. To use the Code Function Node API you need to include (or add the class to) the namespace UnityEditor.ShaderGraph and inherit from the base class CodeFunctionNode.

using UnityEngine;
using UnityEditor.ShaderGraph;

public class MyCustomNode : CodeFunctionNode
{

}

The first thing you will notice is that MyCustomNode is highlighted with an error. If we hover over the message we see that we need to implement an inherited member called GetFunctionToConvert. The base class CodeFunctionNode handles most of the work that needs to be done to tell the Shader Graph how to process this Node, but we still need to tell it what the resulting function should be.

The method GetFunctionToConvert uses Reflection to convert another method into an instance of MethodInfo that CodeFunctionNode can convert for use in Shader Graph. This simply allows us to write the shader function we want in a more intuitive way.

For more information on Reflection see Microsoft’s Programming Guide on Reflection (C#).

Add the namespace System.Reflection and the override function GetFunctionToConvert as shown in the example below. Note the string that reads MyCustomFunction. This will be the name of the function that is written into the final shader. This can be named whatever you wish to suit the function you are writing and can be anything that doesn’t begin with a numeric character, but for the rest of this article we will assume its name is MyCustomFunction.

using UnityEngine;
using UnityEditor.ShaderGraph;
using System.Reflection;

public class MyCustomNode : CodeFunctionNode
{
	protected override MethodInfo GetFunctionToConvert()
	{
		return GetType().GetMethod("MyCustomFunction",
			BindingFlags.Static | BindingFlags.NonPublic);
	}
}

Now that our script errors are resolved, we can start working on the functionality of our new Node! Before we continue we should name it. To do this add a public constructor for the class with no arguments. In it, set the variable name to a string that contains the title of your Node. This will be displayed in the title bar of the Node when it appears in a graph. For this example, we will call the Node My Custom Node.

using UnityEngine;
using UnityEditor.ShaderGraph;
using System.Reflection;

public class MyCustomNode : CodeFunctionNode
{
	public MyCustomNode()
	{
		name = "My Custom Node";
	}

	protected override MethodInfo GetFunctionToConvert()
	{
		return GetType().GetMethod("MyCustomFunction",
			BindingFlags.Static | BindingFlags.NonPublic);
	}
}

Next, we will define the Node’s function itself. If you are familiar with Reflection you will notice that the method GetFunctionToConvert is attempting to access a method in this class called MyCustomFunction. This is the method that will define the shader function itself.

Let’s create a new static method of return type string with the same name as the string in the method GetFunctionToConvert. In this case it’s MyCustomFunction. In the arguments of this method we can define what Ports we want the Node to have. These will map directly to the arguments in the final shader function. We do this by adding an argument of a type supported in Shader Graph with a Slot Attribute. For now, let’s add two arguments of type DynamicDimensionVector called A and B and another out argument of type DynamicDimensionVector called Out. Then we will add a default Slot Attribute to each of these arguments. Each Slot Attribute needs a unique index and a binding, which we will set to None.

	static string MyCustomFunction(
		[Slot(0, Binding.None)] DynamicDimensionVector A,
		[Slot(1, Binding.None)] DynamicDimensionVector B,
		[Slot(2, Binding.None)] out DynamicDimensionVector Out)
{

}

For a full list of types and bindings that are supported see the CodeFunctionNode API documentation on GitHub.

In the following method we will define the contents of the shader function in the return string. This needs to contain the braces of the shader function and the HLSL code we wish to include. For this example let’s define Out = A + B;. The method we just created should look like this:

	static string MyCustomFunction(
		[Slot(0, Binding.None)] DynamicDimensionVector A,
		[Slot(1, Binding.None)] DynamicDimensionVector B,
		[Slot(2, Binding.None)] out DynamicDimensionVector Out)
	{
		return
			@"
{
	Out = A + B;
}
";
	}
}

This is exactly the same C# code that is used in the Add Node that comes with Shader Graph.

There is one last thing we need to do before we have a working Node: tell it where to appear in the Create Node Menu. We do this by adding the Title Attribute above the class.This defines a string array that describes where it should appear in the hierarchy in the menu. The last string in this array defines what the Node should be called in the Create Node Menu. For this example, we will call the Node My Custom Node and place it in the folder Custom.

[Title("Custom", "My Custom Node")]
public class MyCustomNode : CodeFunctionNode
{

Now we have a working Node! If we return to Unity, let the script compile then open Shader Graph we will see the new Node in the Create Node Menu.

Create an instance of the Node in the Shader Graph. You will see it has the Ports we defined with the same names and types as the arguments to the MyCustomFunction class.

Now we can create all kinds of different Nodes by using different Port types and bindings. The return string of the method can contain any valid HLSL in a regular Unity shader. Here is a Node that returns the smallest of the three input values:

	static string Min3(
		[Slot(0, Binding.None)] DynamicDimensionVector A,
		[Slot(1, Binding.None)] DynamicDimensionVector B,
		[Slot(2, Binding.None)] DynamicDimensionVector C,
		[Slot(3, Binding.None)] out DynamicDimensionVector Out)
	{
		return
			@"
{
	Out = min(min(A, B), C);
}
";
		}
}

And here is a Node that inverts normals based on a Boolean input. Note in this example how the Port Normal has a binding for WorldSpaceNormal. When there is no Edge connected to this Port it will use the mesh’s world space normal vector by default. For more information see the Port Binding documentation on GitHub. Also note how when using a concrete output type like Vector 3 we have to define it before we return the shader function. Note that this value is not used.

	static string FlipNormal(
		[Slot(0, Binding.WorldSpaceNormal)] Vector3 Normal,
		[Slot(1, Binding.None)] Boolean Predicate,
		[Slot(2, Binding.None)] out Vector 3 Out)
	{
		Out = Vector3.zero;
		return
			@"
{
	Out = Predicate == 1 ? -1 * Normal : Normal;;
}
";
		}
}

Now you are ready to try making Nodes in Shader Graph using Code Function Node! But this is, of course, just the beginning. There is much more you can do in Shader Graph to customize the system.

Stay tuned to this blog and talk to us on the Forums!

March 27, 2018 in Engine & platform | 6 min. read

Is this article helpful for you?

Thank you for your feedback!