5.2 - A Primer on Shaders

The Big Picture

Section 12 of these tutorials will cover the GL Shader Language (GLSL) in detail. But you need to understand the “big picture” and a few basic language details to get started.

Shader programs work on individual vertices and individual fragments, one vertex and one fragment at a time. You don’t have control over calling the shader programs. They are called automatically by the graphics pipeline.

Before a shader can be used, you have to get a model’s data into a GPU vertex object buffer, which is just a contiguous block of memory that the GPU can directly access without using the system bus. A vertex object buffer must be organized as an array, where data can be accessed using an index to get sequential values out of the array. A model’s data is ideally copied to a vertex object buffer only once as a pre-processing step. You will create many vertex object buffers. Each one will store data for a particular attribute value of an array of vertices, or possibly all the vertex attributes for an entire model.

To render a model, your Javascript code will tell a shader program which vertex object buffer to use for its data source. When your JavaScript program calls gl.drawArrays(mode, first, count), the graphics pipeline will retreive vertices starting at the array[first] vertex and render count vertices from the array. If you made the function call gl.drawArrays(mode, 5, 10), it would render the vertices in array locations [5] to [14]. (Array indexes are always zero subscripted.)

../_images/drawing_modes.png

The mode parameter to the gl.drawArrays function will determine how the vertices are used to render elements. The possible drawing modes are shown in the diagram.

A vertex shader will perform identical calculations on each vertex it retrieves from the vertex object buffer. Each execution of the vertex shader is a unique run-time instance. You can’t set a variable in one execution and expect the variable to have that value in the next run-time instance. This is much different from your previous programming language experiences.

A vertex shader‘s job is to assign a special variable called gl_Position a vector of 4 values, (x,y,z,w). This value is passed on in the graphics pipeline to the next stage of the pipeline. Wow this is strange! This might not make much sense until you see more concrete examples. But keep reading!

../_images/pipeline.png

The output, gl_Position, of a vertex shader is passed to the next stage in the graphics pipeline - the “Viewport Transformation” stage. This converts the vertex’s position to a pixel location in the rendered image. This pixel location (and its associated rendering data) is passed to the next stage - the “Rasterizer”. This stage determines the pixels that an object covers. This varies based on whether the vertex is part of a point, a line, or a triangle. The “rasterizer” makes a list of all the pixels that need to colored to represent the object and then passes each pixel and its associated rendering data to your fragment shader. Remember that a pixel and its associated rendering data is called a fragment.

A fragment shader‘s job is to assign an RGBA color value to a special variable called gl_FragColor. This color is passed on to the “Composting” stage of the graphics pipeline and the color value is used to update the raster image that is being created.

Let’s summarize! Your vertex shader will position the vertices of a model. Then your fragment shader will assign a color to all of the pixels covered by an object defined by the vertices. It’s that simple, or that complex, depending on your perspective. Please pause and study the diagram of the graphics pipeline.

Fragments

Just to make things clear:

  • A vertex shader must set the value of one variable: gl_Position
  • A fragment shader must set the value of one variable: gl_FragColor

But what is passed between stages in the pipeline is more than just these values. Vertex shaders and fragment shaders share variables on a vertex basis. So a vertex shader will typically associate a color and other information with the gl_Position value. This information is passed on the next stage of the graphics pipeline. As we develop shader programs, we will try to make it clear how data is passed through the pipeline.

Shader Language Variables

GLSL is a strongly typed language. All variables must be declared before they are used. Variable declarations have three parts: “Storage Qualifier,” “data type,” and “variable name.”

“Storage Qualifiers” determine how a value changes during a call to gl.drawArrays(),

uniform A data value that is the same for an entire execution of gl.drawArrays().
attribute A data value that changes for every vertex of an execution of gl.drawArrays().
varying A data value that changes for every fragment of an execution of gl.drawArrays().

The “data type” determines the type of data a variable holds. There are many data types. For now, we will restrict ourselves to the following:

int A signed integer.
float A fractional number; approximately 7 digits of accuracy.
vec3 A vector of 3 floating point numbers.
vec4 A vector of 4 floating point numbers.
mat4 A 4x4 matrix of floating point numbers. (16 values)

Variable names start with a letter or underscore (_) and contain only letters, digits, and the underscore character. Variable names are case sensitive. A common convention, is to start uniform variables with a u_, attribute variables with a a_, and varying variables with a v_.

Execution of a shader program always starts with the main() function. You can implement as many sub-functions as your shader needs.

Variables declared outside of the main() function have global scope. If a variable by the same name is declared as global in both a vertex shader and a fragment shader it is the same variable. Any variables declared inside a function have local scope and local lifetime.

Note that JavaScript and GLSL are very different languages and you will have to keep their syntax and semantics separated.

All of these issues will make much more sense as we work through examples. If example shaders have extra verbiage that we have not discussed yet, just ignore it for now. The “extra stuff” will be explained in Section 12.

The Simplest Shaders Possible

Below is perhaps the simplest, functional shader programs possible. The vertex and fragment shader programs must be stored separately because they are compiled separately and then linked into a single shader program. The syntax is most closely aligned with the C programming language for function definitions, assignment statements, if statements, and loops. What is very different from the C language is the build-in data types, the casting of values into different data types, and the built-in vector and matrix operators.

Below is a very simple vertex shader, which transforms each vertex by a 4x4 transformation matrix.

// Vertex Shader
uniform   mat4 u_Transform;
uniform   vec4 u_Color;

attribute vec3 a_Vertex;

void main() {
  // Transform the location of the vertex
  gl_Position = u_Transform * vec4(a_Vertex, 1.0);
}

Below is a very simple fragment shader, which sets every pixel to the same color.

// Fragment shader
uniform vec4 u_Color;

void main() {
  gl_FragColor = u_Color;
}

Glossary

GL Shader Language (GLSL)
a computer programming language that can be compiled into GPU commands.
vertex shader
a computer program written in GLSL that positions the geometry of models in a scene.
fragment shader
a computer program written in GLSL that assigns a color to the pixels that compose a point, line or triangle.
pixel
a single color value in a raster image.
fragment
a group of data values used to calculate the color for an individual pixel.
gl
the typical name of the JavaScript object that holds a WebGL context for a canvas. All WebGL functionality is accessed through this object.
storage qualifier
defines how a value is used in the GPU. For a single rendering cycle created by calling gl.drawArrays(), uniform values remain constant, attibute values change for every vertex, and varying values change for every pixel.
data type
defines how the bits stored in memory are interpreted and what operations are valid on the bits.
variable
a memory location that stores a value and that can be referenced using an identifier. It is called a “variable” because its contents can change while a program is executing.
Next Section - 5.3 - A Primer on Buffer Objects