ASAMgpu Tutorial

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. 1. Open a window including an OpenGL context
  2. 2. Using a shader without textures
  3. 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)
{
GLint intbuf;
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );
SDL_SetVideoMode( sizexn, sizeyn, 32, SDL_OPENGL);
glGetIntegerv(GL_MAX_TEXTURE_UNITS, &intbuf);
printf(“Textureunits: %d\n”,intbuf);
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &intbuf);
printf(“Max Texturesize:%d\n”,intbuf);
GLenum err=glewInit();
if(err!=GLEW_OK){printf(“glew error:%s\n”,glewGetErrorString(err));}
if(!GLEW_ARB_vertex_shader){printf(“system doesn’t support ARB_vertex_shader_extension!\n”);}
if(!GLEW_ARB_fragment_shader){printf(“system doesn’t support ARB_fragment_shader_extension!\n”);}
if(!GL_EXT_framebuffer_object){printf(“system doesn’t support framebuffer_extension!\n”);}
}

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[])
{
openglcontext c(800,600);
}

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
{
public:
GLhandleARB Handle,VertexHandle,FragmentHandle;

void step();
void bind();
shader(const char *filename);
};

#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 
#include 
#include 
#include 
#include "shader.h"

shader::shader(const char *filename)
{
 char buf[255];
 FILE *f;
 int infologLength=0;
 int charsWritten =0;
 char *infoLog;
 char *source;
 size_t back;
 long lCurPos, lEndPos, lHeaderLength;

 printf("init shader:%s\n",filename);
 // read and compile fragment shader from ascii-file
 sprintf(buf,"%s.fsd",filename);                                  // add extension to filename
 f=fopen(buf,"r");                                                // openfile
 fseek(f, 0, 2);                                                  // go to the end of file
 lEndPos = ftell(f);                                              // get position (size of file)
 source=new char[lEndPos+1];                                      // allocate memory for code
 fseek(f, 0, 0 );                                                 // go to begin of file
 back=fread(source,1,lEndPos,f);                                  // read file
 source[lEndPos]=(char) NULL;                                     // add NULL-termination for string
 fclose(f);                                                       // close file

 const GLcharARB *pgms=source;
 FragmentHandle=glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);  // create shader object
 glShaderSourceARB(FragmentHandle, 1, &pgms ,NULL );              // set source for object
 glCompileShaderARB(FragmentHandle);                              // compile shader
 delete[] source;                                                 // delete source

 // error handling
 glGetObjectParameterivARB(FragmentHandle, GL_OBJECT_INFO_LOG_LENGTH_ARB, &infologLength);
 if (infologLength>0)
 {
  infoLog=new char[infologLength];
  glGetInfoLogARB(FragmentHandle, infologLength, &charsWritten, infoLog);
  printf("%s\n",infoLog);
  delete infoLog;
 }

 // read and compile vertex shader from ascii-file
 sprintf(buf,"%s.vsd",filename);                                  // add extension to filename
 f=fopen(buf,"r");                                                // openfile
 fseek(f, 0, 2);                                                  // go to the end of file
 lEndPos = ftell(f);                                              // get position (size of file)
 source=new char[lEndPos+1];                                      // allocate memory for code
 fseek(f, 0, 0 );                                                 // go to begin of file
 back=fread(source,1,lEndPos,f);                                  // read file
 source[lEndPos]=(char) NULL;                                     // add NULL-termination for string
 fclose(f);                                                       // close file

 const GLcharARB *pgms2=source;
 VertexHandle=glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);      // create shader object
 glShaderSourceARB(VertexHandle, 1, &pgms2 ,NULL );               // set source for object
 glCompileShaderARB(VertexHandle);                                // compile shader
 delete[] source;                                                 // delete source

 // error handling
 glGetObjectParameterivARB(VertexHandle, GL_OBJECT_INFO_LOG_LENGTH_ARB, &infologLength);
 if (infologLength>0)
 {
  infoLog=new char[infologLength];
  glGetInfoLogARB(VertexHandle, infologLength, &charsWritten, infoLog);
  printf("%s",infoLog);
  delete infoLog;
 }

 // allocate programm object and link fragment and vertex shader
 Handle=glCreateProgramObjectARB();
 glAttachObjectARB(Handle,FragmentHandle);
 glAttachObjectARB(Handle,VertexHandle);
 glDeleteObjectARB(FragmentHandle);
 glDeleteObjectARB(VertexHandle);
 glLinkProgramARB(Handle);

 // error handling
 glGetObjectParameterivARB(Handle, GL_OBJECT_INFO_LOG_LENGTH_ARB, &infologLength);
 if (infologLength>0)
 {
  infoLog=new char[infologLength];
  glGetInfoLogARB(Handle, infologLength, &charsWritten, infoLog);
  printf("%s\n",infoLog);
  delete infoLog; 
 }
}

void shader::bind()
{
 // activate shader
 glEnable(GL_VERTEX_PROGRAM_ARB);
 glUseProgramObjectARB(Handle);
}

void shader::step()
{ 
 glViewport(0,0,800,600);
 glBegin(GL_QUADS);
  glVertex2d(-1.0,-1.0);
  glVertex2d(1.0,-1.0);
  glVertex2d(1.0,1.0);
  glVertex2d(-1.0,1.0);
 glEnd();   
}

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[])
{
openglcontext c(800,600); // open window
shader s(“firstshader”); // load shader

int quit=0;
SDL_Event event;
while(quit==0) // while quit==0 ;-)
{
s.bind(); // bind the shader
s.step(); // use the shader once
SDL_GL_SwapBuffers(); // swap the doublebuffers of the video context
while(SDL_PollEvent(&event)) // check for SDL events
{
switch(event.type)
{
case SDL_QUIT: // if Quit (clicked cross or something)
quit = 1; // quit=1 -> exit
break;
case SDL_KEYDOWN: // if some key pressed
quit = 1; // quit=1 -> exit
break;
default:
break;
}
}
}
}

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[])
{
openglcontext c(800,600);
shader s(“firstshader”);

int quit=0;
int frame=0;
SDL_Event event;
while(quit==0)
{
s.bind();
glUniform1iARB(glGetUniformLocationARB(s.Handle,”frame”),frame); // < -here we send an integer from the CPU to the GPU
s.step();
SDL_GL_SwapBuffers();
while(SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
quit = 1;
break;
case SDL_KEYDOWN: // if some key pressed
quit = 1; // quit=1 -> exit
break;
default:
break;
}
}
frame++;
}
}

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 ()
{
float time=float(frame)/60.;
float x=gl_TexCoord[0].x; //<- get the position of the current fragment
float y=gl_TexCoord[0].y;
gl_FragData[0] = vec4(sin(10.*x+10.*time),sin(10.*x-10.*time),sin(30.*y-20.*time),1.0);
}

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()
{
gl_Position = vec4(sin(time)*gl_Vertex.xy, 0, 1);
gl_TexCoord[0]= vec4(gl_Vertex.xy, 0, 1);
}

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
{
public:
GLuint id; // this will contain an integer handle of the texture
int sx,sy; // this will contain the size of the texture
float *data; // this will contain the floatin point data of the 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)
{
// set size to given parameters
sx=sxn; sy=syn;

// allocate buffer and init with some data
data = new float[sx*sy*4];
for(int x=0;x for(int y=0;y {
data[4*(x+y*sx)] =float(x)/800.;
data[4*(x+y*sx)+1]=float(y)/600.;
data[4*(x+y*sx)+2]=float(x*y)/800./600.;
data[4*(x+y*sx)+3]=1.0;
}

// build texture
glGenTextures(1,&id); // generate texture id
glBindTexture(GL_TEXTURE_2D,id); // bind texture with id to actual texture device (default:0)
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F_ARB,sx,sy,0,GL_RGBA,GL_FLOAT,data); // create texture on gpu + upload data

// no interpolation always return the nearest pixel for float coordinates
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
// the texture wraps over at the edges (repeat)
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_R,GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
}

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[])
{
openglcontext c(800,600); // open window
shader s(“firstshader”); // load shader
texture t(800,600); // create texture with 800×600 pixels

int quit=0;
int frame=0;
SDL_Event event;
while(quit==0) // while quit==0 ;-)
{
s.bind(); // bind the shader
glUniform1iARB(glGetUniformLocationARB(s.Handle,”t0″),0); // give texture device to shader
glUniform1iARB(glGetUniformLocationARB(s.Handle,”frame”),frame); // frame animation fun
s.step(); // use the shader once
SDL_GL_SwapBuffers(); // swap the doublebuffers of the video context
while(SDL_PollEvent(&event)) // check for SDL events
{
switch(event.type)
{
case SDL_QUIT: // if Quit (clicked cross or something)
quit = 1; // quit=1 -> exit
break;
case SDL_KEYDOWN: // if some key pressed
quit = 1; // quit=1 -> exit
break;
default:
break;
}
}
frame++;
}
}

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.