Shader Programming: Massively parallel art [Episode 1]
Shader Programming: Massively parallel art
Hi! I'm Rex. I thought I'd get started with STEEM by writing about something I love to teach others to do -- playing with shader programming to create beautiful graphics!
This series is intended for programmers of any level. No prior knowledge of graphics programming is necessary. A certain amount of maths agility will be helpful, but isn't required for a while!
PART 1: My First Shader
How GPUs used to work
In the fairly recent past, if you wanted to build a 3D game, you had certain limitations as to how you make surfaces look. You could provide lighting parameters, such as light position and colour, and material parameters such as specular coefficient. You could also specify an image to use as a texture; or multiple textures, and specify how they should be blended.
Although there was plenty of parameterisation, the GPU still executed a "fixed pipeline". Although it would use the provided parameters, it would plug them into unchangeable functions to calculate the colour of each screenspace pixel of a worldspace triangle.
How GPUs work now
Modern GPUs have replaced the fixed pipeline with a programmable pipeline. Now, you the programmer get to substitute in whatever formulae you want in order to render a given surface, in place of the previously unchangeable function.
This means that fancy surface effects such as normal mapping (bump mapping) become a lot easier. The programmable pipeline has lots of other implications too, but we're going to ignore most of that and focus on a very specific part that we can have a lot of fun with.
So what's a shader program?
A typical 3D scene contains thousands or millions of triangles. For every pixel of every one of the triangles we see on screen, a shader program is being run.
This little program accepts certain parameters, including the 3D worldspace coordinate of the pixel that's being calculated, and anything else it needs in order to render whatever sort of surface we're after. For example, to replicate classic fixed-pipeline-style Phong shading, we would also need a light vector.
The shader program has one job and one job only: To calculate a single, final RGBA colour value for the current pixel.
A typical modern GPU contains thousands of execution cores, each of which can execute a shader program simultaneously.
I have no idea what you're talking about
This all sounds very complicated! Why do we care? Well, forget about 3D scenes in a traditional sense. Imagine a "scene" in which the entire screen is just covered with a flat, 2D quad made up of 2 triangles.
By attaching a shader program to this scene, we get to write a program which dictates how every pixel should look, based on the XY screen coordinate of the pixel in question.
Our modern, incredibly powerful GPU, will execute this little program in parallel. It will run millions of times just to render a single frame, calling our little program over and over again.
The GPU will split execution of the shader program across all available shader cores, meaning we get to do massively parallel programming for very little effort -- and that's COOL, right?!
OK... so what does a shader program look like, and how do we run them?
We're going to use Shadertoy, a wonderful web-based environment which sets up a quad for us, accepts our shader program, and renders the result.
You'll need a browser that supports WebGL; yours probably does already.
Head to https://shadertoy.com and hit New, towards the top right of the screen. There's no need to register just yet, but eventually I hope you'll want to save your shader, which does need registration (it's free).
OK, in the code panel that you'll see on the right hand half of the screen, delete what's there and paste the following:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
fragColor = vec4(1.0, 0, 0, 1.0);
}
To recompile the shader, press the Play button at the bottom-left of the code panel. In the display panel, everything should go red! Let's examine why.
The language we're using here is GLSL (the OpenGL Shading Language); but don't worry about that. You'll pick it up as we go along!
Every shader program will start exactly the same way, with a mainImage function declared with the line:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
This function accepts one argument, fragCoord, and must set one output variable, fragColor.
fragCoord is a vec2: A 2-dimensional vector. This gives us the XY coordinates of the current pixel, which we can address as fragCoord.x and fragCoord.y.
fragColor is a 4-dimensional vector, and this is how we output the final colour for the pixel (in RGBA).
The only actual line of code in our shader program so far:
fragColor = vec4(1.0, 0, 0, 1.0);
This simply sets the RGBA pixel colour to {1.0, 0, 0, 1.0}, which results in full red. Each colour component is a float which ranges from 0.0 to 1.0, rather than 0 to 255 as you may be more used to.
Again, this one little function will be run millions of times, once for each screen pixel.
All you've done is make a red thing :/
Yes, but a red thing that renders on your GPU and uses all available shader cores to individually colour the pixels!
This is a trivial first shader program, but I can promise you that you'll be astounded by what can be created with only this simple paradigm of having a little program that gets to decide the colour of a given pixel, and runs across thousands of execution cores simultaneously.
If you need convincing, have a look at the Shadertoy homepage.
Next episode, I'll show you how we can get a lot more interesting than just plain red!