Notifications
Article
Getting light information with a custom node in ShaderGraph
Updated 15 days ago
678
2
Custom Nodes are a formidable addition to ShaderGraph to extend its functionality
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:
Light mainLight = GetMainLight(); Color = mainLight.color; Direction = mainLight.direction; float4 shadowCoord; #ifdef _SHADOWS_ENABLED #if SHADOWS_SCREEN float4 clipPos = TransformWorldToHClip(WorldPos); shadowCoord = ComputeShadowCoord(clipPos); #else shadowCoord = TransformWorldToShadowCoord(WorldPos); #endif mainLight.attenuation = MainLightRealtimeShadowAttenuation(shadowCoord); #endif Attenuation = mainLight.attenuation;
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; return functionBody; }
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.

Ciro Continisio
Technical Evangelist - Educator
6
Comments
Brendan Bennett
4 days ago
Mr
(dammit enter button posted instead of giving me a new line) I'm getting some small error messages when making custom nodes; "does not have meta file". Using 2018.2 and getting SGE from the package manager (which puts it outside the project Assets folder, which I assume is the problem). Is there an official import process that will handle this? Trying to keep a clean error bar.
0