No history yet

Shader Programming Fundamentals

The Graphics Pipeline

At its core, a shader is a small program that runs on your computer's graphics processing unit (GPU). Its job is to tell the GPU how to draw an object on the screen. Every material on every object in a game, from a character's skin to a brick wall, is controlled by a shader.

To understand shaders, you first need to understand the graphics pipeline. This is the sequence of steps the GPU takes to turn a 3D model, which is just a collection of vertices (points in space), into the 2D image you see on your monitor. Shaders are the programmable parts of this pipeline.

The two most important programmable stages for us are the vertex shader and the fragment shader. Think of it like a car factory assembly line. The vertex shader is the first station, where the car's frame is put together and positioned. The fragment shader is the paint shop, where each part of the car gets its final color and finish.

Inside a Unity Shader

Unity uses a language called ShaderLab to structure shaders. It's a wrapper that organizes the different parts of the shader and contains snippets of standard shader code, usually written in High-Level Shading Language (HLSL).

A basic ShaderLab file has a clear structure.

Shader "MyShaders/SimpleColor" // Defines the shader's name and location in the editor
{
    Properties
    {
        // Input properties for the artist (e.g., colors, textures)
    }
    SubShader
    {
        Pass
        {
            // HLSL code for the vertex and fragment shaders goes here
        }
    }
}

The Properties block defines variables you can change in Unity's Inspector window, like colors or sliders. This is powerful because it lets artists or designers customize a material's look without editing code.

The SubShader block contains the actual rendering logic. A shader can have multiple SubShaders to support different hardware. Unity will try to use the first one, and if the user's GPU can't handle it, it will fall back to the next one.

Inside a SubShader, you have one or more Pass blocks. Each Pass represents one full run of the vertex and fragment shader. Most simple shaders only need one Pass.

Vertex and Fragment Shaders

Let's look at the two workhorses of the pipeline. These are written inside the CGPROGRAM and ENDCG tags within a Pass block in ShaderLab.

The vertex shader runs once for every vertex in your 3D model. Its main job is to take a vertex's position in 3D space and convert it into the 2D coordinate where it will appear on the screen. It can also be used to move vertices around to create effects like waving flags or rippling water.

// This struct defines the data we get for each vertex
struct appdata
{
    float4 vertex : POSITION; // The vertex position
};

// This struct defines the data we send to the fragment shader
struct v2f
{
    float4 vertex : SV_POSITION; // The final screen position
};

// The vertex shader function
v2f vert (appdata v)
{
    v2f o;
    // Convert vertex position from local object space to screen space
    o.vertex = UnityObjectToClipPos(v.vertex);
    return o;
}

After the vertex shader runs, the GPU figures out which pixels on the screen are covered by the model's triangles. This step is called rasterization. For each of these pixels, it then runs the fragment shader.

The fragment shader (also called a pixel shader) runs once for every pixel that an object covers. Its main job is to return the final color for that pixel.

// The fragment shader function
fixed4 frag (v2f i) : SV_Target
{
    // Return a solid red color
    return fixed4(1.0, 0.0, 0.0, 1.0); // RGBA
}

If you put these pieces together, you get a shader that renders any object in a flat, bright red color.

Properties and Textures

Hard-coding a red color isn't very flexible. We can use the Properties block to let anyone change the color from the Unity Inspector.

First, we declare the property:

Properties
{
    _Color ("Main Color", Color) = (1,1,1,1) // Default is white
}

Then, we declare a matching variable in our HLSL code and use it in the fragment shader.

fixed4 _Color; // Variable name must match the property name

fixed4 frag (v2f i) : SV_Target
{
    // Return the color from our property
    return _Color;
}

Applying a texture works similarly. You define a texture property, pass texture coordinates (called UVs) from the vertex shader to the fragment shader, and then use a function to look up the color from the texture at those coordinates.

// In Properties block
_MainTex ("Albedo (RGB)", 2D) = "white" {}

// In HLSL code
sampler2D _MainTex;
float4 _MainTex_ST; // For tiling and offset, added by Unity

// Add UV coordinates to our structs
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

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

// In vertex shader
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

// In fragment shader
fixed4 frag (v2f i) : SV_Target
{
    // Sample the texture at the given UV coordinates
    fixed4 col = tex2D(_MainTex, i.uv);
    return col;
}

The tex2D function samples the _MainTex texture using the interpolated UV coordinates it receives from the vertex shader. The result is that the image is wrapped around the 3D model.

Quiz Questions 1/5

What is the primary role of a shader?

Quiz Questions 2/5

In the graphics pipeline, the vertex shader runs first, followed by rasterization, and then the fragment shader. True or False?

This covers the absolute basics. You now know how data flows through the pipeline and how to write simple programs that run on the GPU to control the shape and color of objects.