Introduction To Unity Shader Graph

Preface

Since Unity announced Shader Graph editor available as an optional package starting version 2018.1 I had mixed feelings about that. Is it really such a big deal? Is it that hard to grasp some knowledge about basic shader functions and create own, using CG code? Will it be as performent as code? As a starter, let’s try to implement basic 2D texture manipulation using both, handwritten code and the graph and see the results.

First things first

To be able to use the new Shader Graph we can create a new project that utilize any of new scriptable rendering pipeline (either high definition or lightweight) or add package manually to existing project. In this post I’ll use lightweight rendering pipeline.

To add scriptable rendering pipeline go to Window/Package Manager, find and install lightweight rendering pipeline package. Then we will have to create new rendering pipeline settings. We can do it by creating new asset called Lightweight Pipeline Asset (you can find it under Rendering tab in creation menu). After that, proceed to Project Settings/Graphics and set your newly created asset as current Scriptable Render Pipeline Settings.

Now we are all set up and ready to create our first shader graph.

Image transition effect

Let’s start with pretty much basic texture operation which is transitioning between two given textures based on selected parameters. First, let’s write some CG code. Since we will operate on 2D texture, create new shader by selecting Create/Shader/Image Effect Shader in project hierarchy and name it TransitionCG.

Start by changing shader appearance folder at the very top of file to be able to assign it to material later on (in my case I changed it to Custom).

Inside properties block define two new variables. _SecondTex("Second texture", 2D) = "" {} will stand for second texture. _OpacityLevel("Opacity level", Range(0.0, 1.0)) = 0.0 will stand for the current opacity level.

Next declare those two structures next to other declarations as follows:

sampler2D _SecondTex;
Float _OpacityLevel;

Then inside fragment shader function interpolate between sampled texture based on opacity level:

fixed4 col = tex2D(_MainTex, i.uv);
fixed4 secCol = tex2D(_SecondTex, i.uv);
return lerp(col, secCol, _OpacityLevel);

That’s it! Full code snippet for this shader is available here.

I also wrote small script that ping-pong opacity level during runtime so we can later compare both effects. We can attach it to image itself and fill required components (same script will be to manipulate other shaders).

public class PingPongOpacity : MonoBehaviour
{
  [SerializeField] Image image;
  [SerializeField] string variableName;

  void Update()
  {
    image.material.SetFloat(variableName, Mathf.PingPong(Time.time, 1.0f));
  }
}

Now, to create shader graph, go to project hierarchy and select Create/Shader/Unlit Graph, name file and then double click it to open graph editor.

We will start by defining all properties we need inside properties window that should appear along with Unlit Master node:

Please note that we should change reference name if you are planning to access property from outside (like we will in this example to manipulate Opacity level property).

First drawback is that we can’t access _MainTex directly as one of the node (been searching for solution here and here, found nothing).

Of course, we can work it around by defining two separate textures but this way we lose flexibility to have few images using same material.

Graph layout:

TransitionGraph

Runtime comparison:

GraphVsCG

Finally, let’s compile and peek at the code produced by Shader Graph and compare it with handwritten CG code.

Shader type Vertex shader instructions Fragment shader instructions
CG Code 10 5
Shader Graph 39 5

We can observe significant difference between CG code and shader graph vertex shader output instruction count. It was kinda predictable that such generated code will have some overhead, still, didn’t expect that to be that much.

Image infinite scrolling effect

Once again, lets start with CG code. This time we won’t need any additional properties since all we will do is adding current time to our UV coordinates based on which texture is sampled.

Inside fragment shader block modify one (nothing stops us from modifying both) of the cardinal axis. In our case we will go with X axis.

i.uv.x = i.uv.x + _Time[1];
fixed4 col = tex2D(_MainTex, i.uv);
return col;

And… That’s it. Full source code available here. One more thing we need to do before testing it out is to make sure our sprite wrap mode is set to Repeat instead Clamp. Shader graph turn.

ScrollGraph

Once again, runtime effect:

GraphVsCGv2

Quick peek on shaders instruction count:

Shader type Vertex shader instructions Fragment shader instructions
CG Code 10 4
Shader Graph 39 4

No surprise here, almost same outcome as in previous example.

Image clipping effect

We will use ranged float as property to determine whether fragment of image should be discarded or not.

_ClipValue("Clip value", Range(0.0, 1.0)) = 0.0

Don’t forget to declare variable again in program section:

Float _ClipValue;

and then, inside fragment shader function we check whether current UV coordinates are less then clip value or not. If they are, we will discard current fragment.

if(i.uv.x < _ClipValue)
{
    discard;
}

fixed4 col = tex2D(_MainTex, i.uv);
return col;

Source code available here.

Shader graph version:

ClippingGraph

Runtime effect:

GraphVsCG3

Instruction count comparison:

Shader type Vertex shader instructions Fragment shader instructions
CG Code 10 4
Shader Graph 39 5

That is kinda surprise. Even though difference between handwritten code and shader graph is insignificant, difference between instruction count and count of nodes we used to build out clipping effect is very significant! Compiler did great job here optimizing all nodes ending up with just 5 instructions!

Conclusion

Despite unknown source (atleast for me) of overhead in vertex shader function while building shaders using shader graph, it did, surprisingly, very good job at optimizing stuff and achieving almost same fragment shader instruction count as handwritten shaders. Although finding proper nodes can be sometimes cumbersome, after some practice, shader graph is a huge time saver in shader effect design thanks to immidiate result and very flexible nodes manipulation.

AllEffects

I recommand you to play with new Unity Shader Graph yourself. After you get familiar with basic nodes it will be pure pleasure to create new shader for your game!