GPU Shader Tutorial Logo
GPU Shader Tutorial
This tutorial is currently a work in progress. Content may be added, updated, or removed at any time.

Shader Basics - Fragment Shader

What is a fragment shader

Similar to how a vertex shader operates on vertices of an object, a fragment shader operates on a "fragment" of an object and tells what the color of that fragment is supposed to be.

Since it is executed per fragment on all fragments generated by the GPU pipeline, any operation that requires modification of the color of the fragment (like brightening due to lights or darkening due to shadows) can be done through the fragment shader.

What is a fragment

As previously mentioned in the render pipeline overview, a fragment is a sample of a primitive that contains certain information required for coloring a pixel.

A pixel can consist of multiple fragments, because, depending upon what area of the primitive the pixel covers, there can be multiple values present within the pixel, which have to either be combined, or just one selected at random.

As an example, take a circle that is to be rendered on a screen of size 8x8 pixels (total of 64 pixels), as shown by the image below:

Fragment Example Part 1

After splitting the circle into 16 equal parts, a decision on what the color of each pixel should be needs to be made. For pixels with just a single color, they just adopt that color.

However, the other pixels contain two colors, as they contain both the circle and the background. This requires a decision needs to be made as to what the final color of the pixel should be.

In order to do this, a sample can be taken from somewhere in the area the pixel covers, and fix that as the final color of the pixel. This sample is what is considered a "fragment".

If only one "fragment" is requuired per pixel, then a sample from the center of each pixel can be taken, resulting in the render below:

Fragment Example Part 2

Instead, if multiple fragments are taken, a final color value for the pixel can be interpolated based on what the color of each fragment is.

In the case of the circle, by taking 4 "fragments" (one from approximately each corner), the final color would be a reddish-pink, since two of these fragments would have the color red, and the other two would have the color white:

Fragment Example Part 3

While not as accurate as our initial image, it is still closer to reality compared to the first result. If you're not sure how, let's see how these images look at 30px width and height.

Fragment Example Part 2 Mini

Fragment Example Part 3 Mini

At a much smaller scale, the second result looks considerably more like a circle than the first result. This is how certain anti-aliasing methods works.

Along with this case, primitives can also overlap other primitives, which means fragments can overlap other fragments. This requires fragments to be discarded if they are covered, or combined with other fragments if some of them are not opaque (a fragment from a translucent glass over an object).

Do note that in DirectX, fragments are called pixels (and by extension, fragment shaders are called pixel shaders), but that name isn't technically accurate.

An example - The triangle returns

Let's go back to our standard triangle example. Previously, we only had the edges of the triangle drawn to provide an explanation on how vertex shaders work. This time we'll be coloring the entire triangle.

Cannot run WebGL examples (not supported)
Triangle Vertices:
    Vertex 1: { x: 0.000, y: 1.000, z: 0.000 }
    Vertex 2: { x: -0.866, y: -0.500, z: 0.000 }
    Vertex 3: { x: 0.866, y: -0.500, z: 0.000 }

How it works

Fragment Shader Code:

1
2
3
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Looking at the code, you'll see the fragment shader is extremely simple, it just generates a vec4 of 1.0, 0.0, 0.0, 0.0, which corresponds to R, G, B, and A values respectively. The color is then assigned to the special output variable defined in WebGL called gl_FragColor (WebGL Fragment Color)

In the vertex shader examples, WebGL was told that the three vertices provided were part of a line loop, meaning they are coordinates that define a line that loops back around to the start.

So when WebGL would call the fragment shader, it would color the fragments of the lines that joined the first and second vertex, then the line connecting the second and third vertex, and (since it was defined to be a loop), the line connecting third and first vertex.

In this case, since WebGL is told the vertices belong to a triangle, the 3 vertices are taken and used to create a triangle, and that triangle is then split into fragments, with each fragment then colored by the fragment shader.

Another example - A triangular color wheel

What if we wanted to define our own color values that should be used to color the pixels? Just like with the vertex shader, we can pass our own needed values to the fragment shader.

These values have to be passed through the vertex shader, meaning that when the vertex shader receives the value, it has to set that value to another variable, which is then received by the fragment shader.

One very important note is that you can only define the color values of each vertex, and not each fragment or pixel, since their number is an unknown (you won't know how many fragments may be generated, or pixels that the object may cover).

So if we can only define color values for each vertex, how will the fragment shader know what color it should receive for pixels that are inside the triangle not defined by any vertex? Let's take a look at an example.

Cannot run WebGL examples (not supported)
Triangle Vertices:
    Vertex 1: { x: 0.000, y: 1.000, z: 0.000 }
    Vertex 2: { x: -0.866, y: -0.500, z: 0.000 }
    Vertex 3: { x: 0.866, y: -0.500, z: 0.000 }
Vertex Colors:
    Vertex 1: { r: 1.000, g: 0.000, b: 0.000 }
    Vertex 2: { r: 0.000, g: 1.000, b: 0.000 }
    Vertex 3: { r: 0.000, g: 0.000, b: 1.000 }

How it works

As is visible from the result, since we set the colors of each vertex have been set to red, green, and blue, those corners of the triangles have been colored appropriately.

However, the rest of the triangle seems to be a color mixture of all these colors in specific ratios. This is done through interpolation.

When we set the colors of the vertices, and then the object/primitive is set into fragments, the fragments covering the vertices automatically get the color of that vertex, since that is what that fragment represents.

The color of other fragments is determined by checking its position and distance relative to every vertex, and interpolating what its color value should be based upon the distance.

So, for example, as a fragment moves further away from vertex 2 and moves closer to vertex 3, the green color component slowly fades away and is slowly overtaken by the blue component.

Let's look at the code for the shaders in this example:

Vertex Shader Code:

1
2
3
4
5
6
7
8
9
10
11
attribute vec4 vertexPosition;
attribute vec3 vertexColor;

uniform mat4 mvpMatrix;

varying highp vec3 color;

void main() {
  gl_Position = mvpMatrix * vertexPosition;
  color = vertexColor;
}

Fragment Shader Code:

1
2
3
4
5
varying lowp vec3 color;

void main() {
  gl_FragColor = vec4(color, 1);
}

We pass the color of the vertices to their respective vertex shader by passing the color values as an attribute (since the color is different per vertex).

The vertex shader then passes this on to the fragment shader through the vec3 varying named color.

The color is defined as a varying is because while it is fixed per vertex, it will need to be interpolated when passed to fragments, depending on their position relative to the vertices.

It is also defined as lowp which just means the precision of it's value is low (we can ignore these sorts of qualifiers).

As a result, when we pass the color of the vertices down to the fragment shaders, the value is interpolated based on the position of the fragment, and then passed to the fragment.

Just as attributes are read-only for the vertex shader and can differ per vertex, varyings are read-only for the fragment shader and can differ per fragment (they are write only for the vertex shader).

A final example - A pulsing triangle color wheel

Cannot run WebGL examples (not supported)
Triangle Vertices:
    Vertex 1: { x: 0.000, y: 1.000, z: 0.000 }
    Vertex 2: { x: -0.866, y: -0.500, z: 0.000 }
    Vertex 3: { x: 0.866, y: -0.500, z: 0.000 }
Vertex Colors:
    Vertex 1: { r: 1.000, g: 0.000, b: 0.000 }
    Vertex 2: { r: 0.000, g: 1.000, b: 0.000 }
    Vertex 3: { r: 0.000, g: 0.000, b: 1.000 }
Time: 2275.1331189999764

How it works

Similar to how we did the rotating triangle in the previous chapter, we determine how much to shift the color by relative to the current timestamp.

By subtracting the calculated color shift from the interpolated fragment color, we can have the color of the triangle oscillate from pure black, to the standard triangle color wheel, to pure white, and back again.

Fragment Shader Code:

1
2
3
4
5
6
7
8
varying highp vec3 color;

uniform highp float time;

void main() {
  highp float colorShift = cos(time / 500.0);
  gl_FragColor = vec4(clamp(color - colorShift, 0.0, 1.0), 1.0);
}

The color shift is calculated in the shader by taking the time elapsed since the start of the animation as an input, dividing it by 500 so that the animation runs slower, and then finding the cosine of the elapsed time.

The time is passed in milliseconds, and the cos function in glsl takes time in radians. So the animation depends on every half second passed.

Since the division results in a floating point number, this number does change every frame, which results in the color shift value also updating accordingly.

clamp is a function that takes a specific value (color - colorShift) and makes sure it doesn't go outside of a certain range (0.0 and 1.0), "clamping" it to either end depending on if it is too large or too small. It also works with both vector (vec2, vec3, vec4) and scalar values (float, int)

Summary

  • The fragment shader receives a fragment from a list of fragments and sets the appropriate color value for that fragment.
  • At least one fragment that comes under a pixel is used when determining the color of that pixel.
  • The fragment shader requires certain values in order to determine what the final color of the fragment should be.
    • If this data is passed through the vertex shader, its actual value will be interpolated by the GPU based on the distance of the fragment from each vertex of the primitive.
    • If the data is passed as a uniform, then the GPU will not interpolate its actual value, since it is supposed to be uniform among all fragments.