⏱ Reading time: 7 min

Welcome back, in the last article, we saw both how to create a very simple shader and get some fundamental concepts on shader functioning.

In this article, we will delve into HLSL and we will understand the basics of language. Then in the practical part, we will examine in depth the concept of shader properties.

Variables

HLSL is a statically typed language. This means that each variable has its own specific datatype, which is declared at the time of the declaration and cannot change.

Scalar types

The scalar quantities represent the simplest datatype of variables to use and display; they are variables with a single numerical value. There are different types of scalar values ​​in HLS. They differ according to the precision of the value to be represented. Choosing a type with a lower precision value has noticeable positive effects on shader performance even if by now the power of graphics cards is such that the difference is often negligible.

  • Integer
    Simple numbers without fractional part, not useful for many complex operations but indispensable for the management of arrays and indexes.
  • Fixed
    Numbers between -2 and +2, always within 256 steps between the different values, are usually used optimally for colour information since the colours are saved in 256 colour steps per channel. (RGBA)
  • Half
    Numbers with low precision can contain any numerical value but tend to be inaccurate, the further away from zero. They are commonly used for HDR colours or to handle small vectors.
  • Float
    Numbers with double the precision of half. They are more accurate and usually used to save and manage positions.
int integer = 6; // Integer
fixed fixed_point = 0.2; //fixed point number
half low_precision = 1.12; //low precision number
float high_precision = 155.321; //high precision number

Vector Types

Then there are vector values based on the scalar ones. With each vector, we can represent things like colours, positions and directions. In HLSL, we write the number of dimensions we need at the end of the type to create a vector type. ( Note that the maximum number of dimensions here is 4 if you need more you have to create arrays )

fixed4 color = fixed4(1, 1, 1, 1);
float3 position = float3(1, 1, 0);
float2 textureCoord = float2(0.2, 0.2);

Packed Arrays

Particularly interesting is the fact that every component of a vector type could be accessed by a standard way know as packed arrays. Typically they are x, y, z and w but the alias used in CG are also r,g,b,a

fixed4 color = fixed4(1,2,3,4);
color.x = color.r;
color.y = color.g;
color.z = color.b;
color.w = color.a;

Swizzling

Another thing to remember is the swizzling, the rapid possibility to rearrange components in vectors.

fixed4 color = fixed4(1,2,3,4);<br>
color.xyzw = color.wzyx;

Matrix Types

Matrix Types are a vector of vectors. They are used for rotation, move and transformation of vectors. We can create matrices by writing [dimension 1] x [dimension2] behind any scalar type. Remember that in 3D graphics, we need a 3×3 matrix for the rotation or scale and a 4×4 matrix to move objects.

float4x4 transformMatrix3d; //a matrix that can scale rotate and translate a 3d vector
//a matrix that can scale and rotate a 2d vector, but not translate it
float2x2 rotationMatrix2d = {
    0, 1,
    -1, 0
};

//when doing matrix multiplication we have to use 4d vectors, 
//the 4th component is just used to make moving the vector via matrix multiplication possible
float4 position;

//we rotate, scale and translate a position my multiplying a vector with it 
//(!!! the order of factors is important here !!!)
position = transformMatrix3d * position;

Packed Matrices

HLSL allows types such as float4x4, access to a single element of the matrix using the _mRC notation, where R is the row, and C is the column:

float4x4 matrix;
// ...
float first = matrix._m00;
float last = matrix._m33;

The _mRC notation can also be chained:

float4 diagonal = matrix._m00_m11_m22_m33;

An entire row can be selected using squared brackets:

float4 firstRow = matrix[0];
// Equal
float4 firstRow = matrix._m00_m01_m02_m03;

Samplers

The Samplers are used to read data from textures. When reading from a sampler, the texture has always fixed coordinates from [0,0] to [1,1]. The UV Space of the textures always starts with the 0,0 as the lower left corner and the 1,1 as the upper right corner.

sampler2d texture; 

float4 color = tex2D(texture, coordinates);

Structs

The final type is the structs, custom datatypes which can hold several other types. Usually, we use them to represent lights, inputs and other complex data. To use a struct we need to define it and then we can use it somewhere else.

//define the struct
struct InputData{
    float4 position;
    fixed4 color;
    half4 normal;
};

//create a instance and use the struct
InputData data;
data.position = float4(2, 0, 0, 2);

Time to practice

Let’s start with creating a new shader in Unity, let’s call it “Shader_01” and like in the previous article, we use this skeleton to initialize it.

Shader "Shader_01"{
	
	Properties{
		
	}

	SubShader{

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

		Pass{
			CGPROGRAM
                        //include useful shader functions
                        #include "UnityCG.cginc"

                        //define vertex and fragment shader
                        #pragma vertex vert
                        #pragma fragment frag



			ENDCG
		}
	}
}

So far we’ve always left the property section empty in the shader, now it’s time to start filling it.
The properties of a shader are parameters that we supply to the shader and that we can use in the code to get any effect. All the properties entered must respect a well-defined syntax.

      _VariableName ("Inspector GUI Name", Type) = (Default Value)

Each property is then called up in the HLSL code, thus specifying a new variable within CGPROGRAM with the same name as the one in the properties.

                          type _VariableName;

As, written above, there are different types of properties, each linked to a different type of HLSL variable.

//Variables that appear in the GUI Inspector and are usable by code 
_MyColor ("Some Color", Color) = (1,1,1,1) 
_MyVector ("Some Vector", Vector) = (0,0,0,0) 
_MyFloat ("My float", Float) = 0.5 
_MyTexture ("Texture", 2D) = "white" {} 
_MyCubemap ("Cubemap", CUBE) = "" {} 

//Same variables called in HLSL in Shaders
fixed4 _MyColor; 
float4 _MyVector;
float _MyFloat; 
sampler2D _MyTexture;
samplerCUBE _MyCubemap;

However, for our practice today, we will need to create only two properties.

_Color ("Tint", Color) = (0, 0, 0, 1)<br>
_MainTex ("Texture", 2D) = "white" {}

With the first, we define colour, and with the second, we define a texture instead. We will use this as a texture with the Molo17 logo, but it will be possible to use any image of course.

Now add after the statements #pragma the two HLSL variables and a new float4 variable that derives from the texture sampler.

 sampler2D _MainTex;
 fixed4 _Color;

 float4 _MainTex_ST; 

We define a simple struct that allows us to insert elements into the vertex shader.

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

also, a struct that is returned to us by the vertex shader to pass to the fragment shader.

struct v2f {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
};

Once done, we can proceed to write the vertex shader code.

v2f vert (appdata v) {
  v2f o;
  // convert position from 3d Space to Normalized Space useful to avoid 
  // problems with multiple devices
  o.position = UnityObjectToClipPos (v.vertex);
  // Apply UV position to Vertex Coordinates
  o.uv = TRANSFORM_TEX (v.uv, _MainTex);
  return o;
}

Then we move on to the fragment shader.

fixed4 frag (v2f i): SV_TARGET {
   fixed4 col = tex2D (_MainTex, i.uv);
   col * = _Color;
   return col;
}

Unlike the Vertex Shader, where we are only converting vertex values ​​to UV values, in the fragment shader in addition to applying the texture with tex2D, we also make an additional modification.

After receiving the colour from the texture, multiply the variable col by our _Color property. This will allow us to dye the texture.

For this third article, we will stop here, in the next we will talk about the different lighting methods.

//Full Shader Code

Shader "Shader_01"{

	Properties{

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

	}

	SubShader{

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

		Pass{
			CGPROGRAM
			//include useful shader functions
			#include "UnityCG.cginc"

			//define vertex and fragment shader
			#pragma vertex vert
			#pragma fragment frag

		 sampler2D _MainTex;
		 fixed4 _Color;

		 float4 _MainTex_ST;

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

		 struct v2f {
			 float4 position: SV_POSITION;
			 float2 uv: TEXCOORD0;
		 };

		 v2f vert(appdata v) {
			 v2f o;
			 // convert position from 3d Space to Normalized Space useful to avoid 
			 // problems with multiple devices
			 o.position = UnityObjectToClipPos(v.vertex);
			 // Apply UV position to Vertex Coordinates
			 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
			 return o;
		 }

		 fixed4 frag(v2f i) : SV_TARGET{
		   fixed4 col = tex2D(_MainTex, i.uv);
		   col *= _Color;
		   return col;
		 }

			ENDCG
}


	}
}

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

Leave a Reply