Providence Salumu Tutorial 44 - GLFW

Background

In the first tutorial we learned that OpenGL doesn't deal directly with windowing and that this responsibility is left to other APIs (GLX, WGL, etc). To make life simpler for ourselves we used GLUT to handle the windowing API. This makes our tutorials portable between different OSs. We've been using GLUT exclusively, until today. We are now going to take a look at another popular library that handles the same stuff as GLUT. This library is called GLFW and is hosted at www.glfw.org. One of the main differences between the two libraries is that GLFW is more modern and is actively being developed while GLUT is, well, older and its development has mostly stopped. GLFW has many features and you can read all about them in its home page.

Since there is no mathematical background for this tutorial we can go right ahead and review the code. What I've done here is to abstract the contents of glut_backend.h and glut_backend.cpp behind a general "backend" API that wraps the details of setting up the window and handling the input from the mouse and keyboard. You can easily switch between a GLUT backend and a GLFW backend and this gives a very nice flexibility for future tutorials.

In order to install GLFW (run as root):

If you're using Windows simply use the GLFW headers and libraries that I provide as part of the source package. This tutorial should build out of the box (please let me know if it doesn't...).

In order to build stuff aginst the GLFW library you must tell the compiler where the headers and libraries are located. On Linux my recommendation is to use the pkg-config utility:

pkg-config --cflags --libs glfw3

The '--cflags' flag tells pkg-config to output the flags GCC needs to compile a file that uses GLFW. The '--libs' flags outputs the flags required for linking. I'm using these flags in the Netbeans project that I provide for Linux and you can use them in your own makefile. If you're using one of the build systems such as autotools, cmake or scons you will need to check that system documentation for details.

Source walkthru

(ogldev_glfw_backend.cpp:24)

#define GLFW_DLL
#include <GLFW/glfw3.h>

This is how you include GLFW in your application. The 'GLFW_DLL' macro is required on Windows for using GLFW as a DLL.

(ogldev_glfw_backend.cpp:168)

void GLFWBackendInit(int argc, char** argv, bool WithDepth, bool WithStencil)
{
    sWithDepth = WithDepth;
    sWithStencil = WithStencil;

    if (glfwInit() != 1) {
        OGLDEV_ERROR("Error initializing GLFW");
        exit(1);
    }

    int Major, Minor, Rev;

    glfwGetVersion(&Major, &Minor, &Rev);

    printf("GLFW %d.%d.%d initialized\n", Major, Minor, Rev);

    glfwSetErrorCallback(GLFWErrorCallback);
}

Initializing GLFW is very simple. Note that the argc/argv parameters are not used but to keep the interface identical with the one we used for FreeGLUT they are still passed to the function. In addition to GLFW initialization we also print the version of the library for informative purposes and set a general error callback. If anything goes wrong we will print the error and exit.

(ogldev_glfw_backend.cpp:195)

bool GLFWBackendCreateWindow(uint Width, uint Height, bool isFullScreen, const char* pTitle)
{
    GLFWmonitor* pMonitor = isFullScreen ? glfwGetPrimaryMonitor() : NULL;

    s_pWindow = glfwCreateWindow(Width, Height, pTitle, pMonitor, NULL);

    if (!s_pWindow) {
        OGLDEV_ERROR("error creating window");
        exit(1);
    }

    glfwMakeContextCurrent(s_pWindow);

    // Must be done after glfw is initialized!
    glewExperimental = GL_TRUE;
    GLenum res = glewInit();
    if (res != GLEW_OK) {
        OGLDEV_ERROR((const char*)glewGetErrorString(res));
        exit(1);
    }

    return (s_pWindow != NULL);
}

In the function above we create a window and perform other important initialization stuff. The first three parameters to glfwCreateWindow are obvious. The fourth parameter specifies the monitor to use. 'GLFWmonitor' is an opaque GLFW object that represents the physical monitor. GLFW support multi-monitor setups and for such cases the function glfwGetMonitors returns a list of all the available monitors. If we pass a NULL monitor pointer we will get a regular window; if we pass a pointer to an actual monitor (we get the default using glfwGetPrimaryMonitor) we get a full screen window. Very simple. The fifth and last parameter is used for context sharing which is out of scope for this tutorial.

Before we start dispatching GL commands we have to make the window current on the calling thread. We accomplish this using glfwMakeContextCurrent. Finally, we initialize GLEW.

(ogldev_glfw_backend.cpp:238)

while (!glfwWindowShouldClose(s_pWindow)) {
    // OpenGL API calls go here...
    glfwSwapBuffers(s_pWindow);
    glfwPollEvents();
}

Unlike GLUT, GLFW doesn't provide its own main loop function. Therefore, we construct it using the above code which is part of wrapper function called GLFWBackendRun(). s_pWindow is a pointer to a GLFW window previously created using glfwCreateWindow(). In order for the application to signal the end of this loop the function glfwSetWindowShouldClose is available to the application via the wrapper function GLFWBackendLeaveMainLoop().

(ogldev_glfw_backend.cpp:122)

static void KeyCallback(GLFWwindow* pWindow, int key, int scancode, int action, int mods)
{
}


static void CursorPosCallback(GLFWwindow* pWindow, double x, double y)
{
}


static void MouseCallback(GLFWwindow* pWindow, int Button, int Action, int Mode)
{
}

static void InitCallbacks()
{
    glfwSetKeyCallback(s_pWindow, KeyCallback);
    glfwSetCursorPosCallback(s_pWindow, CursorPosCallback);
    glfwSetMouseButtonCallback(s_pWindow, MouseCallback);
}

What we see above is the initialization of our keyboard and mouse callbacks. If you are interested in using GLFW exclusively in your application simply review the documentation here for information about the values of Button, Action, Mode, etc. For my tutorials I have created a set of enums to describe the various keyboard and mouse keys and translated GLFW to these enums. I have done the same for GLUT and this provides the commonality which lets the same application code quickly switch from one backend to the other (see the implementation of the above functions in the code for further details).

(ogldev_glfw_backend.cpp:)

void GLFWBackendTerminate()
{
    glfwDestroyWindow(s_pWindow);
    glfwTerminate();
}

This is how we shutdown the GLFW backend. First we destroy the window and after that we terminate the GLFW library and free all of its resources. No call to GLFW can be done after that which is why this has to be the last thing we do in the main function (graphics-wise).

(ogldev_backend.h)

enum OGLDEV_BACKEND_TYPE {
    OGLDEV_BACKEND_TYPE_GLUT,
    OGLDEV_BACKEND_TYPE_GLFW
};

void OgldevBackendInit(OGLDEV_BACKEND_TYPE BackendType, int argc, char** argv, bool WithDepth, bool WithStencil);

void OgldevBackendTerminate();

bool OgldevBackendCreateWindow(uint Width, uint Height, bool isFullScreen, const char* pTitle);

void OgldevBackendRun(ICallbacks* pCallbacks);

void OgldevBackendLeaveMainLoop();

void OgldevBackendSwapBuffers();

I have created a new backend interface which we see in the above header file. These functions replace the GLUT specific code which we have been using. They are implemented in ogldev_backend.cpp in the Common project and are essentially redirections into GLUT or GLFW. You select the backend using OgldevBackendInit() and after that everything is transparent.

Since there isn't nothing new to display in this tutorial I have used the Sponza model which is very common in the 3D community to test new global illumination algorithms.

Providence Salumu