Let's Learn: Shaders in Unity! Pt 1 - Introduction

Well, hello there. You’ve read through the entire title and have decided to continue reading, so I’ll presume you’re here to learn about shader programming in Unity. If that’s the case, then we have something in common already! I also want to learn how to write shaders (that work). I’ve avoided it for a while now, maybe you have too? My coworkers are plenty competent in the shaders department, so in the past I’ve leaned on them when shader-related problems arose.

Let’s embark on this journey together! I’m not sure where it’ll take us, but I hope we come out the other side with some new skills and a healthy appreciation for all the things graphics programmers do for us.

What Do Shaders Do?

Well, here’s what I know: a shader’s job is to decide how your virtual three dimensional scene should be represented on your actual two dimensional display. The shader gets some information like the textures of the object that it’s affecting, as well as what vertex is currently being processed and decides how that vertex should appear.

You could say that a shader executes blindly on each vertex that it processes. This means that while it’s deciding how the current vertex should look, it knows nothing about the rest of your vertices. Imagine trying to write a function to align three buttons in your UI, one at a time, without knowing where any of the buttons had been placed previously. It’s possible, it just takes a different way of thinking. Shaders might not be the easiest thing to learn. They can get pretty math-heavy pretty quickly and they’re tough to debug. Some best practices for writing shaders might feel totally backwards when compared to writing non-shaders.

Normally you wouldn’t calculate something unless you had to. You’d throw in a condition to make sure work is only done if it needs to be done. But for a GPU, math is cheap and branching is expensive. An if...else statement will actually cost you more than just calculating both outcomes of the condition and “mathing” your way to the correct outcome. But more on that later. All that to say, this is no walk in the park, I guess that’s why some people focus an entire career on graphics programming.

Getting Started

Oh good, you’re still here! Starting an instructional post with how hard it is to do that thing might not be a great idea. I’ll try to keep things more encouraging.

YOU’RE DOING GREAT SO FAR!

How’s that? Too soon? Okay let’s accomplish something first.

I’ve been told that surface shaders are a little simplistic, not as powerful or capable. Simplistic huh? That sounds like a great place to start! At least just to get our feet wet. We’ll take a stab at big kid shaders after this.

  1. Create a new blank project. We don’t need much so don’t bother importing any packages for now.
  2. Create a new Shader by selecting the Assets menu in the top menu bar or right clicking in your Project view, now select Create > Shader > Standard Surface Shader. Name your shader whatever you like, I named mine Colorize. (Descriptive names are a handy tool. The name should tell you or anyone else working on the project what the shader or script does, or at least give a rough idea. “Reginald” is a great example of a terrible name for a shader.)
  3. Create a new Material (also under the Create menu) and give it a name as well. If you select your new Material in the Project panel and have a look at the Inspector panel, near the top you’ll see a little drop-down menu that lets you choose the shader for that material, you’re going to want to pick the one you just made.
  4. Add a model to your scene. We’re going to need something in our scene that our soon-to-be-freshly-minted shader can affect. You can use a 3D primitive if you like, or follow along and use this free model of a crayon.   

This will be a good example of how you can use shaders simplify your art assets and make them more flexible. In this case we’re taking a group of crayons with “hard coded” colors, and simplifying down to just one model that we can recolour and use to make any colored crayon without having to create more art assets.

I’ve isolated a single crayon and stripped all the color from the materials and texture. There’s a unity package available here that will get you set up with the single crayon just like you see above.  

I’M VERY PROUD OF WHAT YOU HAVE ACCOMPLISHED SO FAR!

Double click the shader file in your Project panel to open it up in your code editor. Now erase everything in there, we’re going to start from scratch. Every shader has a few basic parts. Have a gander at this empty shader:

Shader "Colorize"
{
    Properties
    {
    }

    SubShader
    {
    }
}

The Properties block, appropriately enough, contains the properties for your shader. This is where you declare and initialize them (you also have to declare them again in the SubShader but we’ll get to that).

The SubShader block is where the real meat is, this is where you’ll do the actual shader work. While you can only have one Properties block, you can define multiple SubShader blocks, each subsequent block will be treated as a fallback if the first one isn’t compatible with your GPU.

Now let’s define some properties:

Properties
{
    _MainTex ("Texture", 2D) = "white"{}
    _Color ("Color", Color) = (0,0,0,0)
}

WHOA! YOUR PROPERTY DEFINITIONS ARE EXQUISITE!

There now, that felt good. You’ll see we’ve added  the properties _MainTex and _Color. Let's talk about them.

_MainTex will accept the type 2D which is a texture. Its name in the Inspector panel in Unity will be "Texture" and its default value will be white if no texture is supplied. 

_Color will accept the type Color. Its name in the Inspector panel will be "Color" and we're initializing it with the color black.

When you select your material in the Project panel, you’ll see these two properties appear and you can configure them. You won’t see them yet though. You’ll probably just see errors. Let’s set up the SubShader:

SubShader
{
    CGPROGRAM
    #pragma surface surf Lambert
    ENDCG
}

Not much going on here. Yet. But let’s walk through what we have.

CGPROGRAM...ENDCG tells the compiler that the code inside is an Nvidia CG main program entry point.

#pragma tells the compiler to do something. In our case, that something is…

surface surf which says, “Hey compiler, we want to run a surface shader and the function to do that is called ‘surf’

Lambert indicates that we want to use the Lambertian lighting model.

But we’re not done yet. We’ve told the compiler to use a function called surf, but we haven’t declared that function yet. That’s going to make the compiler upset. Let’s not do that.

We’ll finish off the shader like so:

CGINCLUDE
struct Input {
    float2 uv_MainTex;
};

sampler2D _MainTex;
half4 _Color;

void surf (Input IN, inout SurfaceOutput o) 
{
    o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * _Color;
}
ENDCG

The code above can live anywhere in your shader, or it can live in its own file and be used by other shaders too. But other shaders that use it will need to make sure to have _MainTex and _Color properties defined.

struct Input {...} defines what input your shader needs. We need to sample the _MainTex, and to do so we need to know what UV we’re operating on. To do that we must define it as uv_MainTex.

sampler2D _MainTex is defined once again with the same name but the type is now sampler2D which represents a texture.

half4 _Color is also defined with the same name as in our Properties block, and we’ll use a half4 to represent it. Colors can be represented as fixed4, half4 or float4 all with increasing amounts of precision.

Generally speaking, it’s best to use lower precision types where possible, as it will likely yield better performance. We opted for medium precision because we’re doing a bit of math and don’t want to lose too much color information.

void surf is our surface shader function declaration. It accepts some Input, which in our case will have our current UV data in it and it provides output, which is whatever we’ve done in our shader. We’re setting the output’s Albedo (or color) property to the return value of tex2D() which is a built-in function that does a texture lookup on a given sampler2D at the provided UV coordinates and returns a color value. We’re multiplying the rgb values of our texture at any given UV position by the value of our _Color property. The result will be lighter pixels turning a shade of our color, while preserving full black pixels.

Notice that the Materials for both the label and the wax use the same shader. All the white pixels on the crayon label get tinted to whatever color we choose, and the black pixels are preserved. Go ahead and create a whole rainbow of crayons if you like. Or don’t. If something didn’t quite work out for you, feel free to grab this unity package containing the end result for this tutorial. Use it to see where you went wrong.

FANTASTIC WORK! YOU READ THIS WHOLE POST, PROBABLY DID THE WORK AND HOPEFULLY LEARNED SOMETHING!

Resources

Full disclosure, I came up with the idea for this post after having started to learn shaders. So I wasn’t totally clueless when I started writing. It helps to know at least a little bit about a topic before you attempt to write about it for the purpose of helping others. Below are some links to sites that I found helpful in explaining various concepts.

I actually read a couple posts on shaders before beginning to write this post. It’s tough to write about something of which you know very little about. If you want some more advice with more technically detailed explanations, check out the links below.

http://www.alanzucconi.com/

http://patriciogonzalezvivo.com/2015/thebookofshaders

And for general Cg language documentation, do check out:

http://http.developer.nvidia.com/Cg/index.html