With the release of Unity Editor 2019.1, the Shader Graph package officially came out of preview! Now, in 2019.2, we’re bringing even more features and functionality to Shader Graph.

What’s Changed in 2019?

Custom Function and Sub Graph Upgrades

To maintain custom code inside of your Shader Graph, you can now use our new Custom Function node. This node allows you to define your own custom inputs and outputs, reorder them, and inject custom functions either directly into the node itself, or by referencing an external file.

Sub Graphs have also received an upgrade: you can now define your own outputs for Sub Graphs, with different types, custom names, and reorderable ports. Additionally, the Blackboard for Sub Graphs now supports all data types that the main graph supports.

Color Modes and Precision Modes

Using the Shader Graph to create powerful and optimized shaders just got a little easier. In 2019.2, you can now manually set the precision of calculations in your graph, either graph-wide or on a per-node basis. Our new Color Modes make it fast and easy to visualize the flow of Precision, the category of nodes, or display custom colors for your own use!

See the Shader Graph documentation for more information about these new features.

Sample Project

To help you get started with the new custom function workflow, we’ve created an example project together with step-by-step instructions. Download the project from our repository and follow along! This project will show you how to use the Custom Function node to write custom lighting shaders for the Lightweight Render Pipeline (LWRP). If you want to follow along using a fresh project, make sure you’re using the 2019.2 Editor and LWRP package version 6.9.1 or higher.

Getting Data from the Main Light

To get started, we need to get information from the main light in our Scene. Start by selecting Create > Shader > Unlit Graph to create a new Unlit Shader Graph. In the Create Node menu, locate the new Custom Function node, and click the gear icon on the top right to open the node menu.

In this menu, you can add inputs and outputs. Add two output ports for Direction and Color, and select Vector 3 for both. If you see an “undeclared identifier” error flag, don’t be worried; this will go away when we start to add our code. In the Type dropdown menu, select String. Update your function name — in this example, we’re using “MainLight”. Now, we can start adding our custom code in the text box.

First, we’re going to use a flag called #ifdef SHADERGRAPH_PREVIEW . Because the preview boxes on nodes don’t have access to light data, we need to tell the node what to display on the in-graph preview boxes. #ifdef tells the compiler to use different code in different situations. Start by defining your fallback values for the output ports.

#if SHADERGRAPH_PREVIEW Direction = half3(0.5, 0.5, 0); Color = 1; 1 2 3 #if SHADERGRAPH_PREVIEW Direction = half3 ( 0.5 , 0.5 , 0 ) ; Color = 1 ;

Next, we’ll use #else to tell the compiler what to do when not in a preview. This is where we actually get our light data. Use the built-in function GetMainLight() from the LWRP package. We can use this information to assign the Direction and Color outputs. Your custom function should now look like this:

#if SHADERGRAPH_PREVIEW Direction = half3(0.5, 0.5, 0); Color = 1; #else Light light = GetMainLight(); Direction = light.direction; Color = light.color; #endif 1 2 3 4 5 6 7 8 #if SHADERGRAPH_PREVIEW Direction = half3 ( 0.5 , 0.5 , 0 ) ; Color = 1 ; #else Light light = GetMainLight ( ) ; Direction = light . direction ; Color = light . color ; #endif

Now, it’s a good idea to add this node to a group so that you can mark down what it’s doing. Right-click the node, select Create Group from Selection, and then rename the group title to describe what your node is doing. Here we’ve entered “Get Main Light”.

Now that we have our light data, we can calculate some shading. We’re going to start with a standard Lambertian lighting, so let’s take the dot product of the world normal vector and the light direction. Pass it into a Saturate node, and multiply it by the light color. Plug this into the Color port of the Unlit Master node, and your preview should update with some custom shading!

Using the Custom Function File Mode

Since we now know how to get light data using the Custom Function node, we can expand on our function. Our next function gets attenuation values from the main light in addition to the direction and color.

As this is a more complicated function, let’s switch to file mode, and use an HLSL include file. This lets you author more complicated functions in a proper code editor before injecting it into the graph. This also means that we have one unified location to debug the code from.

Start by opening the CustomLighting include file in the Assets > Include folder of the project. For now, we’ll only focus on the MainLight_half function. The function looks like this:

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten) { #if SHADERGRAPH_PREVIEW Direction = half3(0.5, 0.5, 0); Color = 1; DistanceAtten = 1; ShadowAtten = 1; #else #if SHADOWS_SCREEN half4 clipPos = TransformWorldToHClip(WorldPos); half4 shadowCoord = ComputeScreenPos(clipPos); #else half4 shadowCoord = TransformWorldToShadowCoord(WorldPos); #endif Light mainLight = GetMainLight(shadowCoord); Direction = mainLight.direction; Color = mainLight.color; DistanceAtten = mainLight.distanceAttenuation; ShadowAtten = mainLight.shadowAttenuation; #endif } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void MainLight_half ( float3 WorldPos , out half3 Direction , out half3 Color , out half DistanceAtten , out half ShadowAtten ) { #if SHADERGRAPH_PREVIEW Direction = half3 ( 0.5 , 0.5 , 0 ) ; Color = 1 ; DistanceAtten = 1 ; ShadowAtten = 1 ; #else #if SHADOWS_SCREEN half4 clipPos = TransformWorldToHClip ( WorldPos ) ; half4 shadowCoord = ComputeScreenPos ( clipPos ) ; #else half4 shadowCoord = TransformWorldToShadowCoord ( WorldPos ) ; #endif Light mainLight = GetMainLight ( shadowCoord ) ; Direction = mainLight . direction ; Color = mainLight . color ; DistanceAtten = mainLight . distanceAttenuation ; ShadowAtten = mainLight . shadowAttenuation ; #endif }

This function includes some new input and output data, so let’s go back to our Custom Function node and add them. Add two new outputs for DistanceAtten (distance attenuation) and ShadowAtten (shadow attenuation). Then, add the new input for WorldPos (world position). Now that we have our inputs and outputs, we can reference the include file. Change the Type dropdown to File. In the Source input, navigate to the include file, and select the Asset to reference. Now, we need to tell the node which function to use. In the Name box, we’ve entered “MainLight”.

You’ll notice that the include file has _half at the end of the function name, but our name option doesn’t. This is because the Shader Graph compiler appends the precision format to each function name. Since we’re defining our own function, we need the source code to tell the compiler which precision format our function uses. In the node, however, we only need to reference the main function name. You can create a duplicate of the function that uses ‘float’ values to compile in float precision mode. The ‘Precision’ Color Mode lets you easily track the precision set for each node in the graph, with blue representing float and red representing half.

We’ll probably want to use this function again somewhere else, and the easiest way to make this Custom Function reusable is to wrap it in a Sub Graph. Select the node and its group, and then right-click to find Convert to Sub-graph. We’ve called ours “Get Main Light”. In the Sub Graph, simply add the required output ports to the Sub Graph output node, and plug the node’s output into the Sub Graph output. Next, we’ll add a world position node to plug into the input.

Save the Sub Graph, and go back to our unlit graph. We’re going to add two new multiply nodes to our existing logic. First, multiply the two attenuation outputs together. Then, multiply that output by the light color. We can multiply this by NdotL from earlier to properly calculate attenuation in our basic shading.

Creating a Direct Specular Shader

The shader we’ve made is great for matte objects, but what if we want some shine? We can add our own specular calculations to our shader! For this step, we’ll use another Custom Function node wrapped in a Sub Graph, called Direct Specular. Take a look at the CustomLighting include file again, and see that we’re now referencing another function from the same file:

void DirectSpecular_half(half3 Specular, half Smoothness, half3 Direction, half3 Color, half3 WorldNormal, half3 WorldView, out half3 Out) { #if SHADERGRAPH_PREVIEW Out = 0; #else Smoothness = exp2(10 * Smoothness + 1); WorldNormal = normalize(WorldNormal); WorldView = SafeNormalize(WorldView); Out = LightingSpecular(Color, Direction, WorldNormal, WorldView, half4(Specular, 0), Smoothness); #endif } 1 2 3 4 5 6 7 8 9 10 11 void DirectSpecular_half ( half3 Specular , half Smoothness , half3 Direction , half3 Color , half3 WorldNormal , half3 WorldView , out half3 Out ) { #if SHADERGRAPH_PREVIEW Out = 0 ; #else Smoothness = exp2 ( 10 * Smoothness + 1 ) ; WorldNormal = normalize ( WorldNormal ) ; WorldView = SafeNormalize ( WorldView ) ; Out = LightingSpecular ( Color , Direction , WorldNormal , WorldView , half4 ( Specular , 0 ) , Smoothness ) ; #endif }

This function performs some simple specular calculations, and if you’re curious, you can read more about them here. The Sub Graph for this function also includes some inputs on the Blackboard:

Make sure that your new node has all the appropriate input and output ports to match the function. Adding properties to the Blackboard is simple; just click the Add (+) icon on the top right, and select the data type. Double-click the pill to rename the input, and drag and drop the pill to add it to the graph. Lastly, update the output port for your Sub Graph, and save it.

Now that specular calculation is set up, we can go back to the unlit graph, and add it through the Create Node menu. Connect the Attenuation output to the Color input of the Direct Specular Sub Graph. Next, connect the Direction output from the Get Main Light function to the Direction input of the specular Sub Graph. Add the result of NdotL*Attenuation to the output of the Direct Specular Sub Graph, and plug this in the Color output.

Now we’ve got a bit of shine!

Working with Multiple Lights

The LWRP’s main light refers to the brightest directional light relative to the object, which is usually the sun. To improve performance on lower end hardware, the LWRP calculates the main light and any additional lights separately. To make sure our shader calculates correctly for all lights in the Scene, and not just the brightest directional light, you need to create a loop in your function.

To get the additional light data, we used a new Sub Graph to wrap a new Custom Function node. Take a look at the AdditionalLight_float function in the CustomLighting include file:

void AdditionalLights_half(half3 SpecColor, half Smoothness, half3 WorldPosition, half3 WorldNormal, half3 WorldView, out half3 Diffuse, out half3 Specular) { half3 diffuseColor = 0; half3 specularColor = 0; #ifndef SHADERGRAPH_PREVIEW Smoothness = exp2(10 * Smoothness + 1); WorldNormal = normalize(WorldNormal); WorldView = SafeNormalize(WorldView); int pixelLightCount = GetAdditionalLightsCount(); for (int i = 0; i < pixelLightCount; ++i) { Light light = GetAdditionalLight(i, WorldPosition); half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation); diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal); specularColor += LightingSpecular(attenuatedLightColor, light.direction, WorldNormal, WorldView, half4(SpecColor, 0), Smoothness); } #endif Diffuse = diffuseColor; Specular = specularColor; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void AdditionalLights_half ( half3 SpecColor , half Smoothness , half3 WorldPosition , half3 WorldNormal , half3 WorldView , out half3 Diffuse , out half3 Specular ) { half3 diffuseColor = 0 ; half3 specularColor = 0 ; #ifndef SHADERGRAPH_PREVIEW Smoothness = exp2 ( 10 * Smoothness + 1 ) ; WorldNormal = normalize ( WorldNormal ) ; WorldView = SafeNormalize ( WorldView ) ; int pixelLightCount = GetAdditionalLightsCount ( ) ; for ( int i = 0 ; i < pixelLightCount ; ++ i ) { Light light = GetAdditionalLight ( i , WorldPosition ) ; half3 attenuatedLightColor = light . color * ( light . distanceAttenuation * light . shadowAttenuation ) ; diffuseColor += LightingLambert ( attenuatedLightColor , light . direction , WorldNormal ) ; specularColor += LightingSpecular ( attenuatedLightColor , light . direction , WorldNormal , WorldView , half4 ( SpecColor , 0 ) , Smoothness ) ; } #endif Diffuse = diffuseColor ; Specular = specularColor ; }

Like before, use the AdditionalLights function in the file reference of the Custom Function node, and ensure that you’ve created all the proper inputs and outputs. Make sure to expose Specular Color and Specular Smoothness on the Blackboard of the Sub Graph in which the node is wrapped. Use the Position, Normal Vector, and View Direction nodes to plug in the World Position, World Normal, and World Space View Direction in the Sub Graph.

After you’ve set up the function, use it! First, take your main Unlit graph from the previous step, and collapse it to a Sub Graph. Select the nodes, and right-click Convert to Sub-graph. Remove the last Add node, and plug the outputs into the output ports of the Sub Graph. We recommend that you also create input properties for Specular and Smoothness.

Now you can combine your main light calculations and your additional light calculations together. In the main Unlit graph, create a new node for the Additional Light calculations to go alongside the Main Light calculations. Add the Diffuse and Specular outputs from Main Light and Additional Lights together. Pretty simple!

Creating a Simple Toon Shader

Now you know how to get the data from all lights in a Scene for an LWRP project, but what can you do with it? One of the most common uses for custom lighting in shaders is a classic toon shader!

With all of the light data, creating a toon shader is pretty simple. First, take all the light calculations you’ve done so far, and wrap them in a Sub Graph one more time. This will help with readability in the final shader. Don’t forget to remove the final Add node, and feed Diffuse and Specular into separate output ports on the Sub Graph output node.

There are lots of methods to create toon shading, but in this example, well use light intensity to look up colors from a Ramp Texture. This technique is usually called Ramp Lighting. We’ve included some examples of the kind of Texture Asset needed for Ramp Lighting in the sample project. You can also sample a gradient to use dynamic ramps in Ramp Lighting.

The first step is to convert the intensity of Diffuse and Specular from RGB values to HSV values. This lets us use the intensity of the light color (the HSV values) to determine the brightness on the shader, and helps us sample the Texture at different spots along the horizontal axis of the Asset. Use a static value for the Y channel of the UV to determine, from top to bottom, what part of the image should be sampled. You c

an use this static value as an index to reference multiple lighting ramps for the project in a single Texture Asset.

Once you’ve set the UV values, use a Sample Texture 2D LOD node to sample the Ramp Texture. The Sample LOD is important; if we use a regular Sample Texture 2D node, the ramp is automatically mipped in a Scene, and objects further away will have different lighting behaviors. Using a Sample Texture 2D LOD node allows us to manually determine the mip level. Additionally, since the Ramp Texture is only 2 pixels high, we created our own Sampler State for the Textures. To make sure that the Texture is sampled correctly, we set the Filter to Point, and the Wrap to Clamp. We exposed this as a property on the Blackboard so that you can change the settings if the Texture Asset changes.

Finally, we multiply the ramp sample from the diffuse calculations by a color property, Diffuse, so that we can change the object’s colors. Add the ramp sample from the specular calculations to the Diffuse output, and plug the final color into the Master node.

Expanding Custom Lighting

This simple custom lighting setup can be expanded and applied to a wide variety of use cases in all kinds of Scenes. In our example project, we’ve included a full Scene configured with shaders that use our custom lighting setup. It also contains vertex animation, a simple subsurface scattering approximation, as well as refractions and coloring that use depth. Download the project, and check out our Example Assets to explore more advanced methods!

Keep Learning!

If you want to discuss Shader Graph, and the shaders you can make with it, come hang out in our brand new forum space! You can also find community members and (sometimes) a few developers hanging out in the community Discord!

Don’t forget to keep an eye out for recordings of our SIGGRAPH 2019 sessions, where we go into even more detail about using Shader Graph for custom lighting!