15 min read

(For more resources related to this topic, see here.)

Setting up the OpenGL v3.3 core profile on Visual Studio 2010 using the GLEW and freeglut libraries

We will start with a very basic example in which we will set up the modern OpenGL v3.3 core profile. This example will simply create a blank window and clear the window with red color.

OpenGL or any other graphics API for that matter requires a window to display graphics in. This is carried out through platform specific codes. Previously, the GLUT library was invented to provide windowing functionality in a platform independent manner. However, this library was not maintained with each new OpenGL release. Fortunately, another independent project, freeglut, followed in the GLUT footsteps by providing similar (and in some cases better) windowing support in a platform independent way. In addition, it also helps with the creation of the OpenGL core/compatibility profile contexts. The latest version of freeglut may be downloaded from http://freeglut.sourceforge.net. The version used in the source code accompanying this book is v2.8.0. After downloading the freeglut library, you will have to compile it to generate the libs/dlls.

The extension mechanism provided by OpenGL still exists. To aid with getting the appropriate function pointers, the GLEW library is used. The latest version can be downloaded from http://glew.sourceforge.net. The version of GLEW used in the source code accompanying this book is v1.9.0. If the source release is downloaded, you will have to build GLEW first to generate the libs and dlls on your platform. You may also download the pre-built binaries.

Prior to OpenGL v3.0, the OpenGL API provided support for matrices by providing specific matrix stacks such as the modelview, projection, and texture matrix stacks. In addition, transformation functions such as translate, rotate, and scale, as well as projection functions were also provided. Moreover, immediate mode rendering was supported, allowing application programmers to directly push the vertex information to the hardware.

In OpenGL v3.0 and above, all of these functionalities are removed from the core profile, whereas for backward compatibility they are retained in the compatibility profile. If we use the core profile (which is the recommended approach), it is our responsibility to implement all of these functionalities including all matrix handling and transformations. Fortunately, a library called glm exists that provides math related classes such as vectors and matrices. It also provides additional convenience functions and classes. For all of the demos in this book, we will use the glm library. Since this is a headers only library, there are no linker libraries for glm. The latest version of glm can be downloaded from http://glm.g-truc.net. The version used for the source code in this book is v0.9.4.0.

There are several image formats available. It is not a trivial task to write an image loader for such a large number of image formats. Fortunately, there are several image loading libraries that make image loading a trivial task. In addition, they provide support for both loading as well as saving of images into various formats. One such library is the SOIL image loading library. The latest version of SOIL can be downloaded from http://www.lonesock.net/soil.html.

Once we have downloaded the SOIL library, we extract the file to a location on the hard disk. Next, we set up the include and library paths in the Visual Studio environment. The include path on my development machine is D:LibrariessoilSimple OpenGL Image Librarysrc whereas, the library path is set to D:LibrariessoilSimple OpenGL Image LibrarylibVC10_Debug. Of course, the path for your system will be different than mine but these are the folders that the directories should point to.

These steps will help us to set up our development environment. For all of the recipes in this book, Visual Studio 2010 Professional version is used. Readers may also use the free express edition or any other version of Visual Studio (for example, Ultimate/Enterprise). Since there are a myriad of development environments, to make it easier for users on other platforms, we have provided premake script files as well.

The code for this recipe is in the Chapter1/GettingStarted directory.

How to do it…

Let us setup the development environment using the following steps:

  1. After downloading the required libraries, we set up the Visual Studio 2010 environment settings. 

  2. We first create a new Win32 Console Application project as shown in the preceding screenshot. We set up an empty Win32 project as shown in the following screenshot:

  3. Next, we set up the include and library paths for the project by going into the Project menu and selecting project Properties . This opens a new dialog box. In the left pane, click on the Configuration Properties option and then on VC++ Directories .
  4. In the right pane, in the Include Directories field, add the GLEW and freeglut subfolder paths.
  5. Similarly, in the Library Directories , add the path to the lib subfolder of GLEW and freeglut libraries as shown in the following screenshot:

  6. Next, we add a new .cpp file to the project and name it main.cpp. This is the main source file of our project. You may also browse through Chapter1/ GettingStarted/GettingStarted/main.cpp which does all this setup already.
  7. Let us skim through the Chapter1/ GettingStarted/GettingStarted/main.cpp file piece by piece.

    #include
    #include
    #include

    These lines are the include files that we will add to all of our projects. The first is the GLEW header, the second is the freeglut header, and the final include is the standard input/output header.

  8. In Visual Studio, we can add the required linker libraries in two ways. The first way is through the Visual Studio environment (by going to the Properties menu item in the Project menu). This opens the project’s property pages. In the configuration properties tree, we collapse the Linker subtree and click on the Input item. The first field in the right pane is Additional Dependencies. We can add the linker library in this field as shown in the following screenshot:

  9. The second way is to add the glew32.lib file to the linker settings programmatically. This can be achieved by adding the following pragma:

    #pragma comment(lib, "glew32.lib")

  10. The next line is the using directive to enable access to the functions in the std namespace. This is not mandatory but we include this here so that we do not have to prefix std:: to any standard library function from the iostream header file.

    using namespace std;

  11. The next lines define the width and height constants which will be the screen resolution for the window. After these declarations, there are five function definitions . The OnInit() function is used for initializing any OpenGL state or object, OnShutdown() is used to delete an OpenGL object, OnResize() is used to handle the resize event, OnRender() helps to handle the paint event, and main() is the entry point of the application. We start with the definition of the main() function.

    const int WIDTH = 1280;
    const int HEIGHT = 960;
    int main(int argc, char** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE |
    GLUT_RGBA);
    glutInitContextVersion (3, 3);
    glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG);
    glutInitContextProfile(GLUT_FORWARD_COMPATIBLE);
    glutInitWindowSize(WIDTH, HEIGHT);

  12. The first line glutInit initializes the GLUT environment. We pass the command line arguments to this function from our entry point. Next, we set up the display mode for our application. In this case, we request the GLUT framework to provide support for a depth buffer, double buffering (that is a front and a back buffer for smooth, flicker-free rendering), and the format of the frame buffer to be RGBA (that is with red, green, blue, and alpha channels). Next, we set the required OpenGL context version we desire by using the glutInitContextVersion. The first parameter is the major version of OpenGL and the second parameter is the minor version of OpenGL. For example, if we want to create an OpenGL v4.3 context, we will call glutInitContextVersion (4, 3). Next, the context flags are specified:

    glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG);
    glutInitContextProfile(GLUT_FORWARD_COMPATIBLE);

    In OpenGL v4.3, we can register a callback when any OpenGL related error occurs. Passing GLUT_DEBUG to the glutInitContextFlags functions creates the OpenGL context in debug mode which is needed for the debug message callback.

  13. For any version of OpenGL including OpenGL v3.3 and above, there are two profiles available: the core profile (which is a pure shader based profile without support for OpenGL fixed functionality) and the compatibility profile (which supports the OpenGL fixed functionality). All of the matrix stack functionality glMatrixMode(*), glTranslate*, glRotate*, glScale*, and so on, and immediate mode calls such as glVertex*, glTexCoord*, and glNormal* of legacy OpenGL, are retained in the compatibility profile. However, they are removed from the core profile. In our case, we will request a forward compatible core profile which means that we will not have any fixed function OpenGL functionality available.
  14. Next, we set the screen size and create the window:

    glutInitWindowSize(WIDTH, HEIGHT);
    glutCreateWindow("Getting started with OpenGL 3.3");

  15. Next, we initialize the GLEW library. It is important to initialize the GLEW library after the OpenGL context has been created. If the function returns GLEW_OK the function succeeds, otherwise the GLEW initialization fails.

    glewExperimental = GL_TRUE;
    GLenum err = glewInit();
    if (GLEW_OK != err){
    cerr} else {
    if (GLEW_VERSION_3_3)
    {
    cout}
    }
    coutcoutcoutcoutcout"

    The glewExperimental global switch allows the GLEW library to report an extension if it is supported by the hardware but is unsupported by the experimental or pre-release drivers. After the function is initialized, the GLEW diagnostic information such as the GLEW version, the graphics vendor, the OpenGL renderer, and the shader language version are printed to the standard output.

  16. Finally, we call our initialization function OnInit() and then attach our uninitialization function OnShutdown() as the> glutCloseFunc method—the close callback function which will be called when the window is about to close. Next, we attach our display and reshape function to their corresponding callbacks. The main function is terminated with a call to the glutMainLoop() function> which starts the application’s main loop.

    OnInit();
    glutCloseFunc(OnShutdown);
    glutDisplayFunc(OnRender);
    glutReshapeFunc(OnResize);
    glutMainLoop();
    return 0;
    }

There’s more…

The remaining functions are defined as follows:

void OnInit() {
glClearColor(1,0,0,0);
cout}
void OnShutdown() {
cout}
void OnResize(int nw, int nh) {
}
void OnRender() {
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glutSwapBuffers();
}

For this simple example, we set the clear color to red (R:1, G:0, B:0, A:0). The first three are the red, green, and blue channels and the last is the alpha channel which is used in alpha blending. The only other function defined in this simple example is the OnRender() function, which is our display callback function that is called on the paint event. This function first clears the color and depth buffers to the clear color and clear depth values respectively.

Similar to the color buffer, there is another buffer called the depth buffer. Its clear value can be set using the glClearDepth function. It is used for hardware based hidden surface removal. It simply stores the depth of the nearest fragment encountered so far. The incoming fragment’s depth value overwrites the depth buffer value based on the depth clear function specified for the depth test using the> glDepthFunc function. By default the depth value gets overwritten if the current fragment’s depth is lower than the existing depth in the depth buffer.

The glutSwapBuffers function is then called to set the current back buffer as the current front buffer that is shown on screen. This call is required in a double buffered OpenGL application. Running the code gives us the output shown in the following screenshot.

Designing a GLSL shader class

We will now have a look at how to set up shaders. Shaders are special programs that are run on the GPU. There are different shaders for controlling different stages of the programmable graphics pipeline. In the modern GPU, these include the vertex shader (which is responsible for calculating the clip-space position of a vertex), the tessellation control shader (which is responsible for determining the amount of tessellation of a given patch), the tessellation evaluation shader (which computes the interpolated positions and other attributes on the tessellation result), the geometry shader (which processes primitives and can add additional primitives and vertices if needed), and the fragment shader (which converts a rasterized fragment into a colored pixel and a depth). The modern GPU pipeline highlighting the different shader stages is shown in the following figure.

Note that the tessellation control/evaluation shaders are only available in the hardware supporting OpenGL v4.0 and above. Since the steps involved in shader handling as well as compiling and attaching shaders for use in OpenGL applications are similar, we wrap these steps in a simple class we call GLSLShader.

Getting ready

The GLSLShader class is defined in the GLSLShader.[h/cpp] files. We first declare the constructor and destructor which initialize the member variables. The next three functions, LoadFromString, LoadFromFile, and CreateAndLinkProgram handle the shader compilation, linking, and program creation. The next two functions, Use and UnUse functions bind and unbind the program. Two std::map datastructures are used. They store the attribute’s/uniform’s name as the key and its location as the value. This is done to remove the redundant call to get the attribute’s/uniform’s location each frame or when the location is required to access the attribute/uniform. The next two functions, AddAttribute and AddUniform add the locations of the attribute and uniforms into their respective std::map (_attributeList and _uniformLocationList).

class GLSLShader
{
public:
GLSLShader(void);
~GLSLShader(void);
void LoadFromString(GLenum whichShader, const string& source);
void LoadFromFile(GLenum whichShader, const string& filename);
void CreateAndLinkProgram();
void Use();
void UnUse();
void AddAttribute(const string& attribute);
void AddUniform(const string& uniform);
GLuint operator[](const string& attribute);
GLuint operator()(const string& uniform);
void DeleteShaderProgram();
private:
enum ShaderType{VERTEX_SHADER,FRAGMENT_SHADER,GEOMETRY_SHADER};
GLuint _program;
int _totalShaders;
GLuint _shaders[3];
map _attributeList;
map _uniformLocationList;
};

To make it convenient to access the attribute and uniform locations from their maps , we declare the two indexers. For attributes, we overload the square brackets ([]) whereas for uniforms, we overload the parenthesis operation (). Finally, we define a function DeleteShaderProgram for deletion of the shader program object. Following the function declarations are the member fields.

How to do it

In a typical shader application, the >usage of the GLSLShader object is as follows:

  1. Create the GLSLShader object either on stack (for example, GLSLShader shader;) or on the heap (for example, GLSLShader* shader=new GLSLShader();)
  2. Call LoadFromFile on the GLSLShader object reference
  3. Call CreateAndLinkProgram on the GLSLShader object reference
  4. Call Use on the GLSLShader object reference to bind the shader object
  5. Call AddAttribute/AddUniform to store locations of all of the shader’s attributes and uniforms respectively
  6. Call UnUse on the GLSLShader object reference to unbind the shader object

Note that the above steps are required at initialization only. We can set the values of the uniforms that remain constant throughout the execution of the application in the Use/UnUse block given above.

At the rendering step, we access uniform(s), if we have uniforms that change each frame (for example, the modelview matrices). We first bind the shader by calling the GLSLShader::Use function. We then set the uniform by calling the glUniform{*} function, invoke the rendering by calling the glDraw{*} function, and then unbind the shader (GLSLShader::UnUse). Note that the glDraw{*} call passes the attributes to the GPU.

How it works

In a typical OpenGL shader application, the shader specific functions and their sequence of execution are as follows:

glCreateShader
glShaderSource
glCompileShader
glGetShaderInfoLog

Execution of the above four functions creates a shader object. After the shader object is created, a shader program object is >created using the following set of functions in the following sequence:

glCreateProgram
glAttachShader
glLinkProgram
glGetProgramInfoLog

Note that after the shader program has been linked, we can safely delete the shader object.

There’s more

In the GLSLShader class, the first four steps are handled in the LoadFromString function and the later four steps are handled by the CreateAndLinkProgram member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding This is carried out by the glUseProgram function which is called through the Use/UnUse functions in the GLSLShader class.

To enable communication between the application and the shader, there are two different kinds of fields available in the shader. The first are the attributes which may change during shader execution across different shader stages. All per-vertex attributes fall in this category. The second are the uniforms which remain constant throughout the shader execution. Typical examples include the modelview matrix and the texture samplers.

In order to communicate with the shader program, the application must obtain the location of an attribute/uniform after the shader program is bound. The location identifies the attribute/uniform. In the GLSLShader class, for convenience, we store the locations of attributes and uniforms in two separate std::map objects.

For accessing any attribute/uniform location, we provide an indexer in the GLSLShader class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader object is called shader and our shader contains a uniform called MVP. We can first add it to the map of GLSLShader by calling shader.AddUniform(“MVP”). This function adds the uniform’s location to the map. Then when we want to access the uniform, we directly call shader(“MVP”) and it returns the location of our uniform.


Subscribe to the weekly Packt Hub newsletter. We'll send you this year's Skill Up Developer Skills Report.

* indicates required

LEAVE A REPLY

Please enter your comment!
Please enter your name here