Credits:
Many thanks to [http://wiki.delphigl.com/index.php/Tutorial_glsl] for the great Delphi tutorial that give me the necessary information to learn GLSL and shader computation
Starting with an Linux system at first we have to check the availability of GLX supporting fragment shaders.
this can be done with:
~$ glxinfo | grep fragment |
output should be (important is the GL_ARB_fragment_shader):
GL_ARB_draw_buffers, GL_ARB_fragment_program, GL_ARB_fragment_shader, GL_ARB_fragment_program_shadow, GL_ARB_half_float_pixel, GL_ARB_fragment_coord_conventions, GL_ARB_provoking_vertex, |
If that works we come to our first example:
- 1. Open a window including an OpenGL context
- 2. Using a shader without textures
- 3. Using shader with texture as an input
Open a window including an OpenGL context
This will be done using the SDL library, because it’s open source and widely available. To preserve code structure the opengl context will be capsulated in a class called openglcontext therefore two files have to be created.
openglcontext.h openglcontext.cpp |
the .h file is the header file where the class definition and the interface of the class is defined. The .cpp file is the source code where the functions for the class are implemented.
The header file contains:
#ifndef H_OPENGLCONTEXT #define H_OPENGLCONTEXT class openglcontext { public: openglcontext(int sizexn, int sizeyn); }; #endif |
This is a very simple class definition containing just a constructor. The constructor is a method (functions belonging to a class are called methods) that is called whenever an object of the class openglcontext is constructed. In our source code file (openglcontext.cpp) we now have to implement the code for our constructor.
#include #include #include #include “stdio.h” #include “openglcontext.h” openglcontext::openglcontext(int sizexn, int sizeyn) |
As you can see we use some libraries (SDL, gl, glew, stdio) and the header file containing our class definition. The then defined constructor does several things. At first the SDL library is initialized for doublebuffered video output and an opengl accelerated window with the size sizexn x sizeyn is created. The number of texture units and the maximum texturesize for the created OpenGL-context is determined. After that the OpenGL extension wrapper library (glew) is initialized and checked for some necessary features.
Now we can try to compile our openglcontext-class therefore we type:
g++ -c openglcontext.cpp |
on an fresh linux install you will have to install the g++ compiler first:
ubuntu:> sudo apt-get install g++ |
also the libglew-dev and the libSDL1.2-dev packages are missing (this will install libgl-dev as an dependency):
ubuntu:> sudo apt-get install libglew-dev libSDL1.2-dev |
with the necessary libraries the compilation should be succesful and a file called openglcontext.o was created. This file contains the platform dependent binary code for the implemented functions. The next step is a main file using the openglcontext class. This file we will call main.cpp:
#include “openglcontext.h”
int main(int argc, char *argv[]) |
Compiling this file calling
g++ main.cpp |
will cause an undefined reference on the openglcontext constructor function, because we missed to link our object file (openglcontext.o) to the main executable. Instead we have to call
g++ main.cpp openglcontext.o |
but this is still not sufficient because with that, all references to funtions from our libraries are missing. Therefore we also have to link libSDL, libglew and libgl. This can be done with:
g++ main.cpp openglcontext.o -lSDL -lGL -lGLEW |
After the execution of this command a file called a.out should have been created, which is our executeable. We now can execute this programm calling
./a.out |
A window will be opened and instantly closed again and the OpenGL parameters determined in the constructor are printed to the console. The output should look something like:
user@ubuntu:~/asamgpu/tutorial/1$ ./a.out Textureunits: 8 Max Texturesize:8192 |
If this all works fine, we now have an environment for GPGPU computation. Therefore we will go further to the next step
Using a shader without textures
This example introduces fragment shaders without textures. A preliminary shader class will be defined which later on in this tutorial will be extended to texture support. Again we will encapsulate the shader functions in a shader class with the two files
shader.h shader.cpp |
The class definition:
#ifndef H_SHADER #define H_SHADER #include class shader void step(); #endif |
Beside the constructor two methods have to be implemented. The bind() method is called to set the used shader programm to the one contained in the corresponding object, and the step() method draws one rectangle using the certain shader, and with that performs the computation. The constructor reads two simple ASCII text files (vertex and fragment shader) and sends them to the GPU-driver for compilation and linking, so it can be used afterwards. So here the shader.cpp:
include |
Now that we have defined our shader class, we have to use it in our main.cpp, therefore we also need two additional files containing the source code for our vertex and fragment shader. Those two files should be named
firstshader.vsd firstshader.fsd |
the vertex shader is used to convert the coordinates for every edge vertex from three/four dimensional space onto the projection plane, the simplest method is:
void main() { gl_Position = vec4(gl_Vertex.xy, 0, 1); } |
the coordinates on the projection plane are just the first coordinates of the incoming vertex. The fragment shader is then used to fill up the triangle spanned by the vertices. The simplest one would just use one color for example red:
void main () { gl_FragData[0] = vec4(1.0,0.0,0.0,1.0); // red,green,blue,alpha } |
The next and final step is to alter the main.cpp to use the shader class. Currently our window closes instantly, that’s why we have to add a so called event handling loop as well.
#include “SDL/SDL.h” #include “openglcontext.h” #include “shader.h” int main(int argc, char *argv[]) int quit=0; |
compile the complete thing with
g++ -c shader.cpp openglcontext.cpp g++ main.cpp *.o -lGL -lGLEW -lSDL |
and run it with
./a.out |
you should see a red frame. Not that spectacular. Let’s add some fun :-).
Change the main.cpp to:
#include “SDL/SDL.h” #include “openglcontext.h” #include “shader.h” int main(int argc, char *argv[]) int quit=0; |
the vertex shader to:
void main() { gl_Position = vec4(gl_Vertex.xy, 0, 1); gl_TexCoord[0]= vec4(gl_Vertex.xy, 0, 1); //< -with this we can forward the position to the fragment shader } |
and the fragment shader to:
uniform int frame; // < -we get the integer from our CPU code
void main () |
compile and be happy :-D
the uniform variable may be used in the vertex shader as well:
uniform int frame; float time=float(frame)/60.; void main() |
Feel free to play around and build some cool effects. Feedback requested.
Using shader with texture as an input
A texture as we use it can be interpreted as a two dimensional image, consistent of many many small pixels, with four components each. The components are the red, green and blue channel and an additional alpha channel for effects like blending or transparency. In this example we will just build a simple texture and give it to the shader as an input data field.
Again we will encapsulate our texture functions in a class, so two new files are needed:
texture.h texture.cpp |
The header file:
#ifndef H_TEXTURE #define H_TEXTURE #include class texture texture(int sxn,int syn); // texture constructor #endif |
and the source code:
#include “GL/glew.h” #include “GL/gl.h” #include “texture.h” texture::texture(int sxn,int syn) // allocate buffer and init with some data // build texture // no interpolation always return the nearest pixel for float coordinates |
and finally the edited main.cpp:
#include “SDL/SDL.h” #include “openglcontext.h” #include “shader.h” #include “texture.h” // include textures int main(int argc, char *argv[]) int quit=0; |
compile the complete thing with
g++ -c shader.cpp openglcontext.cpp texture.cpp g++ main.cpp *.o -lGL -lGLEW -lSDL |
and run it with
./a.out |
what we could have learned from these code snippets is, how to upload a field of data to the GPU and use it in our shader calculation. The next step would be to get the result of the calculation back into another texture instead of drawing it on the screen. This enables us to re-use the results of the calculation and perform further steps with it, which is the basic principle of any iterative process like time integration or equation solvers. This can be done using textures as framebuffers.