Shaders

SHADERS

What is this?

I wanted to learn as much as I could about creating shaders using HLSL, so I created a project where I could explore it for myself. In this project I started off by implementing shaders using Unity's Standard Lambert Shader. When I got an understanding of it, I created my own vertex and fragment shader. 



  • Engine: Unity
  • Language: HLSL & ShaderLab
  • Development Time: 2 weeks
  • Team Size: Solo

Shaders

Bump Map

One of the first shaders I created was a shader to be able to control the depth of the bumps of a texture. I also wanted to modify the depth values in the inspector. 


When I changed the tiling of the texture, I realized I had to do the same thing with the normal map to make the sync. I thought it was annoying having to change the tiling of both textures manually, so I created another slider to control the placement of both texture and normal.  



Shader "Olivia/BumpDiffuse"
{
    Properties
    {
       _myDiffuse("Diffuse Texure: ", 2D) = "white" {} 
       _myBump("Normal (Bump) Texure: ", 2D) = "bump" {} 

	   _myBumpSlider("Bump Amount: ", Range(0, 10)) = 1
	   _myScaleSlider("Texture Bump Scale: ", Range(0.5, 5)) = 1
	}

		SubShader
	{
		CGPROGRAM
			#pragma surface Surface Lambert

			sampler2D _myDiffuse;
			sampler2D _myBump;
			half _myBumpSlider;
			half _myScaleSlider;

			struct Input
			{
				float2 uv_myDiffuse;
				float2 uv_myBump;
			};
			   
			void Surface (Input input, inout SurfaceOutput output)
			{
				output.Albedo = tex2D(_myDiffuse, input.uv_myDiffuse * _myScaleSlider).rgb;

				output.Normal = UnpackNormal(tex2D(_myBump, input.uv_myBump * _myScaleSlider));

				// Bump depth
				output.Normal *= float3(_myBumpSlider, _myBumpSlider, 1);
			}

        ENDCG
    }
    FallBack "Diffuse"
}
    

Outline

I created two types of outline shaders. I did this by creating two passes, in other words two draw calls.


In the first shader I created the outline in the first pass, by making the whole object into one solid color. In the second pass I drew the actual object. To get the outline-effect I magnified the solid color to be bigger than the actual object.

Although I liked the effect it gave, I wanted my outline to consider the geometry of the model. Instead of drawing out the outline and then the object, I put the outline on the existing mesh


To do so I calculated the normal, based on the world position using a matrix multiplication

with x and y. Then I calculated an offset based on the x and y value of the normal. 




	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_OutlineColor("Outline Color: ", Color) = (0, 0, 0, 1)
		_Outline("Outline Width: ", Range(0.002, 0.1)) = 0.005
	}

	SubShader
	{
		CGPROGRAM
		#pragma surface surf Lambert 
				
		struct Input
		{
			float2 uv_MainTex;
		};

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

		Pass {
			Cull Front

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"
				
			struct appdata {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
			};

			struct v2f {
				float4 pos : SV_POSITION;
				fixed4 color : COLOR;
			};
			
			float _Outline;
			float4 _OutlineColor;
			
			v2f vert(appdata v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);

				float3 norm   = normalize(mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal));
				float2 offset = TransformViewToProjection(norm.xy);

				o.pos.xy += offset * o.pos.z * _Outline;
				o.color = _OutlineColor;
				return o;
			}


			fixed4 frag(v2f i) : SV_Target
			{
				return i.color;
			}
			ENDCG
		}
    

Extruding

Until this point, I had only changed the look of a mesh with my shaders. One goal I had was to be able to create water. Not only did this mean I had to change the appearance of the mesh but also manipulate the vertices. I decided to start with the basics to learn vertex manipulation; the first thing I thought of was creating a swollen effect.

 

I created this Extrude Shader by raising the value of each vertex position and corresponding normal to then multiply the values with an arbitrary amount.

Lighting

One thing I was curious about was lighting and shadows. Unity has this all set up for you by default. Regardless of that I wanted to get an understanding of how it works in general and what better way to do that then creating your own?

 

By default, Unity uses the Blinn Phong lighting model. I decided to create a shader using that lighting model. The reason I choose it was because it’s the most popular one and I can use the same principles if I want to create the same light in OpenGL (which I later ended up doing – and yes, it was the same rules applied)!

 

To create this light model, I had to get the halfway vector of the surface by normalizing the light direction and the viewer’s vector.

 

The pink rabbit on the left is a regular model using Unity Standard Material. The light on that model is the light Unity provide by default. The rabbit on the right is using my shader; and the light you see on it is from my shader code.

 



        half4 LightingMyBlinn(SurfaceOutput so, half3 lightDir, half3 viewDir, half atten)
		{
			half3 halfWay = normalize (lightDir + viewDir);

			half diffuse = max (0, dot(so.Normal, lightDir));

			float nh = max (0, dot(so.Normal, halfWay));
			float specular = pow (nh, 48.0);

			half4 color;
			color.rgb = (so.Albedo * _LightColor0.rgb * diffuse + _LightColor0.rgb * specular) * atten * _SinTime;
			color.a = so.Alpha;
			return color;
		}
    

Toon shader

To create this cartoon-effect, I created a shader that took in a ramp texture as property. The ramp texture controls how dark the color of the regular texture should be depending on the light direction. I simply used the dot product of the normal of each pixels and light direction to get the diffuse color. After, I used the value from the dot product to use as coordinates for the UVs of the ramp texture.



        float4 LightingToonRamp(SurfaceOutput so, fixed3 lightDir, fixed atten)
		{
			float diffuse = saturate (dot(so.Normal, lightDir));
			float3 ramp = tex2D(_RampTex, float2(diffuse, 0)).rgb;
			
			float4 color;
			color.rgb = so.Albedo * _LightColor0.rgb * (ramp);
			color.a = so.Alpha;
			return color;
		}

    

Rim Light

The rim light effect was created by taking the dot product of the viewer direction (meaning us watching the screen) and the normal of the object. This gave me the opposite rim light-effect. To give it the black color in the middle of the object and the color on the edges, I flipped the value. To get more control over my shader I created properties to be able to adjust my values.



        void Surface(Input input, inout SurfaceOutput output)
        {

			half rim = 1 - saturate(dot(normalize(input.viewDir), output.Normal));	
			rim = step (_rimCutoff, rim) * rim;

			output.Emission = _rimColor.rgb * rim;

			output.Emission *= _rimColor.rgb * (pow(rim, _rimSlider));														
        }
    

Stencil Effect

The stencil buffer is a pixel mask that allows you to get more control over what pixels goes from the scene to the frame buffer. By using the stencil buffer, I could choose the visibility of each pixel.

 

My first shader using this technique was to create an X-Ray-effect. I created two shaders; one for the wall (the object with the hole inside) and one for a quad representing the hole on the wall.

 

I created a stencil scope to store the data the stencil buffer needed. In this case I wanted to store the value 1 in the Stencil Buffer for each pixel that existed in this quad.

 

With the help of the Comparison-operator I compared the value 1 with the content of the stencil buffer.

 

I then created a pass to tell each pixel what to do if both values were the same. In this case, I replaced the pixel if it had the same value. If the quad representing the hole was in front of the wall, both pixels would have the same values meaning it would only show the pixel behind the wall.



        // The Hole Shader
		ColorMask 0
		ZWrite off

		Stencil 
		{
			Ref 1
			Comp always
			Pass replace
		}
		
        // The Wall Shader
		Stencil
		{
			Ref 1
			Comp notequal
			Pass keep
		}

    

After getting an understanding of the stencil buffer I decided to create a model giving optical illusion.

 

I did this similar to the X-Ray effect but made it easier for a user to change the data our stencil should have. In this case I wanted to be able to change the stencil data in the inspector to make it easier for me to build this cube giving the optical illusion. By doing this, I could tell how each pixel on each side should behave.



    
        Properties
	{
		_Color("Color", Color) = (1,1,1,1)

		_StencilRef("Stencil Ref: ", Float) = 1

		[Enum(UnityEngine.Rendering.CompareFunction)] _StencilComp("Spencil Com: ", Float) = 8
		[Enum(UnityEngine.Rendering.StencilOp)] _StencilOp ("Stencil Op: ", Float) = 2

	}
		SubShader
	{
		Tags { "Queue" = "Geometry -1" }

		ZWrite off
		ColorMask 0

		Stencil
		{
			Ref[_StencilRef]
			Comp[_StencilComp]
			Pass[_StencilOp]
		}
		
	// ...
	}

    

Waves

Creating water was a bit of a challenge. A wave can mathematically be created by using a sine function. It returns the height of a wave over time, so I started by manipulating the vertices in a sine function. The values that determines how the waves looks is the amplitude; to adjust the height of each wave and the frequency; to adjust the number of waves per cycle.

 

After giving the mesh some shapes, I realized it looked very stiff. I added two scrolling textures on top of it; one being the water texture and the other one being the foam texture. To make it more realistic I halved the speed of the foam texture.



        void vert(inout appdata v, out Input o)
		{
			UNITY_INITIALIZE_OUTPUT(Input, o); 
			float t = _Time * _Speed; 
			float waveHeight = sin(t + v.vertex.x * _Frequency) * _Amplitude
				+ sin(t * 2 + v.vertex.x * _Frequency * 2) * _Amplitude;

			float waveHeightZ = sin(t + v.vertex.z * _Frequency) * _Amplitude
				+ sin(t * 2 + v.vertex.z * _Frequency * 2) * _Amplitude;

			v.vertex.y = v.vertex.y + waveHeight + waveHeightZ;

			v.normal = normalize(float3(v.normal.x + waveHeight, v.normal.y, v.normal.z));
			o.vertColor = waveHeight + 2 * _Tint; 
		}

    

Glass

To be able to create this effect I had to consider a few things. Firstly, I had to be able to “grab” the texture behind the quad without changing the size of what’s behind it. For this I used the Unity Grab Pass.

 

I also created a bump map; a texture that distorts the object on the other side of the “glass”.

 

In my vertex function I connect the textures to the UVs and projected the data to the clipping space. In the fragment function I calculate how much the object on the other side of the “glass” should be distorted and then returned that color.



            // Projecting data to clipping space
			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex); 
				
				// Flipping it so it's not upside down 
				#if UNITY_UV_STARTS_AT_TOP
				float scale = -1.0;
				#else
				float scale = 1.0;
				#endif
				
				// Based on where it's located in the world, calculate where the UVs would be 
				o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y * scale) + o.vertex.w) * 0.5;  																							
				o.uvgrab.zw = o.vertex.zw; 

				// Conenct texture to UVs
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.uvbump = TRANSFORM_TEX(v.uv, _BumpMap);
				return o;
			}
			
			// Calculating color of each pixel
			fixed4 frag (v2f i) : SV_Target
			{
				// "bumpiness" when moving object
				half2 bump = UnpackNormal(tex2D(_BumpMap, i.uvbump)).rg;
				float2 offset = bump * _ScaleUV * _GrabTexture_TexelSize.xy; 
				i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;

				fixed4 col = tex2Dproj (_GrabTexture, UNITY_PROJ_COORD (i.uvgrab)); 
				fixed4 tint = tex2D(_MainTex, i.uv); 
				col *= tint;
				return col;
			}

    

The Process

Usually, when I start a project on something I’ve never done before, I like to research as much as I can to understand how and why it works.

 

Before working on this project, I had never created any type of shaders or used HLSL. I stared off by reading articles about the code structure; how it’s built up, the different data types and so on. I also read up on the Rendering Pipeline and the three phases (application, geometry, rasterization) to get an understanding of the order things are being rendered behind the scenes. When I felt like I understood the basics, I created my first shader. The only thing my first shader did was creating a texture and color so I could learn the basics in a practical way. I wanted to get an understanding of how the properties worked and how it all was combined.

 

My first shaders I created was all Standard Surface Shaders, this to avoid having to create my own fragment shader. I wanted to understand how everything was set up before jumping into the more advanced stuff.

My goal was not only to learn the structure of a shader, but also to be able to play with some optical illusion and water.

 

When I felt somewhat comfortable with the shader structure and HLSL I created my own vertex and fragment shaders.

 

In Unity, shadows and lighting happens automatically. I didn’t want to take it for granted since all engines don’t have the luxury to have it all by default. To get a better understanding of the concept I created my own lighting and shadow shader.

Challenges

Even though the code is very similar to C# it still has some differences I had trouble understanding; mostly because I had trouble finding a good explanation of the structure. For instance, it took me a while to understand that the properties only existed to show the variables in the inspector. If I wanted to use properties, I had to make sure I also declared them in the SubShader.

 

It was also challenging to create shaders in general, I always had a vision of how I wanted it to look or behave but wasn’t sure how to start. I ended up writing bullet points of the execution order. Every time I had to use any kind of vector math’s I used my note block to help me visualize it.

 

I still have much to learn when it comes to shaders. I went from not understanding any of it to being able to design some pretty cool stuff –so in my opinion I fulfilled my goal!