Summary

One of the most important ways to get information into our shaders are textures. Textures are read via “texture samplers”, they allow us to read from textures super fast and do stuff like stuff like filtering and mipmapping without us having to worry about it.

To start implementing textures into your shader it’s beneficial that you know how to use properties in shaders, you can find a tutorial for that here.

Result

Sampler Declaration

We add the texture to our shader by declaring a sampler2D in our global scope (in the hlsl part of the shader outside of functions or structs). Then we add it to the properties and set the type to 2D and the default value to “white” {}, that way the sampler will return white for all coordinates when it doesn’t have a texture. This shoudn’t change anything in the rendering of our shader so far.

Shader "Tutorial/03_Properties"{
    Properties{
        _Color("Color", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }
    
    // ...

            //texture
			sampler2D _MainTex;
			//tint of the texture
            fixed4 _Color;

Texture Coordinates

Next we’ll add the UV coordinates to our input struct. UV coordinates are coordinates which define which part of the texture is shown at which part of the mesh. They are already in the mesh so we don’t have to generate them in the shader, we can just use them. To get the UV coordinates, we add a 2 dimensional float vector and give it the textcoord0 attribute so it gets filled with the coordinates. In the vertex shader, we copy the uv from the input struct to a uv coordinate in the vertex to fragment struct and in the fragment shader we can then use the coordinates. For now we’ll just use the coordinates as red and green values for our color.

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

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

//texture
sampler2D _MainTex;
//tint of the texture
fixed4 _Color;

//the object data that's put into the vertex shader
struct appdata{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

//the data that's used to generate fragments and can be read by the fragment shader
struct v2f{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
};

//the vertex shader
v2f vert(appdata v){
    v2f o;
    //convert the vertex positions from object space to clip space so they can be rendered
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    return o;
}

//the fragment shader
fixed4 frag(v2f i) : SV_TARGET{
    return fixed4(i.uv.x, i.uv.y, 0, 1);
}
ENDCG

Coordinates

Here we can see well that the uv coordinates start at {0,0}(black) at the lower left corner and go to {1,1} yellow at the upper right corner. The next step is to use those values to read from a texture. For that we use the tex2D function which takes the sampler as the first parameter and the uv coordinates as the second parameter. With that addition we can add the texture to our model in the inspector and should be able to see it. Use any image you want for this.

//the fragment shader
fixed4 frag(v2f i) : SV_TARGET{
    fixed4 col = tex2D(_MainTex, i.uv);
    return col;
}

Apply

Tiling

To improve upon we did so far, let’s add a texture tiling and a tint. Next to the texture, you can see the “tiling” and “offset” parameter, changing them doesn’t do anything right now, but when used correctly they can scale and move the texture on the mesh. To archive that, we add a new 4 dimensional float to our global scope and call it TextureName_ST, TextureName being the name of the texture you’re tiling. The ST stands for scale/translation, in the first 2 parameters of the vector (x&y) the tiling of the texture will be saved and in the second 2 parameters of the vector (z&w) the offset of the texture will be saved. To use those values we can just call the TRANSFORM_TEX macro which unity gives us. It takes the original UV coordinates and the name of the texture we want to change the UV coordinates for. We use it in the vertex shader where we copy the coordinates from the input to the v2f struct. After that we can now use tiling and offset in the editor as expected.

//the vertex shader
v2f vert(appdata v){
    v2f o;
    //convert the vertex positions from object space to clip space so they can be rendered
    o.position = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

Tint

Finally we’ll add the ability to tint the texture. We will use the the Color variable from the previous tutorial to be our tint. For that we’ll rename it to be called tint in the inspector and in the fragment shader we multiply the color from the texture with the tint. With those additions we can now change the tint and it will multiplied with the image color. That means a white tint will change nothing, a completely red tint[1,0,0,1] will only show the red parts of the image etc…

// ...
//show values to edit in inspector
Properties{
    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
}

// ...

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

// ...

Shader "Tutorial/004_Textures"{
	//show values to edit in inspector
	Properties{
		_Color ("Tint", Color) = (0, 0, 0, 1)
		_MainTex ("Texture", 2D) = "white" {}
	}

	SubShader{
		//the material is completely non-transparent and is rendered at the same time as the other opaque geometry
		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

			//texture and transforms of the texture
			sampler2D _MainTex;
			float4 _MainTex_ST;

			//tint of the texture
			fixed4 _Color;

			//the object data that's put into the vertex shader
			struct appdata{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			//the data that's used to generate fragments and can be read by the fragment shader
			struct v2f{
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			//the vertex shader
			v2f vert(appdata v){
				v2f o;
				//convert the vertex positions from object space to clip space so they can be rendered
				o.position = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}

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

			ENDCG
		}
	}
}

With that our shader has basically the same functionality as the shader unity generates when you press new->shader->unlit, but we understand what every bit of it does and we can improve upon it in the future!

You can find the source code of the shader here: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/004_Textures/textures.shader

If you liked my tutorial and want to support me you can do that on Patreon (patreon.com/RonjaTutorials) or Ko-Fi (ko-fi.com/RonjaTutorials).