OpenGL Window

Learn how get started with OpenGL and SFML

Code Repository

SFML and OpenGL

OpenGL stands for Open Graphics Library, it’s a collection of functions and algorithms that a computer uses to calculate and render 3D (or 2D) Images like a 3D Cube. Most commonly it is called a Graphics API. Other popular graphics API are Direct3D from Windows, Metal from MacOS, and Vulkan the successor or OpenGL.

This is not an OpenGL tutorial, you can find really awesome OpenGL tutorials here (Learn OpenGL) and here (OpenGL).

In this tutorial, I will show you how to get started your Journey with OpenGL  and SFML. In the previous tutorial Graphics Window, you’ve learned about the SFML Graphics module and the RenderWindow. What if you don’t want to use the Graphics module ? What if you want to use SFML to build a 3D Scene / 3D Game ? In those cases, you can use the class sf::Window to create a basic OpenGL Window.

The code below shows how to draw a triangle using SFML and OpenGL. The details on how the triangle is created and drawn using OpenGL will not be covered. Expand the tab (Utility Functions – OpenGL Code) below to see the implementation of the following functions :

  • struct TriangleState : a structure that holds the OpenGL state of our triangle (VBO, VAO, and EBO)
  • sf::Vector2f pixelToNDC(vertex, resolution): SFML and OpenGL use different coordinate system. This function converts the SFML pixels position into NDC (Normalized Device Coordinate)
  • int createShaderProgram() : this function creates and return an OpenGL shader program
  • void createTriangle(triangleState, resolution, vertex1, vertex2, vertex3) : this function creates a triangle from three points (vertex).
  • void drawTriangle() : this function draws our triangle
  • void destroyTriangle() : this function removes the triangle data from memory
//Convert SFML pixel coordinates into OpenGL Normalized Device coordinates (NDC)
sf::Vector2f pixelToNDC(const sf::Vector2f& vertex, sf::Vector2u resolution)
{
    return sf::Vector2f(-1.f + vertex.x/resolution.x * 2.f,
                         1.f - vertex.y/resolution.y * 2.f);
}
int createShaderProgram()
{
    const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
    const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";
    // load resources, initialize the OpenGL states, ...
     // build and compile our shader program
    // ------------------------------------
    // vertex shader
    int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // check for shader compile errors
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // fragment shader
    int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // check for shader compile errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // link shaders
    int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // check for linking errors
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    return shaderProgram;
}
void createTriangle(TriangleState& triangle, sf::Vector2u resolution, const sf::Vector2f& vertex1, const sf::Vector2f& vertex2, const sf::Vector2f& vertex3)
{
    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] =
    {
        pixelToNDC(vertex1, resolution).x, pixelToNDC(vertex1, resolution).y, 0.0f, // left
        pixelToNDC(vertex2, resolution).x, pixelToNDC(vertex2, resolution).y, 0.0f, // right
        pixelToNDC(vertex3, resolution).x, pixelToNDC(vertex3, resolution).y, 0.0f  // top
    };
    glGenVertexArrays(1, &triangle.VAO);
    glGenBuffers(1, &triangle.VBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(triangle.VAO);
    glBindBuffer(GL_ARRAY_BUFFER, triangle.VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0);
}
void drawTriangle(TriangleState& triangle, int shaderProgram)
{
    glUseProgram(shaderProgram);
    glBindVertexArray(triangle.VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}
void destroyTriangle(TriangleState& triangle)
{
    glDeleteVertexArrays(1, &triangle.VAO);
    glDeleteBuffers(1, &triangle.VBO);
    glDeleteBuffers(1, &triangle.EBO);
}
////////////////////////////////////////////////////////////
// Nero Game Engine - SFML Tutorials
////////////////////////////////////////////////////////////
//Glew
#include <GL/glew.h>
//SFML
#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>
//
#include <iostream>
// Forward Declaration : Utility functions
struct          TriangleState{unsigned int VBO, VAO, EBO;};
sf::Vector2f    pixelToNDC(const sf::Vector2f& vertex, sf::Vector2u resolution);
int             createShaderProgram();
void            createTriangle(TriangleState& triangle, sf::Vector2u resolution, const sf::Vector2f& vertex1, const sf::Vector2f& vertex2, const sf::Vector2f& vertex3);
void            drawTriangle(TriangleState& triangle, int shaderProgram);
void            destroyTriangle(TriangleState& triangle);
int main()
{
    //Create OpenGL Window
        //window parameters (OpenGL 3.3)
    sf::Vector2u    window_resolution(1080, 720);
    std::string     window_title    = "OpenGL Window";
    sf::ContextSettings contextSettings;
        contextSettings.depthBits           = 24;
        contextSettings.stencilBits         = 8;
        contextSettings.antialiasingLevel   = 4;
        contextSettings.majorVersion        = 3;
        contextSettings.minorVersion        = 3;
        //create the window
    sf::Window openGLWindow(sf::VideoMode(window_resolution.x, window_resolution.y), window_title, sf::Style::Default, contextSettings);
        //setup the window
    openGLWindow.setVerticalSyncEnabled(true);
    openGLWindow.setActive(true);
    //Initialize GLEW (OpenGL Extension Wrangler Library) and check if OpenGL is available
    bool openGlAvailable = true;
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK)
    {
        openGlAvailable = false;
    }
    //If OpenGL is not available let's stop everything and exit the program.
    if(!openGlAvailable) return 0;

    //Initialize your game here, let's create a simple triangle
        //create shader program
    int shaderProgram = createShaderProgram();
        //build a triangle
    TriangleState triangle; //a structure to hold the OpenGL state of the triangle
    createTriangle(triangle, window_resolution, sf::Vector2f(540.f, 100.f), sf::Vector2f(340.f, 360.f), sf::Vector2f(740.f, 360.f));

    //Game loop
    bool gameRunning = true;
    while(gameRunning)
    {
        // handle events
        sf::Event event;
        while(openGLWindow.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
            {
                // end the program
                gameRunning = false;
            }
            else if (event.type == sf::Event::Resized)
            {
                // adjust the viewport when the window is resized
                glViewport(0, 0, event.size.width, event.size.height);
            }
        }

        //update your game here
            //we have noting to update, so it's empty

        // clear the buffers
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            drawTriangle(triangle, shaderProgram);
        // end the current frame (internally swaps the front and back buffers)
        openGLWindow.display();
    }
    //Destroy triangle state
    destroyTriangle(triangle);
    //Destroy shader Program
    glDeleteProgram(shaderProgram);
    return 0;
}

Step 1 : Includes and Main

Using the Engine SDK, create an empty project or a console project, then replace the code inside main.cpp with the code below. In order to use OpenGL with SFML you will have to include the following headers

  • GL/glew.h : GLEW stands for OpenGL Extension Wrangler Library, it is a library that loads OpenGL Extensions for you. The OpenGL API is made of a Core API plus many Extensions that are OS or GPU specific. Those Extensions are located somewhere inside your computer. GLEW can find them and make them usable in your C++ code
  • SFML/Window.hpp : This header contains the class sf::Window which is the most basic window that SFML offers. In the tutorial Graphics window, you’ve learned about the class sf::RenderWindow. sf::RenderWindow is an extension of sf::Window created to take advantage of the Graphics module. Since we don’t want to use the Graphics module, we’ll use sf::Window.
  • SFML/OpenGL.hpp : This header includes the OpenGL Core API (functions).
//Glew
#include <GL/glew.h>
//SFML
#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>
//CPP
#include <iostream>
//Create an empty main function
int main()
{
    //Our code will be here
    
    return 0;
}

Step 2 : Create a Window

The code below shows how to create a window configured to use OpenGL 3.3. In OpenGL terms, that’s equivalent to creating an OpenGL Context. The OpenGL version is specified through the attributes majorVersion and minorVersion of the parameter  contextSettings. If you would for example use OpenGL 4.5, the majorVersion will be 4 and the minorVersion will be 5. The window is created using 4 parameters

    • The window width
    • The window height
    • The window title
    • The Context Settings that define some OpenGL data
The code below combines the window width and height into a single parameter called window resolution.
 
Finally, we activate the window Vertical Synchronization with setVerticalSyncEnabled(true) which is optional and we make sure the OpenGL context is active with setActive(true), which is also optional.
 
If you place this code inside the main function and compile it, you will see the window appear and then disappear immediately. This is because our program reaches the last line of code “return 0” and exit. To maintain the window visible, we need a way to say “stay visible until I close you myself“. We’ll learn how to do that in the next sections
//Create OpenGL Window
    //window parameters (OpenGL 3.3)
sf::Vector2u    window_resolution(1080, 720);
std::string     window_title    = "OpenGL Window";
sf::ContextSettings contextSettings;
    contextSettings.depthBits           = 24;
    contextSettings.stencilBits         = 8;
    contextSettings.antialiasingLevel   = 4;
    contextSettings.majorVersion        = 3; //OpenGL major version
    contextSettings.minorVersion        = 3; //OpenGL minor version
    //create the window
sf::Window openGLWindow(sf::VideoMode(window_resolution.x, window_resolution.y), window_title, sf::Style::Default, contextSettings);
    //setup the window
openGLWindow.setVerticalSyncEnabled(true);
openGLWindow.setActive(true);

Step 3 : Initialize GLEW and Check OpenGL Availability

Now that we have our OpenGL window, let’s make sure OpenGL is working properly before we continue. The code below shows how to load the OpenGL extensions with GLEW. When loading the extensions fails, we consider that OpenGL is not available and we exit the program. Not all computers come with OpenGL or the latest version of OpenGL, you can use OpenGL Extensions Viewer on Windows to check the OpenGL installed on your computer.

//Initialize GLEW (OpenGL Extension Wrangler Library) and check if OpenGL is available
bool openGLAvailable = true;
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK)
{
    openGLAvailable = false;
}
//If OpenGL is not available let's stop everything and exit the program.
if(!openGLAvailable) return 0;

Step 4 : Maintain the Window visible

The way we maintain the window visible is by creating an Infinite Loop using a simple Boolean and a While Loop. Pretty simple isn’t it ? Before you try this code, read the next step to learn how to close the window.

bool gameRunning = true;
while(gameRunning)
{
    //Game Loop
}

Step 5 : Close Event and Resized Event

  • Close Event : To get the window closed when you click on the upper right button, we need to detect your click by catching the right Event and then end the Infinite loop by setting the boolean gameRunning to False. Events have already been briefly explained in the previous tutorial Graphics Window – Step 4.
  • Resize Event : When you resize the window by dragging its borders with the mouse, you need to inform OpenGL of that change. Our window has been created with a resolution of 1080 by 720, these are the values that OpenGL knows, when you resize the window, OpenGL has no way to know that the resolution has changed. You need to inform it manually by calling the function glViewport(origin x, origin y, new width, new height). Like clicking on the close button, changing the window size with the mouse generates an Event called Resized Event. The code below shows how to catch that event and retrieve the new size of the window.
Bonus Lesson : All the names of functions in OpenGL begin with gl like for glViewport(). Anytime you see a function beginning by gl or a variable(attribute) beginning with GL_, remember that’s part of the OpenGL Library. You can have more details about those functions here (Docs GL).
bool gameRunning = true;
while(gameRunning)
{
    // handle events
    sf::Event event;
    while(openGLWindow.pollEvent(event))
    {
        if(event.type == sf::Event::Closed)
        {
            // end the program, close the window
            gameRunning = false;
        }
        else if (event.type == sf::Event::Resized)
        {
            // adjust the OpenGL viewport when the window is resized, the values are in pixel
            glViewport(0, 0, event.size.width, event.size.height);
        }
    }
}

Step 6 : Create a Shader Program and a Triangle

The code below is used just before the Game Loop to create a simple Shader Program and a Triangle. The triangle is created using three coordinates (540, 100), (340, 360), and (740, 360).

//Initialize your game here, let's create a simple triangle
    //create shader program
int shaderProgram = createShaderProgram();
    //build a triangle
TriangleState triangle; //a structure to hold the OpenGL state of the triangle
createTriangle(triangle, window_resolution, sf::Vector2f(540.f, 100.f), sf::Vector2f(340.f, 360.f), sf::Vector2f(740.f, 360.f));

Step 7 : Draw our Triangle

Like in the tutorial Graphics Window – Step 6, the drawing happens in three steps

  • Clear the screen
  • Draw what we want
  • Display the result to the viewer
In this case, we use the OpenGL functions glClear to clear the screen, then we use our  function drawTriangle to draw the triangle we’ve created previously, finally, we call the sf::Window function display to make the drawing visible on the window.
// clear the buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    drawTriangle(triangle, shaderProgram);
// end the current frame (internally swaps the front and back buffers)
openGLWindow.display();

Step 8 : Destroy our Triangle

When you close the window and stop the Game Loop, just before exiting the program, there is one last thing to do : Clear the GPU of all data we have created. We use our function destroyTriangle to clear the triangle data and the OpenGL function glDeleteProgram to clear the shaderProgram’s data.

//Destroy triangle state
destroyTriangle(triangle);
//Destroy shader Program
glDeleteProgram(shaderProgram);