Last time we learned how to use a function in a surface shader to pass changes into the vertex function so we could give our toon shader an outline. In this lesson we're going to make a shader for changing colors on a sprite by writing a vertex/fragment shader directly.
What is a Vertex/Fragment Shader?
Before any shader can start worrying about what color any part of a 3d object is going to be, it first has to get info about the 3d object itself. (After all, how can you put color on something if you don't even know what you're putting color on?) When we make shaders in Unity, this happens in the vertex function. This is when we can gather the information we want to use later from the 3d object -- and change some of it too, like we saw with the outlines last time. Every shader we have made so far has had a vertex function in it, but since we were making surface shaders, it was hidden where we couldn't see it.
The fragment function is the stage at which we actually start applying the color. (In some other shader languages, this part gets called the pixel shader, although people often use the term pixel shader to refer to any kind of shader.) "Surface shader" is actually a Unity-specific thing - it's a bunch of fancy stuff to make writing shaders that get affected by lighting a lot easier. Under the hood they're really vertex/fragment shaders too!
From here out, I'm going to use "vert" to mean vertex and "frag" to mean fragment -- most other shader nerds do too.
So why would we want to use a vert/frag shader instead of a surface shader, if a surface shader is so fancy? Well there are a lot of reasons, but one big one is that a surface shader is overkill if we don't want the thing we're shading to be affected by lighting. If we're using sprites, we almost never want them to be affected by lighting, and that's the kind of shader we're going to make now.
In your project, make a new folder in your Shaders directory and call it "Unlit". Go into the new folder and right click inside of it and select Create > Shader > Unlit Shader
Name the new shader file SpriteColorMask.
Import these sprites into your project: https://assetstore.unity.com/packages/2d/textures-materials/parallax-dusk-mountain-background-53403
In the folder with the sprites, make a new material and name it ParallaxMountainBG.
In the hierarchy (the tab that has the list of everything in the scene in it) right click and select 2D Object > Sprite
You'll get this
Rename the sprite ParallaxMountainBG. Then drag the parallax-mountain-bg sprite from your assets to the sprite slot in the Sprite Renderer component
Now drag the ParallaxMountainBG material into the material slot on the Sprite Renderer. In the Material section, open the dropdown and find the SpriteColorMask shader we just created.
This flips the Sprite for some reason.
Hit the X checkbox in the Sprite Renderer to flip it back.
Whew, all set? Open SpriteColorMask in Visual Studio.
Anatomy of a Vert/Frag Shader
Conveniently, our new shader has been placed in an Unlit subpath already. Just to keep all my stuff organized, I'm going to give it the same naming pattern than I have to everything else.
Right off the bat there's some junk about fog we don't care about, so I deleted that straight away. You can get your shader to look like mine in this screenshot or paste in the cleaned up code from here: https://pastebin.com/1aj5cJkp
OK, now this is suitable to look at. You'll note that right after we begin our shader with CGPROGRAM, instead of #pragma surface surf, we have this:
And in fact rather than a void surf function down below we have:
so these pre-compiler directives are indicating we have a vertex function named vert and a fragment function named frag. We could even rename them if we wanted, but I don't see a reason to. I am going to change the data type of the frag function to half4 though. There is zero reason to do this except that it makes the data type look blue in Visual Studio and I just like that better.
Anyhow, below our pre-compiler directives, we have our include,
But you know all about that already, since we learned how to use includes last time! "UnityCG.cginc" has all the common functions and macros we'll need today.
Below that we do have some stuff we've never seen before. Instead of a single Input struct, we have TWO structs, and they have a weird format.
So here's how this works: if we want any information from the 3d object, we basically have to ask for it by putting it in the struct we pass into the vertex function. We can name the struct and the info we get from it whatever we want, but there are specific definitions that we have to attach to each name so that ShaderLab knows what the heck we're asking for.
OK, we have this struct we feed to our vertex function, so what's with this v2f struct? It makes more sense if you think of it as "Vertex To Fragment," because this is basically the package of info we'll be able to access in the fragment function. This struct can also be named whatever but it's generally a good idea to give things boring and predictable names so that if you come back and look at your code a few months from now you don't get confused.
Below these structs, we have a sampler2D _MainTex, something we're pretty familiar with, and then a float4 _MainTex_ST. float4 [texture name]_ST is a special variable type. If you put it in your shader, it gets auto-filled with information about the texture scale and offset. It holds 4 numbers, xyzw, which all have to do with the texture whose name goes before _ST. x is the horizontal scale, y is the vertical scale, z is the horizontal offset, and w is the vertical offset.
If you look through the rest of the shader, it would seem that _MainTex_ST doesn't get used, but you should not delete it or you will get errors because it IS used, but in some code we don't see inside UnityCG.cginc. Let's have a look at what's going in inside our vertex function.
the data type of the vertex function is the same as the struct that will be fed to our fragment function. That means that we have to return an initialized copy of that struct. So the first thing here is the declaration of v2f, which is named o for output. It can be called anything you want, but calling it o or OUT is a common convention.
UnityObjectToClipPos is a macro (defined in UnityCG.cginc) that turns the raw position data from the 3d object into something the shader can use. The next part is where _MainTex_ST comes in. TRANSFORM_TEX is a macro which takes the coordinates of the surface of the 3d object and a texture, and gets the coordinates for that specific texture based on its scale and offset.
Down in the frag function, the v2f struct works a lot like the Input struct we're used to using in our surface functions. And this is well-trod territory for us by now.
Making a sprite shader
OK OK OK so enough with the boring explanations! Let's make a dang shader already! First things first, we need to get this shader set up to play nice with our sprite renderer. I don't know if you've noticed, but if you go to the sprite renderer in Unity and try to change the color, nothing happens. We can pick up the color set in the SpriteRenderer in the vertex function, but we have to basically request it by putting it in our appdata struct.
(I also tabbed the colons so they lined up and looked nice, this is optional.)
The frag function is when we can actually do stuff with colors though, so we will need to pass this info there too, so we'll also add half4 color : COLOR; to the v2f struct too.
Transferring the color data from the 3d object to the frag shader is easy. Just put o.color = v.color; somewhere after the declaration of v2f o and before the final return.
then in the frag function, multiply what's already there by i.color;
Easy! Now back in Unity the sprite color works.
Next bit of weirdness: if you were to drag a different texture into the slot on the shader, it wouldn't work. Let's go ahead and make that play a little nicer with the Sprite Renderer. Change your _MainTex line in the properties to this:
The [PerRendererData] tag hides that property in the inspector and makes it so that if the property is changed in a special way in code while the game is running, it doesn't spawn a new copy of the material, which is important for performance. We're not going to change this texture in code -- but that's because the Sprite Renderer is doing that for us already.
While we're up here, let's change our tags to work better with an unlit sprite.
And here's another good trick: we can add a drop down to our properties like this
(remember, Cull refers to what side of a 3d object Unity doesn't draw to save resources)
Lastly, if we want our sprite to have transparency (like if it isn't a perfect rectangle), we'll want to add
Blend SrcAlpha OneMinusSrcAlpha
right under where we specify the cull mode. This gets us normal transparency how you'd think about it for a layer in photoshop. We will also add ZTest Off. Ztest is a way of determining whether or not to draw something based on if anything is in front of it, but the Sprite Renderer already has its own sorting layers, so we'll turn off the ZTest ordering here to get out of the way of the Sprite Renderer component doing its thing.
So far our tags and properties and so on should look like this:
Save and head back to Unity so we can have a look. If you turn cull off and look behind the sprite, you'll find you'll still be able to see it there too. Also, if you drag a different sprite in, like parallax-mountain-mountain-far, you'll find that the sprite is a perfect transparent cutout.
You can even slide the alpha value of the color in the sprite renderer down to lower the opacity.
Making a Color Masked Sprite Shader
All well and good, but this shader doesn't do anything special we don't get from the default sprite material. Let's make it do something cool. Remember how we made a color mask in Part 7 so we could make Claire's tattoos whatever color we wanted? Let's do that, but for our sprite shader.
Drag parallax-mountain-bg back into the sprite slot in the sprite renderer, then open it in your favorite image editing program.
Make a RGB color mask like we did for Claire's tats. Some fudging may be required. For the fourth color use transparency. (there's a more sophisticated way to incorporate alpha channel masking with photoshop but since I don't know how to recreate that functionality in CLIP studio we're just not gonna do that today!)
save this as parallax-mountain-bg-mask. Also make an image with the same dimensions as the original, but totally white, and save it as parallax-mountain-bg-base.
In our import settings, set the texture type to Sprite. We want these to be tilable and to be stay crisp, so we'll set wrap mode to Repeat and filter mode to Point.
Drag parallax-mountain-bg-base into the Sprite slot of the SpriteRenderer.
Now we'll start doing our layering of colors based on the mask the way we did in Part 7. With the added twist that we'll use the opacity to determine the strength of the fourth color - specifically, the lower the opacity, the stronger the fourth color.
We'll use the same uv coordinates for the mask as we'll use for the main sprite texture, so we'll hide the scale/offset boxes by adding a [NoScaleOffset] tag in front of the _Mask property
Save and head back to Unity. Drag the parallax-mountain-bg-mask sprite into the color mask slot and push the colors around to whatever you like.
And tadaa! Make some materials and mask textures for the other sprites that came in this pack and set them up in a diorama with all the hideous colors your heart desires!
Because we're letting the Sprite Renderer component handle the sorting, you can put the transforms for all of these sprites in the exact same place and then set the order in the Sprite Renderer component. Lower numbers go in the back, higher numbers in the front. So for this I have the parallax-mountain-bg sprite at 0, the background mountain at 1, foreground mountains at 2, trees at 3, and foreground trees at 4.
Parting thought: ain't no law saying you HAVE to use a white sprite as the base. You can use the original sprites too. Just remember that when we multiply colors that have a value between 0 and 1 (most colors) they can only get darker than what they started as.
The shader we made in this tutorial is attached to this post. Let me know what you come up with in the comments here, on Twitter, or in Discord. And if this tutorial helped you out, please consider becoming a patron!
This tutorial is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike4.0International License. The code shared with this tutorial is licensed under a CreativeCommonsAttribution 4.0 International License. The sprites used in this tutorial were created by http://ansimuz.com and are stated here to be in the public domain.