Skip to content

Basic Window

Find a location of your choice somewhere on your file system.

Perhaps something like

(Linux) ~/Projects/OpenGLGettingStarted (Windows) C:\Projects\OpenGLGettingStarted

And we shall refer to it as the project directory.

Now try to reproduce the following structore of the project directory on your system

OpenGLGettingStarted
├── lib
│   └── CMakeLists.txt             # here we describe all third party dependencies
│                                  # like glfw, glad, spdlog, imgui, ...
├── src
│   ├── 01-01-BasicWindow
│   │   ├── Main.cpp               # guess what is in here
│   │   └── CMakeLists.txt         # or here
└── CMakeLists.txt                 # Thats our solution file

Next step is to fill the CMakeLists files, so that CMake knows what to do.

Lets start with lib/CMakeLists.txt

When we create our first window we have some dependencies, namely GLFW, GLAD and spdlog. CMake has a mechanism called FetchContent which we will be using to pull those dependencies.

lib/CMakeLists.txt
include(FetchContent)

find_package(OpenGL REQUIRED)

Those two line mean, make FetchContent available for us to use, and we would also like to be sure that our platform can handle OpenGL otherwise CMake would fail to prepare the project and let us know. But its unlikely that your system will not support OpenGL. We will also not be supporting anything but a somewhat modern Linux Distribution, like Arch or Fedora or the likes, or Windows. Operating systems from Apple will not be supported, as they lack support for the OpenGL version we are going to target.

Next few lines will be pulling the aforementioned dependencies

GLFW

We want the GLFW sources basically, but we dont need its tests, its example or docs built, we also dont want it to install stuff to somewhere, just build so that we can link it together with the rest of the application later.

lib/CMakeLists.txt
#- GLFW ---------------------------------------------------------------------

FetchContent_Declare(
    glfw
    GIT_REPOSITORY https://github.com/glfw/glfw
    GIT_TAG        3.3.8
    GIT_SHALLOW    TRUE
    GIT_PROGRESS   TRUE
)
message("Fetching glfw")
set(GLFW_BUILD_TESTS OFF CACHE BOOL "")
set(GLFW_BUILD_DOCS OFF CACHE BOOL "")
set(GLFW_INSTALL OFF CACHE BOOL "")
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "")
FetchContent_MakeAvailable(glfw)

GLAD

GLAD is a functions loader for OpenGL. It takes the OpenGL specification xml for the targetted version and generates function bindings for us, which we need to load when we have a render context available. What that is I will explain later.

lib/CMakeLists.txt
FetchContent_Declare(
    glad
    GIT_REPOSITORY https://github.com/Dav1dde/glad.git
    GIT_SHALLOW    TRUE
    GIT_PROGRESS   TRUE
)

FetchContent_GetProperties(glad)
if(NOT glad_POPULATED)
    message("Fetching glad")
    FetchContent_Populate(glad)
    set(GLAD_PROFILE "core" CACHE STRING "OpenGL profile")
    set(GLAD_API "gl=4.6" CACHE STRING "API type/version pairs, like \"gl=4.6\", no version means latest")
    set(GLAD_GENERATOR "c" CACHE STRING "Language to generate the binding for")
    set(GLAD_EXTENSIONS "GL_ARB_bindless_texture" CACHE STRING "Extensions to take into consideration when generating the bindings")
    add_subdirectory(${glad_SOURCE_DIR} ${glad_BINARY_DIR})
endif()

spdlog

a logging framework which provides structured logging facilities. No more weird printf or std::cout.

lib/CMakeLists.txt
FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.10.0
    GIT_SHALLOW    TRUE
    GIT_PROGRESS   TRUE
)

message("Fetching spdlog")
FetchContent_MakeAvailable(spdlog)

Next one is CMakeLists.txt in our project directory.

That is our main project file.

CMakeLists.txt
project(OpenGLGettingStarted)

set(CMAKE_CXX_STANDARD 23)

add_subdirectory(lib)

It declares the project name, which c++ standard we want to target and which targets we want to add. Those are a) our dependencies which are declared in lib/CMakeLists.txt hence the add_directory(lib) line as well as b) our actual projects we are going to write in this guide.

Let's put in our first project in too, right below add_subdirectory(lib)

add_subdirectory(src/00-BasicWindow)

And then we move on to BasicWindow's CMakeLists.txt and Main.cpp

src/01-01-BasicWindow/CMakeLists.txt
add_executable(01-01-BasicWindow
    Main.cpp
)

if (MSVC)
    target_compile_options(01-01-BasicWindow PRIVATE /W3 /WX)
else()
    target_compile_options(01-01-BasicWindow PRIVATE -Wall -Wextra -Werror)
endif()

target_include_directories(spdlog PUBLIC include)

target_link_libraries(01-01-BasicWindow PRIVATE glad glfw spdlog)

Again declare a target, in this case 01-01-BasicWindow (I try to keep the docs pages in sync with the target names to aid my brain while writing this). Specify which cpp file/unit to build.

Then there is a small block which detects whether you are using Microsoft's C++ Compiler or not, and configure the compiler to always treat warnings as errors.

Then we make spdlog's include directory available to the target and link the program with our dependencies.

Finally we can start working on the first c++ parts. We will keep everything in one file, to create a window and react to keyboard input ESC and F11.

Let's start.

Main.cpp

I like to use int32_t, uint16_t over types/aliases like int, or ushort. For that we include cstdint.

We also include glad and glfw header as well as spdlog's. When it comes to glad and glfw we need to be careful here to include glad before glfw. C++ is weird and sometimes the order of includes is important, because certain compiler definitions/switches are declared somewhere else which are picked up by something else.

That's one of the cases here.

Plus the usual entry point for your bog standard c/c++ program.

It all should look like this:

#include <cstdint>

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <spdlog/spdlog.h>

int main(int32_t argc, char* argv[])
{
    return 0;   
}

You might want to reconfigure the project here, to be sure all deps are loaded and CMake knows how to build all this stuff. in VSCode press Ctrl+Shift+p and look for CMake: Delete Cache and Reconfigure. It is probably called very similar in Clion or Visual Studio.

VSCode will probably say something like this afterwards:

[cmake] -- Build files have been written to: /home/deccer/Projects/OpenGL-Getting-Started/build

If not you most likely goofed something up, please check if you have setup the project structure as I described or if there is a typo somewhere.

If you try to build it as is. You should get an error saying something like that:

error: unused parameter ‘argc’ [-Werror=unused-parameter]

And another one for argv because we dont really use them anywhere. We still want to treat warnings as errors, but we cant really fix anything here, instead we tell the compiler that we are aware of it, and annotate the 2 parameters with an attribute called [[maybe_unused]].

The whole thing should look like

#include <cstdint>

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <spdlog/spdlog.h>

int main(
    [[maybe_unused]] int32_t argc,
    [[maybe_unused]] char* argv[])
{
    return 0;   
}

If you build now, it should succeed.

Next step, create a window. In order to create a window we have to do a few things first, because we want a window which provides us with a render context. To tell GLFW what parameters we want we have to give it a few hints, literally.

Well, first we initialize GLFW itself by calling glfwInit and check its result value. It could fail because for some reason we didnt link glfw with our project or so.

src/01-01-BasicWindow/Main.cpp
    if (glfwInit() == GLFW_FALSE)
    {
        spdlog::error("Glfw: Unable to initialize");
        return 1;
    }

This is also the first time we use spdlog, instead of printf or std::cout.

Now the hints. We want to use OpenGL, we would like to target OpenGL 4.6 and its core profile. On top of that, we want he window be resizable, and come with window decoration (border, top window bar and system menu/buttons)

src/01-01-BasicWindow/Main.cpp
    glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
    glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);
    glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);

When we create the window we can specify its width and height. Earlier I mentioned that I like my windows centered, and in the case of the OpenGLGettingStarted thing also be 80% of whatever resolution you use at the moment. Therefore we ask the system about the current resolution to derive the window dimensions from.

src/01-01-BasicWindow/Main.cpp
    auto primaryMonitor = glfwGetPrimaryMonitor();
    auto videoMode = glfwGetVideoMode(primaryMonitor);

    auto screenWidth = videoMode->width;
    auto screenHeight = videoMode->height;

    auto windowWidth = static_cast<int32_t>(static_cast<float>(screenWidth) * 0.8f);
    auto windowHeight = static_cast<int32_t>(static_cast<float>(screenHeight) * 0.8f);

Then we can actually create the window, by passing the window dimensions and a title. We don't want to use exclusive fullscreen.

src/01-01-BasicWindow/Main.cpp
    auto windowHandle = glfwCreateWindow(windowWidth, windowHeight, "OpenGL - Getting Started", nullptr, nullptr);
    if (windowHandle == nullptr)
    {
        const char* errorDescription = nullptr;
        auto errorCode = glfwGetError(&errorDescription);
        if (errorCode != GLFW_NO_ERROR)
        {
            spdlog::error("GLFW: Unable to create window Details_{}", errorDescription);
        }
        return 1;
    }

Window creation can fail for various reasons, that's why we are going to ask it for an actual error message, if its not able to create a window.

By the way you can always check GLFW's docs for more information.

Now we have a window, but I like it centered, as mentioned before, GLFW doesn't automatically center the window for us unlike SDL2 (another library providing windowing and input glue, amongst other things)

So that's what we will be doing here

src/01-01-BasicWindow/Main.cpp
    int32_t monitorLeft = 0;
    int32_t monitorTop = 0;
    glfwGetMonitorPos(primaryMonitor, &monitorLeft, &monitorTop);
    glfwSetWindowPos(windowHandle, screenWidth / 2 - windowWidth / 2 + monitorLeft, screenHeight / 2 - windowHeight / 2 + monitorTop);

We can also interact with the window now, that means send keyboard input, or mouse events, resize the window, minify or maxify it to mention a few. We will focus on 2 for now.

Resizing and keyboard input.

To be able to get a feedback when a window has been resized or key pressed/released we need to hook up 2 callbacks using the following calls.

Resize first. Windows consist of roughly 2 major things. A non client area and the client area. The former is everything like border which can be 1-n pixels thick, and the title bar which has a caption, the usual buttons to minimize, maximize and close a window and the system menu. The latter is usually the part wrapped by the non client area. The client area is the space we have available to draw our stuff to/on. Now when a window gets resized, the client area is resized as well, automatically.

Perhaps a ugly picture helps.

Since we only want to focus on that area in particular we will setup a callback which will listen to that change so that we dont have to worry about calculating weird offsets/margins when we have to take border sizes and window title bars into account when it comes to sizes.

GLFW also calls that non client area a framebuffer, and its resize callback FramebufferSizeCallback.

src/01-01-BasicWindow/Main.cpp
    glfwSetFramebufferSizeCallback(windowHandle, [](
        [[maybe_unused]] GLFWwindow* window,
        int32_t framebufferWidth,
        int32_t framebufferHeight)
    {
        glViewport(0, 0, framebufferWidth, framebufferHeight);
    });

Resizing the framebuffer will come in handy later, when we have to adjust your projection matrices and framebuffer attachments (we will talk about what it is later)

We also need some form of handling keyboard input. GLFW has a callback for that as well

src/01-01-BasicWindow/Main.cpp
    glfwSetKeyCallback(windowHandle, [](
        GLFWwindow* window,
        int32_t key,
        [[maybe_unused]] int32_t scancode,
        int32_t action,
        [[maybe_unused]] int32_t mode)
    {
        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        {
            glfwSetWindowShouldClose(window, GLFW_TRUE);
        }
    });

The window should close when we press ESC, thats what we do here, tell the window that next time its events are updated, it will see that we want it to close and it closes. We will see that exact evalution few lines below from here in a second.

The next two lines are important.

OpenGL is a state machine. An OpenGL context holds that state. The state contains information such as which textures are bound to which texture units, which attachments the current framebuffer object has and things like that.

When you set the current context, you are switching all the state from the old context to the new context.

We dont have an old context nor another one than just this one, but thats what you have to call at least once in your application.

src/01-01-BasicWindow/Main.cpp
    glfwMakeContextCurrent(windowHandle);
    gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

The call after glfwMakeContextCurrent is as important, as it loads all necessary OpenGL function pointers, so that we can actually use them.

Then we will be setting some of that mentioned OpenGL state explicitly to their default values.

src/01-01-BasicWindow/Main.cpp
    glEnable(GL_FRAMEBUFFER_SRGB);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glFrontFace(GL_CCW);

    glClearColor(0.35f, 0.76f, 0.16f, 1.0f);
    glClearDepthf(1.0f);

TODO

Explain the states, explain srgb here?

Main Loop

Next block, we are also almost finished, is the so called game loop, a simple loop which runs as long as the window is upen. It refreshes all state GLFW knows about like which keys have been pressed, where is the window being moved to or resized upto what dimensions or whether a joystick has been plugged in amongst other things.

GLFW created a double buffered window for us, and the glfwSwapBuffers swaps those buffers whenever they are ready to be swapped.

src/01-01-BasicWindow/Main.cpp
    while (!glfwWindowShouldClose(windowHandle))
    {
        glfwPollEvents();

        glfwSwapBuffers(windowHandle);
    }

That game loop is the place where most of the magic happens. You update your game objects, read key from the keyboard and mouse, queue sounds to be played, receive network packets perhaps, and most importantly render your virtual world.

TODO

Explain swapping buffers a bit more here

What happens after we exit that game loop?

Not much, as there usually nothing else after the program ends, we clean up. Destroy the main window and terminate GLFW so that it can clean up its internal state.

And then we return to the `OS``.

In the next chapter we will add all the necessary things to render a triangle.