Get an 20% OFF using the RELEASE code on your book purchase. For a limited time.

Introduction to the shader programming language

3.0.1. Structure of a vertex/fragment shader

This post is also available in…

To analyze its structure, we will create an Unlit Shader and call it “USB_simple_color”. As we already know, this type of shader is a basic color model and does not have great optimization in its code, this will allow us to analyze in-depth its various properties and functions.

When we create a shader for the first time, Unity adds default code to facilitate its compilation process. Within the program, we can find blocks of code structured in such a way that the GPU can interpret them. If we open our USB_simple_color shader, its structure should look like this:

Shader “Unlit / USB_simple_color”
{
    Properties
    {
        _MainTex (“Texture”, 2D) = “white” {}
    }
    SubShader
    {
        Tags {“RenderType” = “Opaque”}
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include “UnityCG.cginc”

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

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

            sampler 2D _MainTex;
            float4 _MainTex;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o, o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
        }
    }
}

Likely, we do not fully understand what is happening in the different code blocks from the shader we just created. However, to begin our study, we will pay attention to its general structure.

Shader “InspectorPath / shaderName”
{
    Properties
    {
        // block properties
    }
    SubShader
    {
        // SubShader configuration in this block
        Pass
        {
            CGPROGRAM          
            // Cg - HLSL program in this block
            ENDCG              
        }
    }
    Fallback “ExampleOtherShader”
}

(The shader structure is the same in both Cg and HLSL, the only thing that changes are the program blocks in Cg and HLSL. Both compile in current versions of Unity for compatibility)

The previous example shows the main structure of a shader. The shader starts with a path in the inspector (InspectorPath) and a name (shaderName), then the properties (e.g. textures, vectors, colors, etc) after that the SubShader and at the end of it all is the optional Fallback. 

The “inspectorPath” refers to the place where we will select our shader to apply it to a material. This selection is made through the Unity Inspector.

We must remember that we cannot apply a shader directly to a polygonal object, instead, it will have to be done through a previously created material. Our USB_simple_color shader has the path “Unlit” by default, this means that: from Unity, we must select our material, go to the inspector, search the path Unlit and apply the material called USB_simple_color.

A structural factor that we must take into consideration is that the GPU will read the program from top to bottom in a linear manner, therefore, if shortly we create a function and position it below the code block where it will be used, the GPU will not be able to read it, generating an error in the shader processing, therefore Fallback will assign a different shader so that graphics hardware can continue its process.

Let’s do the following exercise to understand this concept.

// 1 . declare our function
float4 ourFunction()            
{
    // your operation here ...
} 

// 2. use our function
fixed4 frag (v2f i) : SV_Target
{
    // function being used here
    float4 f = ourFunction();         
    return f;
}

The syntax of the above functions may not be fully understood. These have been created only to conceptualize the position of one function for another.

In section 2.4.3 we will talk in detail about the structure of a function. For now, the only important thing is that in the previous example its structure is correct because the function “ourFunction” has been written where the block of code is placed. The GPU will first read the function “ourFunction” and then it will continue to the fragment stage called “frag”.

Let’s look at a different case.

// 2. use our function
fixed4 frag (v2f i) : SV_Target
{
    // function being used here
    float4 f = ourFunction();        
    return f;
}

// 1 . declare our function
float4 ourFunction()            
{
    // your operation here ...
}

On the contrary, this structure will generate an “error”, because the function called “OurFunction” has been written below the code block which it is using.

Follow us to stay informed about all the latest news, updates, and more.

Join the group to share your experiences with other developers.

Subscribe to our channel and keep learning game dev!

jettelly-logo

Jettelly Team

We are a team of indie developers with more than 9 years of experience in video games. As an independent studio, we have developed Nom Noms in which we published with Hyperbeard in 2019. We are currently developing The Unity Shader Bible.

Follow us on our social networks.