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.
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 theOpenGL
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.txtFetchContent_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
orstd::cout
.lib/CMakeLists.txtFetchContent_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.
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
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.
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)
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.
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.
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
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.
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
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.
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.
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.
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.