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 - Vertex Shader

What is a vertex shader

Vertex shaders process vertices and tells what their coordinates are in "clip-space", which is a space that makes it easy for computers to understand which vertices are visible to the camera and which are not and have to be cut or "clipped" out.

This makes it faster for GPUs during later stages since they have less data to work with.

They perform this process by receiving a single vertex from the list of vertices as input, and return a result that determines where the vertex should be present within clip-space,

Since this shader is executed per vertex on all vertices passed to the GPU pipeline, any operation that requires modifications to the vertex can be performed during in this shader, as long as the final output is where the vertex is to be placed in the clip-space.

An example - A triangle

Below is an example of the work simple vertex shader:

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 }

We can see that, for a provided set of vertex positions, a shape is drawn. The points on the canvas where the vertices are placed is determined by the vertex shader (in part).

How it works

Let's look at the code for the vertex shader

Vertex Shader Code:

1
2
3
4
5
6
7
8
9
attribute vec4 vertexPosition;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;

void main() {
  gl_Position = projectionMatrix * viewMatrix * modelMatrix * vertexPosition;
}

Even though this is WebGL (which is similar to OpenGL), the concepts applied here can be mapped across other languages as well.

The void main function is the primary function that is executed when the vertex shader (and any shader for that matter) is to executed, and should contain the primary shader code to be executed. This is similar to C/C++, where the main function in the primary entry is the one that is executed when starting the application.

The vertexPosition attribute is a property that receives the initial coordinates of vertex as it's primary input, the one that it should transform into the final clip-space coordinates.

It is defined as an attribute as it is a description of a certain "attribute" of the vertex (in this case, its position), can change based on which vertex is being operated on, and are always read-only.

Do note that the type of it is set to vec4, which means it's a vector of size 4, but vertices passed to the vertex shader need not be limited to just this type, but a vec4 type would provide the most detail about the coordinates of a vertex.

The modelMatrix, viewMatrix, and projectionMatrix uniforms are additional properties passed separately to the vertex shader. Unlike the vertexPosition attribute, these values need to be the same for every vertex of the object/primitive operated on, which is why it's defined as uniform (it's uniform/same for every data being operated on by shaders).

Similar to the vertexPosition attribute, the types of these uniforms is mat4, which means a matrix of size 4x4. Again, they need not be limited to this type, and can be a matrix of size upto 4x4, or even a vector of size upto 4.

Here's a simple explanation of the uniform variables being used:

1. modelMatrix

This matrix is used to represent where the vertex exists within the world. It represents the center of the model being drawn by the shader, which has been translated, rotated, and/or scaled into the necessary position in the world.

Multiplying the vertex coordinate with this matrix will provide the result of where the vertex exists in the world w.r.t. to the center of the model it belongs to.

2. viewMatrix

This matrix is used to represent where the vertex exists relative to your view, or more specifically the cameras view. Once the vertex position is known in the world (by multiplying it with the modelMatrix), it's position relative to the camera can be determined by multiplying it with this matrix.

3. projectionMatrix

This matrix is used to represent the perspective of the camera. Things like field-of-view, aspect ratio, and others can distort and affect the way objects look. Likewise, the scale of objects can differ depending on distance, which needs to be accounted for.

By multiplying the projection matrix onto the result of the previous calculations, we are able to map the vertex onto the perspective of the camera, taking into account it's aspect ratio, field-of-view, and the farthest and closest it can see.

This final calculation provides us the coordinates of the vertex in clip-space.

From the above explanation, it should be more obvious why the operation on line 8 in the vertex shader is performed.

  1. The vertex position is taken and multiplied with the model matrix to determine where that vertex lies w.r.t the center of the model in the world.
  2. The result is then multiplied with the view matrix to determine where the vertex is positioned w.r.t the camera
  3. Finally the result of the second operation is multiplied with the projection matrix to determine where the vertex is located within the perspective of the camera.

This final result is the output of the vertex shader, which, in WebGL, is stored in the special variable gl_Position.

Another example - A rotating triangle

Since the vertex shader determines where each vertex is w.r.t the perspective-space of the screen, by passing it the necessary transformations to apply to the vertices, it can move their positions as required.

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 }
Time: 2210.1782949999906

How it works

Vertex Shader Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
attribute vec4 vertexPosition;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float time;

const float PI = 3.1415926535897932384626433832795;

mat4 rotateZ(float angle) {
  mat4 rotationMatrix;
  rotationMatrix[0] = vec4(cos(angle), sin(angle), 0, 0);
  rotationMatrix[1] = vec4(-sin(angle), cos(angle), 0, 0);
  rotationMatrix[2] = vec4(0, 0, 1, 0);
  rotationMatrix[3] = vec4(0, 0, 0, 1);
  return rotationMatrix;
}

void main() {
  float angleRadians = (time / 30.0) * PI / 180.0;
  mat4 rotatedModelMatrix = rotateZ(angleRadians) * modelMatrix;
  gl_Position = projectionMatrix * viewMatrix * rotatedModelMatrix * vertexPosition;
}

The time elapsed since the start of the animation is passed to the vertex shader. From this, the angle is calculated by dividing the elapsed time by 30, so that a degree turn occurs every 30ms.

This angle is calculated in degrees, which needs to be converted to radians, which is done by mulitplying the angle by [AsciiMath Syntax:] pi and dividing the product by 180.

By keeping the calculations in float, we don't round of the decimals, ensuring that the angle always changes (primarily in the fractional part) with every frame. The function

This angle is then used to create a rotation matrix. The function rotateZ creates a rotation matrix that rotates around the Z-axis. The details about this matrix is in the reference link provided in the matrix mathematics section of the mathematics primer chapter.

The rotation matrix is multiplied with the model matrix to rotate the entire model according to the time elapsed. Then the same process as with the previous example is followed to calculate the final coordinates of the vertex.

Additional Notes

This rotation matrix can be more easily created outside the shader since there are utility libraries that provide helper functions for performing such operations (OpenGL has the GLM library for such tasks).

However, for the sake of understanding how the rotation works, it is shown within the shader.

We also calculate the multiplication of the model, view, and projection matrix within the vertex shader itself. This is a calculation whose result never changes for any vertex.

Since the result of the calculation is a constant, it can be done once outside the GPU and then passed as a uniform to the vertex shader. This optimization will be done in future examples and chapters.

This will be visible in vertex shaders where we pass an uniform called mvpMatrix, which is the multiplication product of the model, view, and projection matrices. Any additional transformations (like rotations) will also be calculated beforehand and then passed through this uniform.

Summary

  • The vertex shader receives a vertex from a list of vertices and plots it onto a space known as the clip-space.
  • The vertex shader requires certain values provided to it about the model, view, and projection, in order to be able to determine where the final position of the vertex is.
  • Since the shader determines where the vertex is present within this space, it can manipulate and transform the vertex to be placed wherever required.