Custom Nodes are a formidable addition to ShaderGraph to extend its functionality
UPDATE (15/06/19): this post points to resources that are obsolete. In newer versions of Shader Graph (i.e. the version that is out of Preview) the APIs to create custom nodes in C# have been removed. They have been replaced with a node already present in the graph (called Custom Function Node) that allows you to point to a block of HLSL code, to basically obtain the same functionality while at the same time having code that can be easily reused. Check out a new post I have written on the subject.
The first task of any shader trying to redefine the lighting function in ShaderGraph is getting light and shadow information from the scene. Unfortunately, right now* there is no way of getting the main light without coding. One valid method is to approximate the direction of the light with a Property of type Vector3, unexposed, and then set it from a script on a GameObject to the direction of the main Directional light in the scene. Because it's unexposed it's shared between all Materials using that shader: you just need to set it once, and all instances of that shader will get it.
This is a fair trick and it works quite well, but always requires you to set that specific GameObject, the script, etc. I decided instead to use a some coding and made a custom node instead, which gives me a few extra information to work with. I used this node in a custom toon shader I am working on.
* such a node might come in the future, as a native part of ShaderGraph
Enter Custom Nodes
Writing a custom node for ShaderGraph is quite simple. Since ShaderGraph is still in Preview, there are no docs yet (except this) but I posted the node online so you can grab it and modify it. Matt Dean also shared a good blog post, but it's ageing fast (the code in it is already old). However, it's good for learning some concept behind it.
You can find my node here on Gist. Just put the code in a C# file and drop it in your project, and you will be able to use the node in your graphs.
The node looks like this:
It has 3 outputs, and one input. For the input, I actually don't need to connect anything: the information on the World Space position of the vertex is provided automatically by the binding Binding.WorldSpacePosition (line 80 on Gist). For the outputs, I usually run the Direction into a Normalize node because many operations need the normalised vector, but this is really up to you and what you are doing with it.
The node is a C# class inheriting from CodeFunctionNode. We use the attribute [Title("Custom", "Main Light")] to determine the name of the node, and the category you will find it in when in the graph.
The HLSL code
At its heart, a custom node has a string which contains some HLSL code, which will become part of your shader when the graph is compiled into code. In my case, this is what it looks like:
How did I know what to get? Well, it was "easy": I dug into the code of the LWRP, and found the functions which were generating light and shadows. From those, as you can see in the code above, I get 3 values to use in my graph: light colour (Vec3), light direction (Vec3), and light attenuation (a float, which changes per fragment and expresses the light/darkness amount on that specific bit). They become the outputs of my node.
Because this data is not available in the graph (the graph has no main light, after all) the preview of the shader in the graph window might fail. As such, we need to add a little trick: we create 2 strings instead of one, and we use one of them (the above) when the shader is used in the scene, and one of them when the shader is previewed in the graph. In this second string, we hardcode some data, just for the purpose of being able to visualise the shader in the preview:
Color = 1;
Direction = float3(-0.5, -.5, 0.5);
Attenuation = 1;
Just something to keep in mind when you're using data from the scene.
Defining the ports
Another key part of the script is the function called CustomFunction, which defines the ports:
private static string CustomFunction(
[Slot(0, Binding.None)] out Vector3 Direction,
[Slot(1, Binding.None)] out Vector1 Attenuation,
[Slot(2, Binding.None)] out Vector3 Color,
[Slot(3, Binding.WorldSpacePosition)] Vector3 WorldPos)
//Default values are needed or Unity complains that the Vec3s are not initialised
//They won't be zero in the final shader
Direction = Vector3.zero;
Color = Vector3.zero;
As you can see, both in and out ports are defined by the [Slot] attribute.
Summing it all up
I won't go over other parts of the code as I think they are pretty self-explanatory. As mentioned before, you can find the node on Gist. Feel free to use it in your graphs, but remember that these APIs, at the time of writing, are still in Preview. This means that they might change in the future. For questions, hit me up on Twitter as usual.
One final tip. Custom nodes are not only a way to expose things not normally available in ShaderGraph, but you can also use them to optimise performance-critical calculations in your shaders. Once you write that code as a custom node, artists will be able to use it in their graphs.