Introduction to Shaders in Unity

Ever wondered about shaders in Unity? In this tutorial, you’ll learn what shaders are, how to display vertex colors and how to animate within shaders. By Joseph Hocking.

4.7 (25) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Creating a Custom Shader for Vertex Color

Create a new Surface Shader and name it LitVertexColor. Set the cartoon-sand material to use this Shader, and then open the Shader asset to edit the code.

You only need to make two small additions for vertex color. First, add the following code to the Input struct:

struct Input {
	float2 uv_MainTex;
	float4 vcolor : COLOR; // vertex color
};

Next, incorporate vcolor into the color calculation that happens in surf:

fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color * IN.vcolor;

And that’s it! Save the Shader and return to the Unity scene. Once the Shader compiles, you’ll see the edges of the island blend under the water.

Desert island scene with blended edges

Animating the Water Texture

Now the sand looks a lot more appealing! Meanwhile, the other issue you identified was that the water is completely static.

You can change this with another custom Shader, although you don’t need lighting this time. Remember that Surface Shaders handle the lighting for you, so you won’t create a Surface Shader this time.

Creating an Unlit Shader

Instead, create an Unlit Shader through the right-click menu: Create ▸ Shader ▸ Unlit Shader.

Name the new Shader asset CartoonWater and open it in your code editor.

First thing’s first, update the name of the shader right at the very top.

Shader "Custom/CartoonWater"

The default name for this Shader is "Unlit/CartoonWater". Changing that name to "Custom/CartoonWater" makes it easier to find your custom Shaders in the menu.

Next, add some additional properties for your water shader. The new Properties block should look like this:

Properties
{
   _MainTex ("Texture", 2D) = "white" {}
   _Opacity ("Opacity", Range(0,1)) = 0.5
   _AnimSpeedX ("Anim Speed (X)", Range(0,4)) = 1.3
   _AnimSpeedY ("Anim Speed (Y)", Range(0,4)) = 2.7
   _AnimScale ("Anim Scale", Range(0,1)) = 0.03
   _AnimTiling ("Anim Tiling", Range(0,20)) = 8
}

The default Unlit Shader only had a single texture property _MainTex, so you added more properties to control both the opacity of the water and how the surface texture animates.

Update the Tags section next and just after the LOD 100 line, add some new options:

Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

The default tags expected an opaque material, so you added and adjusted some flags to support transparency. In particular, note that turning ZWrite off means the water won’t obscure other objects in the depth buffer, while you set Blend to use alpha values.

Now observe the CGPROGRAM section with its set of pragma directives.

You worked with a Surface Shader previously, but now you’re dealing with straight Vertex and Fragment Shaders. Thus, whereas the #pragma directive declared the Surface Shading function before, this time the #pragma directives declare Vertex and Fragment Shading functions.

Also note the #include statement. Unity provides a library of useful functions here (which this line imports for you to use in the shader). They do useful things like sample the texture and keep track of time.

Surface Shaders get these libraries automatically when you generate their code, but you need to include them explicitly for Vertex and Fragment Shaders.

Unity’s UnityCG.cginc include brings in predefined variables and helper functions

unity brewing up useful shader tools

Unity’s UnityCG.cginc include brings in predefined variables and helper functions

Now take a look at the included structures:

struct appdata
{
   float4 vertex : POSITION;
   float2 uv : TEXCOORD0;
};

struct v2f
{
   float2 uv : TEXCOORD0;
   UNITY_FOG_COORDS(1)
   float4 vertex : SV_POSITION;
};

There are two input structures this time, instead of just one input structure for a Surface Shader. You input the first struct to the Vertex Shader, which outputs the second struct. That output from the Vertex Shader then goes to the input of the Fragment Shader.

Just below the sampler2D _MainTex; line in the main Cg code, add the following new properties:

float4 _MainTex_ST;
half _Opacity;
float _AnimSpeedX;
float _AnimSpeedY;
float _AnimScale;
float _AnimTiling;

Just as in the previous Surface Shader, you must declare all the properties as variables in the Cg code. You’ll use these properties to modify the texture and animate it in the main shader function.

Now, in the main frag function of the Shader, right at the top (above the // sample the texture code comment, add:

// distort the UVs
i.uv.x += sin((i.uv.x + i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedX) * _AnimScale;
i.uv.y += cos((i.uv.x - i.uv.y) * _AnimTiling + _Time.y * _AnimSpeedY) * _AnimScale;

These two lines of code are the meat of the animated water effect. This code offsets the UV coordinates it receives, with the amount of offset changing over time.

The result is an undulating animation of the texture. Meanwhile, both lines of code do essentially the same thing but in different directions. You’ll understand both after breaking down one line in detail.

First, the code calls a trigonometry function, either sine or cosine, so that the value will range on a repeating wave. _AnimTiling scales the number passed in. As the name implies, _AnimTiling controls the tiling of the effect – in this case, by affecting the frequency of the sine wave.

Next, you add the time to the number passed in, causing the returned value to move along the sine wave over time. The library code from earlier includes a variable that tracks the time.

_Time is a vector of four numbers, with the four values being the time scaled by various amounts for convenience. That’s why the code uses _Time.y and not simply _Time; y is the second number in the vector.

Finally, you multiply the entire thing by _AnimScale, a property that controls how strong the effect is. Greater scale means more undulation.

Lastly, just above the return col; line of code in the frag function, add:

col.a = _Opacity;

Here, the code simply sets the alpha value to the _Opacity property. Make sure to do this after sampling the texture; otherwise, the texture would overwrite the alpha value.

Now you’ve written your Shader, you can assign it to the cartoon-water material.

By default, the water’s surface won’t animate in the Scene view, but it will animate in the Game view when you press Play.

shader animating the waves for an undulating effect