You are on page 1of 8

OpenGL with GLM & SDL

Setting up GLEW & GLM


In the last lab (Getting Started with SDL in Visual Studio 2010) you will have downloaded and setup SDL, and created a simple test app. In this lab we will extend our simple SDL application and start rendering with OpenGL. As we will be focussing on the non-deprecated core features of OpenGL 3.x and later, we will need to use some additional libraries for support. GLEW Visual Studio does not ship with the headers or lib files to access features of OpenGL beyond version 1.1. Luckily the GLEW (GL Extension Wrangler) library is able to solve this problem for us. You can download the 32 bit and 64 bit binaries for Windows (which contains the dll, lib and header files) for GLEW from the project homepage: http://glew.sourceforge.net/ . Install these files on your PC in a similar fashion to how you installed SDL. Whichever version of Windows you are using, the 32 bit version of GLEW should work and may be easier to use with additional third part libraries. If you are working on Mac or Linux, then you should not need to use GLEW. However, to ensure that your program complies to the core profile you should not use the normal gl/gl.h header file in your programs you should use the gl3.h header from http://www.opengl.org/registry/api/gl3.h . On Windows, we will rely on GLEW to retrieve the correct function and type definitions and declarations for us. OpenGL Mathematics: GLM The deprecated/removed features of OpenGL include all the matrix management and manipulation functions that are relied upon by the vast majority of older OpenGL programs. It is possible to recreate your own matrix and math libraries from scratch and there are a range of books which provide good exemplars for this. However, in this class we will use a third part math library that has been developed for use with OpenGL GLM. One of the notable features of GLM is that the core functions and types have been based closely on GLSL so familiarity with GLM should help with GLSL and vice versa. You can download the GLM files from http://glm.g-truc.net/ . GLM is a header only library so you only need to add the GLM folder to your include path there are no lib files and there is no dll to worry about. In this lab Ive used GLM version 0.9.2.7. Finally, on Windows, you will probably need to tell Visual Studio to use the openGL32.lib library file add it the Additional Dependencies.

Setting up an OpenGL Render Context


Start with your program from the previous lab exercise. The following function can be used to replace the existing calls to SDL_Init and SDL_CreateWindow. This code will create a SDL window using an OpenGL 3.0 context. You should be able to modify the code to create an OpenGL 3.2 (or later) context. If you try to create an OpenGL context not supported by the current graphics card, the call will silently fail OpenGL will be unable to render, but there may be no error message.

// Set up rendering context, call by reference void setupRC(SDL_WindowID &window, SDL_GLContext &context) { if (SDL_Init(SDL_INIT_VIDEO) < 0) // Initialize video exitFatalError("Unable to initialize SDL"); // Request an OpenGL 3.0 context. Not able to use // should default to core profile with OpenGL 3.2 // If you request a context not supported by your // will be created. SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL to choose profile (yet), or later drivers, no OpenGL context 3); 0);

// double buffering on

// Turn on x4 multisampling anti-aliasing (MSAA) SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); // Create 800x600 window window = SDL_CreateWindow("SDL/GLM/OpenGL Demo", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN ); if (!window) // Check window was created OK exitFatalError("Unable to create window"); context = SDL_GL_CreateContext(window); // Create OpenGL context // Set swap buffers to sync with monitor's vertical refresh rate SDL_GL_SetSwapInterval(1); }

Additionally, edit the clean up calls at the end of main( ) to include a call to delete the OpenGL context:
SDL_GL_DeleteContext(glContext); SDL_DestroyWindow(hWindow); SDL_Quit();

Initialise GLEW (Windows Only)


Add the following code to main() after the call to the setupRC function to initialise GLEW:
GLenum err = glewInit(); // Required on Windows... init GLEW to access OpenGL beyond 1.1 // remove on other platforms if (GLEW_OK != err) { // glewInit failed, something is seriously wrong. cout << "glewInit failed, aborting." << endl; exit (1); }

Test OpenGL
At this point you should have just enough code to test OpenGL! Create a draw function to set a glClearColor, and call glClear(GL_COLOR_BUFFER_BIT) to clear the window. Try setting the clear color to red. End the draw function with a call to the SDL swap buffers function:
SDL_GL_SwapWindow(window); // swap buffers

Call the draw function from the event loop and test your program.

The complete draw function might simply consist of the following lines at this point:

void draw(const SDL_WindowID &window) { glClearColor(1.0, 1.0, 1.0, 1.0); // set background colour glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear window // this is where we will be drawing stuff! SDL_GL_SwapWindow(window); // swap buffers }

Rendering a Simple 3D Mesh


Before you can render vertices in OpenGL, you need to create vertex and fragment shaders. So as well as defining the vertices to be rendered, shaders need to be loaded and configured. As youll see there is a lot of work to be done to set up OpenGL before anything can be rendered but once these steps are done, rendering additional objects will be relatively straight-forward. Load and Initialise Shaders The following functions can load and initialise shaders for the demo program, and display error messages as appropriate. The first function is loadFile this simply loads a file and returns a char* to the file contents in memory. The second function is used to print out basic error messages should the shaders fail to compile correctly. Other than this, these functions provide only very basic error handling. The final function is a basic function to load and initialise the shader programs that will be used in this exercise.
// loadFile - loads text file into char* fname // allocates memory - so need to delete after use char* loadFile(char *fname) { int size; char * memblock; // file read based on example in cplusplus.com tutorial // ios::ate opens file at the end ifstream file (fname, ios::in|ios::binary|ios::ate); if (file.is_open()) { size = (int) file.tellg(); // get file size memblock = new char [size+1]; // create buffer w space for null char file.seekg (0, ios::beg); file.read (memblock, size); file.close(); memblock[size] = 0; cout << "file " << fname << " loaded" << endl;

} else { cout << "Unable to open file " << fname << endl; // should ideally set a flag or use exception handling // so that calling function can decide what to do now } return memblock; } // printShaderError // Display (hopefully) useful error messages if shader fails to compile or link void printShaderError(GLint shader) { int maxLength = 0; int logLength = 0; GLchar *logMessage; // Find out how long the error message is if (glIsShader(shader)) glGetProgramiv(shader, GL_INFO_LOG_LENGTH, &maxLength); else glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); if (maxLength > 0) // If message has length > 0 { logMessage = new GLchar[maxLength]; if (glIsShader(shader)) glGetProgramInfoLog(shader, maxLength, &logLength, logMessage); else glGetShaderInfoLog(shader,maxLength, &logLength, logMessage); cout << "Shader Info Log:" << endl << logMessage << endl; delete [] logMessage; } } GLuint initShaders(char *vertFile, char *fragFile) { GLuint p, f, v; // Handles for shader program & vertex and fragment shaders v = glCreateShader(GL_VERTEX_SHADER); // Create vertex shader handle f = glCreateShader(GL_FRAGMENT_SHADER); // " fragment shader handle const char *vertSource = loadFile(vertFile); // load vertex shader source const char *fragSource = loadFile(fragFile); // load frag shader source // Send the shader source to the GPU // Strings here are null terminated - a non-zero final parameter can be // used to indicate the length of the shader source instead glShaderSource(v, 1, &vertSource,0); glShaderSource(f, 1, &fragSource,0); GLint compiled, linked; // return values for checking for compile & link errors // compile the vertex shader and test for errors glCompileShader(v); glGetShaderiv(v, GL_COMPILE_STATUS, &compiled); if (!compiled) { cout << "Vertex shader not compiled." << endl;

printShaderError(v); } // compile the fragment shader and test for errors glCompileShader(f); glGetShaderiv(f, GL_COMPILE_STATUS, &compiled); if (!compiled) { cout << "Fragment shader not compiled." << endl; printShaderError(f); } p = glCreateProgram(); // create the handle for the shader program glAttachShader(p,v); // attach vertex shader to program glAttachShader(p,f); // attach fragment shader to program glBindAttribLocation(p,0,"in_Position"); // Bind position to attribute 0 glBindAttribLocation(p,1,"in_Color"); // Bind color to attribute 0 glLinkProgram(p); // link the shader program and test for errors glGetProgramiv(p, GL_LINK_STATUS, &linked); if(!linked) { cout << "Program not linked." << endl; printShaderError(p); } glUseProgram(p); // Make the shader program the current active program

delete [] vertSource; // Don't forget to free allocated memory delete [] fragSource; // We allocated this in the loadFile function... return p; // Return the shader program handle }

The Vertex and Fragment Shader Programs You also need to provide the shader programs themselves. First, a handle is required for the compiled shader program set this up as a global variable for now:
GLuint shaderprogram; // handle for shader program

The following code provides the listings for the vertex and fragment shaders:
// simple vertex shader - simple.vert #version 130 // OpenGL 3.2 replace above with #version 150 uniform mat4x4 MVP; in vec3 in_Position; in vec3 in_Color; out vec3 ex_Color; // simple shader program // multiply each vertex position by the MVP matrix void main(void) { gl_Position = MVP * vec4(in_Position, 1.0); ex_Color = in_Color; // pass colour unmodified }

// simple fragment shader simple.frag #version 130 // OpenGL 3.2 replace above with #version 150 // Some drivers require the following precision highp float; in // // // // vec3 ex_Color; GLSL versions after 1.3 remove the built in type gl_FragColor If using a shader lang version greater than #version 130 you *may* need to uncomment the following line: out vec4 gl_FragColor

void main(void) { // Pass through original colour gl_FragColor = vec4(ex_Color,1.0); }

Set up the Vertex Array and Buffer Objects You still need to specify the mesh data itself, and set up the Vertex Buffer Objects and Vertex Array Object that will hold the mesh data. You can do this in an init function. This should be called from main() before entering the event loop. The init function is also a good place to enable various OpenGL options - such as depth testing and back face culling. Variables are also required for the array and buffer objects set these up as globals for now.
GLuint vao, vbo[2]; // Handles for a VAO and two VBOs void init(void) { const GLfloat pyramid[5][3] = { // a simple pyramid { 0.0, 0.5, 0.0 }, // top { -1.0, -0.5, 1.0 }, // front bottom left { 1.0, -0.5, 1.0 }, // front bottom right { 1.0, -0.5, -1.0 }, // back bottom right { -1.0, -0.5, -1.0 } }; // back bottom left const GLfloat colours[5][3] = { { 0.0, 0.0, 0.0 }, // black { 1.0, 0.0, 0.0 }, // red { 0.0, 1.0, 0.0 }, // green { 0.0, 0.0, 1.0 }, // blue { 1.0, 1.0, 0.0 } }; // yellow // Create and start shader program shaderprogram = initShaders("simple.vert", "simple.frag"); glGenVertexArrays(1, &vao); // Allocate & assign Vertex Array Object (VAO) glBindVertexArray(vao); // Bind VAO as current object glGenBuffers(2, vbo); // Allocate two Vertex Buffer Objects (VBO) glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // Bind 1st VBO as active buffer object // Copy the vertex data from diamond to the VBO // 15 * sizeof(GLfloat) is the size of the pyramid array glBufferData(GL_ARRAY_BUFFER, 15 * sizeof(GLfloat), pyramid, GL_STATIC_DRAW); // Position data is going into attribute index 0 & has 3 floats per vertex glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); // Enable attribute index 0 (position)

glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); // bind 2nd VBO as active buf obj // Copy the colour data from colours to our buffer // 15 * sizeof(GLfloat) is the size of the colours array glBufferData(GL_ARRAY_BUFFER, 15 * sizeof(GLfloat), colours, GL_STATIC_DRAW); // Colour data is going into attribute index 1 & has 3 floats per vertex glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(1); // Enable attribute index 1 (color) // OpenGL is state based - we normally need to keep references to shader, VAO, // VBO, etc., to allow swapping between different ones. In this program there // is only one shader program and one VAO glEnable(GL_DEPTH_TEST); // enable depth testing }

Cleanup Function Having created a range of data objects, it is wise to clean these up too. Create a cleanup function, to be called from main() after leaving the event loop:
void cleanup(void) { glUseProgram(0); glDisableVertexAttribArray(0); glDisableVertexAttribArray(1); // could also detach shaders glDeleteProgram(shaderprogram); glDeleteBuffers(2, vbo); glDeleteVertexArrays(1, &vao); }

Actually Draw Something! After all that, we can finally get to the rendering code itself. To keep the current angle of rotation of the pyramid you can either create a global variable (easy but messy), or add a variable to main that gets passed into the draw function (not very difficult, a nicer approach). Taking the easy option for now, create a global variable, r, of type float. To use GLM, the core GLM library needs to be included and we will also be using two of the extension libraries. Add the following includes:
#include <glm\glm.hpp> #include <glm\gtc\matrix_transform.hpp> #include <glm\gtc\type_ptr.hpp> // some versions of glm require: #include <glm\gtc\matrix_projection.hpp> // and may require the type_ptr include to be changed to <glm\gtx\type_ptr.hpp>

Then add the following draw function (dont forget to uncomment the call to draw() in the event loop):
void draw(const SDL_WindowID &window) { glClearColor(1.0, 1.0, 1.0, 1.0); // set background colour glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear window

// Create perspective projection matrix glm::mat4 projection = glm::perspective(45.0f, 4.0f / 3.0f, 1.0f, 100.f); // Apply model view transformations glm::mat4 identity(1.0); glm::mat4 modelview = glm::translate( identity, glm::vec3(0.0f, 0.0f, -4.0f)); modelview = glm::rotate( modelview, r, glm::vec3(0.0f,1.0f,0.0f)); glm::mat4 MVP = projection * modelview; // Calculate final MVP matrix // pass MVP as uniform into shader int mvpIndex = glGetUniformLocation(shaderprogram, "MVP"); glUniformMatrix4fv(mvpIndex, 1, GL_FALSE, glm::value_ptr(MVP)); glDrawArrays(GL_TRIANGLE_FAN, 0, 5); // draw the pyramid r+=0.5; SDL_GL_SwapWindow(window); // swap buffers }

Further Exercises
1. Rotate the pyramid around one of its corners instead of the centre 2. Change the program to load mesh data from file instead of having it hardcoded 3. Create a modification to one or both of the simple shaders, and re-write your program to draw two rotating pyramids: one rendered with each shader (e.g. one colour pyramid, one monochrome pyramid) 4. Research SDL i/o and event handling modify the code to allow the user to move the pyramid around using the arrow keys and/or the WASD keys and/or the mouse. 5. Explore how the error handling in the program could be improved can you find any crash bugs in the code? How would you resolve these?

Troubleshooting
Perhaps the most confusing errors are run-time unhandled exception errors. From experience, the most common causes are either (a) using lib and dll files from different versions of the GLEW or SDL libraries (perhaps with some dll files left in the System folders from previous work) or (b) omitting the code to generate the VBOs. If you modify your program to use extra buffer objects, forgetting to change the code to create the extra buffer objects is an easy mistake.

License
This work by Daniel Livingstone, University of the West of Scotland, is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.

You might also like