A lot of people have commented that
This topic aims to help change that over a few posts by teaching some concepts about modern OpenGL, especially shader programming.
We'll be using ShaderToy for the first few examples so you can make something pretty before anything really scary sets in, which means you should probably make an account there now.
But let's start with the basics, some of which you probably already know. Many graphics-related tasks can be handled in an embarrassingly parallel way. This means that the job can be broken down into many parts, none of which really have to coordinate with one another and can work in parallel, pretty much ignoring what the other ones are doing. Even better, they're usually doing the same thing at the same time on each part of the job, just working with different input data. A Graphics Processing Unit, or "GPU", is a piece of hardware within your computer that allows these calculations to be done much faster than would otherwise be possible by providing many processing cores running in parallel (individually at a much slower speed than your CPU, but in bulk potentially much faster), but whose control-flow is locked together in groups. Traditionally, GPUs are also very fast at floating point arithmetic (useful for graphics applications), and not so good at integer arithmetic.
The OpenGL APIs provide a way to manipulate the internal state of the GPU and thereby control which data it works on. Older graphics cards only knew how to perform a fixed set of lighting operations on this data, called the "fixed function pipeline", but modern graphics cards are more programmable and allow you to write small programs, consisting of functions called "shaders", to control the lighting output yourself (and much much more)! While OpenGL can be accessed in many programming languages, the shaders themselves must be written in a special purpose language, called "GLSL" (for OpenGL Shading Language) which can be compiled into GPU instructions by your graphics card drivers.
GLSL programs are look like a specialized form of C (so it'll be helpful, but not strictly necessary, to have some familiarity with C-like languages, before we start), and are organized into several stages, each with its own shaders, and its own special-purpose inputs and outputs. Before we explain these stages, its important to understand the basic kinds of data and variables you'll be working with.
First, we have a subset of the traditional C data types:
GPUs are very well optimized for floats, so use those wherever possible!
Second, we have vectors, which can be 2, 3, or 4 element sequences of those same types, which can be operated on in parallel.
Vectors can be recombined using a technique called "swizzling". It looks a bit like accessing a struct member in C, but you can get a bit more fancy!
Code:
In case it's not obvious from the code-snippet, after running this code, we would end up with f = 2.0, halfAsMuchFun = vec2(1.0,2.0), and woahMagic = vec4(1.0,3.0,3.0,2.0).
The vector components are named either x,y,z,w (for geometric coordinates, see footnote [1] if you're curious about w); s,t,p,q (for texture coordinates); or r,g,b,a (for colors - red, green, blue, alpha). It doesn't matter which ones you use where, as long as you don't mix them in a single swizzle! But everyone will appreciate it if you use the ones that fit the meaning of your code!
Vectors can be operated on in exactly the same way as scalars as long as the vectors are the same lengths, and the operation is performed between each pair of corresponding elements. So vec4(1,2,3,4)*vec4(1,2,3,4) would produce vec4(1,4,9,16).
Third, we have matrices, which are 2-dimensional structures, which once again can have dimensions from 2 to 4.
Matrices are always floats, unless you give them a dmat type, in which case you get a double matrix. Unlike vectors, you can't swizzle them, you use C-like array indexing instead (e.g. matrixName[0][0])! However if you apply only one index instead of two, you get a vector and can swizzle that (e.g. matrixName[0].xyzw)!
Finally, we have opaque types, such as samplers and images. Unlike the previous 3 sets of types, opaque types don't have a value which is directly accessible to you the programmer. Variables of an opaque type can only be accessed by passing them to a builtin function.
-------------------------------------------------------
Now that we understand the types we're working with, I'll explain the first two (and most important) types of shaders: vertex shaders, and fragment shaders.
A vertex shader, conceptually, transforms points (represented with vectors), and their associated attributes (for example colors, or texture coordinates, or other things), within a scene. Importantly nearly all scene elements to be drawn are represented by collections of vertices organized into triangles. Triangles are special because three points can always be formed into a flat triangle without worrying that it will have weird bends in the surface, or that the points will be tangled about each other. Triangles can be considered to be facing towards or away from the screen depending on the order the points are listed in (clockwise or counter-clockwise), but we won't worry about that too much for now.
A fragment shader, conceptually, takes the screen-space coordinates for pieces of a triangle (usually representing about 1 pixel's worth of the triangle), and returns a color to draw for that piece of the triangle! One reason why floating point values are used everywhere, is because given a triangle, the GPU can smoothly blend between the attributes and coordinates at the vertices, which will turn out to be very important!
There are other kinds of shaders, but now that we can turn points into colors, we have everything we need to get started! Go back to ShaderToy, and click "New Shader" at the top. Hopefully you're using a modern browser and you'll see some smoothly changing colors in a rectangle to the left,
and code like this:
Code:
to the right. Noteably, there's only one shader here! The kind folks at ShaderToy have made our job even easier by making it so we only have to worry about our fragment shader, by drawing a simple rectangle for us to work from!
Click the arrow next to "Shader Inputs" above your code and you'll see a collection of variables labeled with comments. But they're all declared with the word "uniform"! A uniform input is one that is the same across all copies of the shader regardless of which portion of the data they're working on. The special data-specific variables are hidden from us here, but they're declared with either the in or the out keyword! You'll notice that our main function returns nothing and takes no function arguments! That's because everything we need comes to us as either a uniform variable, or an in variable, or is handed off to the next stage in an out variable. Names prefixed with gl_ are special system-provided inputs and outputs that you don't even have to declare for yourself! In this case gl_FragCoord is an input, and gl_FragColor is an output.
Let's click at the bottom in the black box labeled iChannel0. This will pick an input to the uniform sampler variable called iChannel0 from our inputs box. For now, choose one of the textures with rgba format (it will look like noise, but having an alpha channel will be fun). Once you've selected a texture, close the selection box and return to your code. Now that we have a texture, let's sample it! Below the declaration of uv, let's add a new line:
Code:
We've passed our sampler (which is an opaque type, remember?) for the noise texture into a function with a normalized 2d coordinate pair (meaning values between 0 and 1 - i.e. as a fraction of width/height), representing where in the image to draw our sample color. If we were compiling for a newer version of the shader language, the texture2D function might be called just texture instead, so if you run into errors on your own later, keep that in mind!
Now let's do something with that color! Replace the assignment to gl_FragColor with a new variable, so we'll have two color variables!
Code:
Now what should we do with them? The simplest thing is an overlay using the alpha (transparency) from the textured image:
Code:
Here, we've used the "mix" function, which uses the standard alpha blending formula, taking in two colors, and an alpha parameter, and outputting a new mixed color.
This is a little too simple for our first shader, so let's change it again! Instead of assigning just a single color, let's add an if statement!
Code:
Now we only mix in the center half of the screen, and otherwise just take the sampled color! But this brings up an important teaching moment: remember how I said that GPUs run in lock-step on each core? That means that if statements are bad for your performance (especially nested), because all of the cores have to step through the code for both branches together, even if some are ignoring the result of that branch! Designing your shaders to cleverly avoid if statements is an important skill for a GPU programmer (and has a fancy name called designing for "uniform flow control"). Violating this principle can have other negative repercussions, but we won't worry about them for today.
Play around with ShaderToy some more to get comfortable, and come back here to share your creations (and hopefully tune in for the next part of this tutorial). And don't forget to explore their gallery to see some of the awesome things people can do with only a fragment shader!
[1] GPUs allow us to work in Homogeneous coordinates, which allow us to handle cool transformations very easily. They'll be discussed more in a later post.
- graphics programming seems pretty cool, and
- it seems scary and intimidating and they don't know where to start.
This topic aims to help change that over a few posts by teaching some concepts about modern OpenGL, especially shader programming.
We'll be using ShaderToy for the first few examples so you can make something pretty before anything really scary sets in, which means you should probably make an account there now.
But let's start with the basics, some of which you probably already know. Many graphics-related tasks can be handled in an embarrassingly parallel way. This means that the job can be broken down into many parts, none of which really have to coordinate with one another and can work in parallel, pretty much ignoring what the other ones are doing. Even better, they're usually doing the same thing at the same time on each part of the job, just working with different input data. A Graphics Processing Unit, or "GPU", is a piece of hardware within your computer that allows these calculations to be done much faster than would otherwise be possible by providing many processing cores running in parallel (individually at a much slower speed than your CPU, but in bulk potentially much faster), but whose control-flow is locked together in groups. Traditionally, GPUs are also very fast at floating point arithmetic (useful for graphics applications), and not so good at integer arithmetic.
The OpenGL APIs provide a way to manipulate the internal state of the GPU and thereby control which data it works on. Older graphics cards only knew how to perform a fixed set of lighting operations on this data, called the "fixed function pipeline", but modern graphics cards are more programmable and allow you to write small programs, consisting of functions called "shaders", to control the lighting output yourself (and much much more)! While OpenGL can be accessed in many programming languages, the shaders themselves must be written in a special purpose language, called "GLSL" (for OpenGL Shading Language) which can be compiled into GPU instructions by your graphics card drivers.
GLSL programs are look like a specialized form of C (so it'll be helpful, but not strictly necessary, to have some familiarity with C-like languages, before we start), and are organized into several stages, each with its own shaders, and its own special-purpose inputs and outputs. Before we explain these stages, its important to understand the basic kinds of data and variables you'll be working with.
First, we have a subset of the traditional C data types:
- bool: conditional type, values may be either true or false
- int: a signed, two's complement, 32-bit integer
- uint: an unsigned 32-bit integer
- float: an IEEE-754 single-precision floating point number
- double: an IEEE-754 double-precision floating-point number
GPUs are very well optimized for floats, so use those wherever possible!
Second, we have vectors, which can be 2, 3, or 4 element sequences of those same types, which can be operated on in parallel.
- bvecn: a vector of booleans
- ivecn: a vector of signed integers
- uvecn: a vector of unsigned integers
- vecn: a vector of single-precision floating-point numbers
- dvecn: a vector of double-precision floating-point numbers
Vectors can be recombined using a technique called "swizzling". It looks a bit like accessing a struct member in C, but you can get a bit more fancy!
Code:
vec4 funVec = vec4(1.0,2.0,3.0,4.0);
float f = funVec.y; // This should be pretty obvious!
vec2 halfAsMuchFun = funVec.xy; // This should be pretty obvious too!
vec4 woahMagic = funVec.xzzy; // Like I said, magic!
In case it's not obvious from the code-snippet, after running this code, we would end up with f = 2.0, halfAsMuchFun = vec2(1.0,2.0), and woahMagic = vec4(1.0,3.0,3.0,2.0).
The vector components are named either x,y,z,w (for geometric coordinates, see footnote [1] if you're curious about w); s,t,p,q (for texture coordinates); or r,g,b,a (for colors - red, green, blue, alpha). It doesn't matter which ones you use where, as long as you don't mix them in a single swizzle! But everyone will appreciate it if you use the ones that fit the meaning of your code!
Vectors can be operated on in exactly the same way as scalars as long as the vectors are the same lengths, and the operation is performed between each pair of corresponding elements. So vec4(1,2,3,4)*vec4(1,2,3,4) would produce vec4(1,4,9,16).
Third, we have matrices, which are 2-dimensional structures, which once again can have dimensions from 2 to 4.
- matnxm: A matrix with n columns and m rows. OpenGL uses column-major matrices, which is standard for mathematics users. Example: mat3x4.
- matn: A (square) matrix with n columns and n rows. Shorthand for matnxn
Matrices are always floats, unless you give them a dmat type, in which case you get a double matrix. Unlike vectors, you can't swizzle them, you use C-like array indexing instead (e.g. matrixName[0][0])! However if you apply only one index instead of two, you get a vector and can swizzle that (e.g. matrixName[0].xyzw)!
Finally, we have opaque types, such as samplers and images. Unlike the previous 3 sets of types, opaque types don't have a value which is directly accessible to you the programmer. Variables of an opaque type can only be accessed by passing them to a builtin function.
-------------------------------------------------------
Now that we understand the types we're working with, I'll explain the first two (and most important) types of shaders: vertex shaders, and fragment shaders.
A vertex shader, conceptually, transforms points (represented with vectors), and their associated attributes (for example colors, or texture coordinates, or other things), within a scene. Importantly nearly all scene elements to be drawn are represented by collections of vertices organized into triangles. Triangles are special because three points can always be formed into a flat triangle without worrying that it will have weird bends in the surface, or that the points will be tangled about each other. Triangles can be considered to be facing towards or away from the screen depending on the order the points are listed in (clockwise or counter-clockwise), but we won't worry about that too much for now.
A fragment shader, conceptually, takes the screen-space coordinates for pieces of a triangle (usually representing about 1 pixel's worth of the triangle), and returns a color to draw for that piece of the triangle! One reason why floating point values are used everywhere, is because given a triangle, the GPU can smoothly blend between the attributes and coordinates at the vertices, which will turn out to be very important!
There are other kinds of shaders, but now that we can turn points into colors, we have everything we need to get started! Go back to ShaderToy, and click "New Shader" at the top. Hopefully you're using a modern browser and you'll see some smoothly changing colors in a rectangle to the left,

and code like this:
Code:
void main(void)
{
vec2 uv = gl_FragCoord.xy / iResolution.xy;
gl_FragColor = vec4(uv,0.5+0.5*sin(iGlobalTime),1.0);
}
to the right. Noteably, there's only one shader here! The kind folks at ShaderToy have made our job even easier by making it so we only have to worry about our fragment shader, by drawing a simple rectangle for us to work from!
Click the arrow next to "Shader Inputs" above your code and you'll see a collection of variables labeled with comments. But they're all declared with the word "uniform"! A uniform input is one that is the same across all copies of the shader regardless of which portion of the data they're working on. The special data-specific variables are hidden from us here, but they're declared with either the in or the out keyword! You'll notice that our main function returns nothing and takes no function arguments! That's because everything we need comes to us as either a uniform variable, or an in variable, or is handed off to the next stage in an out variable. Names prefixed with gl_ are special system-provided inputs and outputs that you don't even have to declare for yourself! In this case gl_FragCoord is an input, and gl_FragColor is an output.
Let's click at the bottom in the black box labeled iChannel0. This will pick an input to the uniform sampler variable called iChannel0 from our inputs box. For now, choose one of the textures with rgba format (it will look like noise, but having an alpha channel will be fun). Once you've selected a texture, close the selection box and return to your code. Now that we have a texture, let's sample it! Below the declaration of uv, let's add a new line:
Code:
vec4 sampledColor = texture2D(iChannel0, uv);
We've passed our sampler (which is an opaque type, remember?) for the noise texture into a function with a normalized 2d coordinate pair (meaning values between 0 and 1 - i.e. as a fraction of width/height), representing where in the image to draw our sample color. If we were compiling for a newer version of the shader language, the texture2D function might be called just texture instead, so if you run into errors on your own later, keep that in mind!
Now let's do something with that color! Replace the assignment to gl_FragColor with a new variable, so we'll have two color variables!
Code:
vec4 timerColor = vec4(uv,0.5+0.5*sin(iGlobalTime),1.0);
Now what should we do with them? The simplest thing is an overlay using the alpha (transparency) from the textured image:
Code:
gl_FragColor = mix(timerColor, sampledColor, sampledColor.a);
Here, we've used the "mix" function, which uses the standard alpha blending formula, taking in two colors, and an alpha parameter, and outputting a new mixed color.
This is a little too simple for our first shader, so let's change it again! Instead of assigning just a single color, let's add an if statement!
Code:
if(uv.x > 0.25 && uv.x < 0.75){
gl_FragColor = mix(timerColor, sampledColor, sampledColor.a);
} else {
gl_FragColor = sampledColor;
}
Now we only mix in the center half of the screen, and otherwise just take the sampled color! But this brings up an important teaching moment: remember how I said that GPUs run in lock-step on each core? That means that if statements are bad for your performance (especially nested), because all of the cores have to step through the code for both branches together, even if some are ignoring the result of that branch! Designing your shaders to cleverly avoid if statements is an important skill for a GPU programmer (and has a fancy name called designing for "uniform flow control"). Violating this principle can have other negative repercussions, but we won't worry about them for today.

Play around with ShaderToy some more to get comfortable, and come back here to share your creations (and hopefully tune in for the next part of this tutorial). And don't forget to explore their gallery to see some of the awesome things people can do with only a fragment shader!
[1] GPUs allow us to work in Homogeneous coordinates, which allow us to handle cool transformations very easily. They'll be discussed more in a later post.