⏱ Reading time: 7 min

In the last article, we only started exploring how shaders work.
You can find the old article at this link: https://blog.molo17.com/2019/05/dont-let-the-shaders-scare-you/

Now it’s time to explore some concepts and start doing something more complex.
However, first let’s grasp the last series of essential concepts. I am sorry, but this topic is a bit hard and require some effort to be understood well.

Something to learn well for any future battles

In this simple diagram, we can see, in a very loose way, the relationships between 3d Models, Materials and Shaders.

3D models are simply a collection of points called vertices ( or vertex ). All these vertices are connected in forming triangles ( tris ) according to a scheme called vertex order. Every single vertex also has a whole range of information, such as Normal ( called Vertex Normal ) ( at the moment consider it as the only direction the vertex is pointed to ), Colour (Vertex Color) and coordinates necessary to map a texture (UV Data ).

Each 3d model is substantially invisible on the screen and cannot be rendered without a Material. Materials are mostly containers for shaders and their property values. Many Materials can have the same Shader, but by changing their properties or providing different data, it is possible to modify the final result displayed on the screen.

Learn to recognize your enemy

There is no single type of Shader; instead, there are different types of shaders that over time, became obsolete or popular depending on speed and potential.

Unity, for example, approaches the shaders defining them within three categories:

  • surface shaders
  • fragment and vertex shaders
  • fixed function shaders

Do not be frightened, we will soon return to them and everything will appear more evident after some explanation.

Outside of Unity in the field of computer graphics, you may encounter shaders like:

  • Vertex Shader
  • Geometry Shader
  • Pixel Shader
  • Tesselation Shader
  • Fragment Shader

Why this difference? Well, because Unity tries to help in every way the development by providing libraries already prepared to avoid writing too much code by repeating the same functions. The concept of Unity for surface shaders, for example, includes many functions of both Vertex and Pixel and Tesselation Shaders.

Reveal the mysteries of arcane terms

Ok, so now we know that there are terms to define shaders, but what do they mean?

Let’s analyze them together.

Vertex Shader – They are used to manipulate the vertices of 3d models. They operate on only one vertex at a time, altering the position or the Normal or texture coordinates. Usually, shaders of this type are used for waving elements such as flags, clothes, various particles.

Geometry Shader – They take care of creating primitives or performing calculations on them. They can create new points or take away some. They were usually used to create mathematical models or to define complex systems.

Pixel Shader – Once they were used for operations such as lighting and bump mapping, now they usually take information from the Vertex shader and the Geometry Shaders interpolating the results. Many of their tasks have now become Fragment Shaders.

Tesselation Shader – Shader born recently takes care of creating new geometry in 3D models to define them in order to provide more detail on request

Fragment Shader – The last type of Shader that deals with interpolating all the other shaders together with other parameters to provide useful information for final rendering

Let’s move on to the practice.

Let’s try together to create our first Shader and better understand how it works. In the previous article, we had seen the code behind a shader in Unity. Now let’s try to create an even simpler one.

Let’s create a new 3D project in Unity. Let’s call it Shader_Guide, let’s create a new scene called lesson_01 and leave all the scene settings as default. Then add a sphere to the centre of the scene from the editor window.

Now let’s create in the Assets folder a new folder called Shaders and inside it right click to create a new surface shader called “Shader_00”. Click twice to open it in our favourite code editor and delete everything inside it replacing it with this:

Shader "Shader_00"
{
	Properties
	{
		// Proprieties of the shaders

	}

	SubShader
	{
		// Code of the shaders

	}


}

Ignore any errors, save and create in the Asset folder the Shader_00 material to which you will assign the newly created Shader of the same name. Then assign the material by dragging it onto the sphere in the scene view.

What is going on? Why did the sphere become purple/pink?
Simple, Unity is informing us that there is an error in this Shader and failing to find recovery shaders, it displays an empty shader with only one colour, this. Knowing it is essential for us to understand what happens.

Now, after realizing that an empty shader can’t work well, let’s go to fill it correctly and make the simplest Shader in the world work.
Let’s add these lines in the Subshader space.

    Tags{
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
    }

        CGPROGRAM
        #include "unityCG.cginc"

        ENDCG

What are we doing? Simple, with the first lines we explain to Unity what kind of sub shader we are loading with some tags.
In a shader, there may be different types of sub shaders, and it is essential that the rendering pipeline knows when and how to perform the operations. After we did all that, we can start writing the HLSL part of the Shader.

In the lines we have inserted above, it is particularly important to note the word CGPROGRAM. it is precisely through this keyword that we explain to the Shader where our HLSL code is found, ending with the word ENDCG.
At the moment there is only one instruction in our code

#include "UnityCG.cginc"

With this line, we instruct the Shader compiler to load all the code already set up by Unity which contains a whole series of pre-set functions. We will talk about it in depth in the next posts.

Now let’s add a structure that allows us to manage input data. Usually in shaders, it is called “appdata”, and for now, we will only use it to manage the positions of the vertices of the object, to make the compiler understand that the data managed inside the struct we use the Position attribute.

struct appdata {
    float4 vertex: POSITION;
}

Now let’s write a struct that will be returned by the vertex shader. For now, we are not really going to do anything interesting, but despite this, to make Unity understand that we are using location data, we will use the SV_POSITION attribute.

struct v2f {
    float4 position: SV_POSITION;
}

After this, we write the function that will take care of our vertex shading. The function will return a struct v2f and accept an appdata. We won’t do anything complicated. We will initialize a struct v2f O and fill it with the position on the screen of each vertex. The UnityObjectToClipPos function, contained within UnityCG.cginc will allow us for now to avoid multiplying matrices.

v2f vert (appdata v) {
    v2f o;
    o.position = UnityObjectToClipPos (v.vertex);
    return o;
}

Also, finally, we also write the fragment shader. It doesn’t do anything special; it merely receives the data from the vertex function and colours them all in red. To make Unity understand that the result of this function is to be shown on the screen, we use the attribute SV_TARGET.

fixed4 frag (v2f i): SV_TARGET {
    return fixed4 (0.5, 0, 0, 1);
}

Now it seems all over, but if we try to save this Shader code, we will still get an error.
This happens because we have not yet told Unity what we will use the various functions for. The way we can communicate it is by writing immediately after our #include line.

#pragma vertex vert
#pragma fragment frag

These two lines specify to the compiler that there are two functions and their name. This time everything should work, we should not have errors, but the sphere now should be coloured red.

Shader "Shader_00"
{	

        Properties
	{
		// Proprieties of the shaders

	}

	SubShader{
                  // Code of the shaders

		Tags{
				"RenderType"="Opaque" 
				"Queue"="Geometry"
			}
		Pass{
			

			CGPROGRAM
			#include "UnityCG.cginc"

			#pragma vertex vert
			#pragma fragment frag

			struct appdata{
				float4 vertex : POSITION;
			};

			struct v2f{
				float4 position : SV_POSITION;
			};

			v2f vert(appdata v){
				v2f o;
				o.position = UnityObjectToClipPos(v.vertex);
				return o;
			}

			fixed4 frag(v2f i) : SV_TARGET{
				return fixed4(0.5, 0, 0, 1);
			}

			ENDCG
		}
	}
}

With the next articles, we will start to understand how to build shaders and tackle topics like lighting in a more sophisticated way.

One thought on “Don’t let the Shaders scare you – Part 2

Comments are closed.