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.)
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!
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, andvarying
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.