Home Business & Other C++ Game Animation Programming - Second Edition

C++ Game Animation Programming - Second Edition

By Michael Dunsky , Gabor Szauer
ai-assist-svg-icon Book + AI Assistant
eBook + AI Assistant $35.99 $24.99
Print $44.99
Subscription $15.99 $10 p/m for three months
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime! ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Along with your eBook purchase, enjoy AI Assistant (beta) access in our online reader for a personalized, interactive reading experience.
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription. BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime! ai-assist-svg-icon NEW: AI Assistant (beta) Available with eBook, Print, and Subscription.
eBook + AI Assistant $35.99 $24.99
Print $44.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
Gain access to our AI Assistant (beta) for an exclusive selection of 500 books, available during your subscription period. Enjoy a personalized, interactive, and narrative experience to engage with the book content on a deeper level.
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Along with your eBook purchase, enjoy AI Assistant (beta) access in our online reader for a personalized, interactive reading experience.
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
  1. Free Chapter
    Chapter 1: Creating the Game Window
About this book
If you‘re fascinated by the complexities of animating video game characters and are curious about the transformation of model files into 3D avatars and NPCs that can explore virtual worlds, then this book is for you. In this new edition, you’ll learn everything you need to know about game animation, from a simple graphical window to a large crowd of smoothly animated characters. First, you’ll learn how to use modern high-performance graphics, dig into the details of how virtual characters are stored, and load the models and animations into a minimalistic game-like application. Then, you’ll get an overview of the components of an animation system, how to play the animations and combine them, and how to blend from one animation into another. You’ll also get an introduction to topics that will make your programming life easier, such as debugging your code or stripping down the graphical output. By the end of this book, you’ll have gained deep insights into all the parts of game animation programming and how they work together, revealing the magic that brings life to the virtual worlds on your screen.
Publication date:
December 2023
Publisher
Packt
Pages
480
ISBN
9781803246529

 

1

Creating the Game Window

This is the start of your journey into the world of game character animation programming. In this book, you will open a window into a virtual world, enabling the user to take control and move around in it. The window will utilize hardware-accelerated graphics rendering to show detailed characters that have been loaded from a simple file on your system. You will be introduced to character animation, starting with basic steps such as how to show a single, static pose, and you will move on to more advanced topics such as Inverse Kinematics. By the end, the application will have a large crowd of animated people, who are the inhabitants of your virtual world. In addition, the window will have fancy UI elements that you can use to control the animations of the characters, and you will learn how to debug the application if you encounter any trouble, both on the CPU and the GPU. I hope you enjoy the ride – it will take you to various wonderful locations, steep hills, long roads, and nice cities. Buckle up!

To begin, welcome to Chapter 1! The first step might be the most important as it sets the foundation for all the other chapters in this book. Without a window to your virtual world, you won’t be able to see your creations. But it’s not as hard as you might expect, and the right tools can solve this quickly and easily.

As we are using open source software and platform-independent libraries in this book, you should be able to compile and run the code “out of the box” on Windows and Linux. You will find a detailed list of the required software and libraries in the Technical requirements section.

To that end, in this chapter, we will cover the following topics:

  • Creating your first window
  • Adding support for OpenGL or Vulkan to the window
  • Event handling in GLFW
  • The mouse and keyboard input for the game window
 

Technical requirements

For this chapter, you will need the following:

  • A PC with Windows or Linux and the tools listed later in this section
  • A text editor (such as Notepad++ or Kate) or a full IDE (such as Visual Studio or Eclipse)

Now, let’s get the source code for this book and start unpacking the code.

Getting the source code and the basic tools

The code for this book is hosted on GitHub, which you can find here:

https://github.com/PacktPublishing/Cpp-Game-Animation-Programming-Second-Edition

To unpack the code, you can use any of the following methods.

Getting the code as a ZIP file

If you download the code as a ZIP file, you will need to unpack it onto your system. My suggested way is to create a subfolder inside the home directory of the local user account on your computer as the destination, that is, inside the Documents folder, and unpack it there. But any other place is also fine; it depends on your personal preference.

Please make sure the path contains no spaces or special characters such as umlauts, as this might confuse some compilers and development environments.

Getting the code using Git

To get the code of the book, you can also use Git. Using Git offers you additional features, such as reverting changes if you have broken the code during the exploration of the source, or while working on the practical sessions at the end of each chapter. For Linux systems, use your package manager. For Ubuntu,the following line installs git:

sudo apt install git

On Windows, you can download it here: https://git-scm.com/downloads

You can get a local checkout of the code in a specific location on your system either through the git GUI, or by executing the following command in CMD:

git clone (GitHub-Link)

Also, please make sure that you use a path without spaces or special characters.

Downloading and installing GLFW

If you use Windows, you can download the binary distribution here: https://www.glfw.org/download

Unpack it and copy the contents of the include folder here, as CMake will only search within this location:

C:\Program Files (x86)\glfw\include

Then, copy the libraries from the lib-vc2022 subfolder into this lib folder:

C:\Program Files (x86)\glfw\lib

As a Linux user, you can install the development package of glfw3 using the package manager of your distribution. For Ubuntu, this line installs GLFW:

sudo apt install libglfw3-dev

Downloading and installing CMake

To build the code, we will use CMake. CMake is a collection of tools used to create native Makefiles for your compiler and operating system (OS).CMake also searches for the libraries, the headers to include, and more. It refers to all that “dirty” stuff you don’t want to lay your hands on during compilation time.

Important note

You only need CMake if you are using Eclipse or the command-line-based approach to compile the source code. Visual Studio installs its own version of CMake.

Windows users can download it here: https://cmake.org/download/.

Linux users can use the package manager of their distribution to install Cmake. If you use Ubuntu, the following line will install CMake on your system:

sudp apt install cmake

Using the example code with Visual Studio 2022 on Windows

If you want to use Visual Studio for the example files and don’t have it installed yet, download the Community Edition of Visual Studio at https://visualstudio.microsoft.com/de/downloads/.

Then, follow these steps:

  1. Choose the Desktop development with C++ option so that the C++ compiler and the other required tools are installed on your machine:
Figure 1.1: Installing the C++ Desktop development in VS 2022

Figure 1.1: Installing the C++ Desktop development in VS 2022

  1. Then, under Individual components, also check the C++ CMake tools for Windows option:
Figure 1.2: Installing the CMake tools in VS 2022

Figure 1.2: Installing the CMake tools in VS 2022

  1. Finish the installation of Visual Studio, start it, and skip the initial project selection screen.

Compiling and starting the example code can be done using the following steps:

  1. To open an example project, use the CMake... option, which appears after installing the CMake tools:
Figure 1.3: Open a CMake project in VS 2022

Figure 1.3: Open a CMake project in VS 2022

  1. Navigate to the folder with the example file and select the CMakeLists.txt file. This is the main configuration file for CMake:
Figure 1.4: Selecting the CMakeLists.txt file in the project

Figure 1.4: Selecting the CMakeLists.txt file in the project

Visual Studio will automatically configure CMake for you. The last line of the output window should be as follows:

1> CMake generation finished.

This confirms the successful run of the CMake file generation.

  1. Now, set the startup item by right-clicking on the CMakeLists.txt file – this step is required to build and run the project:
Figure 1.5: Configuring the startup item in VS 2022

Figure 1.5: Configuring the startup item in VS 2022

  1. After setting the startup item, we can build the current project. Right-click on the CMakeLists.txt file and choose Build:
Figure 1.6: Building the VS 2022 CMake project

Figure 1.6: Building the VS 2022 CMake project

If the compilation succeeds, start the program using the green arrow:

Figure 1.7: The program starting without debugging in VS 2022

Figure 1.7: The program starting without debugging in VS 2022

Installing a C++ compiler on your Windows PC

If you don’t use Visual Studio, you will need a C++ compiler first. You can use the MSYS2 tools and libs here: https://www.msys2.org.

Download the installation package, install MSYS2 in the default location but do not start MSYS2 at the end of the installation. Instead, start the MSYS2 MINWG64 environment from the start menu and update the MSYS2 system:

pacman -Syu

The MSYS2 system will request to close the current console after the update. This is the normal behaviour.

Open the MINGW64 environment again and install the gcc compiler suite, the glwf3 library, and the basic development tools in the MSYS2 console:

pacman –S mingw-x64-x86_64-gcc mingw-w64-x86_64-glfw base-devel

The preceding command installs the compilation tools you need for the book. We use the glfw3 library included in MSYS2 because it is compiled with the same compiler and version we will use in Eclipse.

You also need to include CMake and the installed compiler within the Windows PATH environment variable:

Figure 1.8: The Windows PATH settings when using MSYS2 on Windows

Figure 1.8: The Windows PATH settings when using MSYS2 on Windows

Eclipse for Windows uses Ninja to build CMake packages, so you need to install Ninja too. The easiest way to do this is by using the Windows package manager named Scoop, which you can access at https://scoop.sh.

Install Scoop in PowerShell Window:

> Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
> irm get.scoop.sh | iex

The preceding code will download and install Scoop on your computer. Now use it to install Ninja:

scoop install ninja

Installing a C++ compiler in Linux

Linux users can install g++ or clang with the package manager. For Ubuntu-based distributions, enter the following command in a Terminal window to install the compiler and the required libraries and tools for the book:

sudo apt install gcc build-essential ninja-build glslang-tools libglm-dev

Using the example code with Eclipse on Windows or Linux

If you prefer Eclipse instead of Visual Studio, follow these steps:

  1. Download and install Eclipse IDE for C/C++ Developers from https://www.eclipse.org/downloads/packages/.
  2. After installing Eclipse, head to the marketplace under Help:
Figure 1.9: Accessing the Eclipse marketplace

Figure 1.9: Accessing the Eclipse marketplace

  1. Install the cmake4eclipse and CMake Editor packages. The first one enables CMake support in Eclipse, with all the features we need, and the second one adds syntax coloring to the CMake files. This makes it more convenient to edit the files:
Figure 1.10: Installing the Eclipse CMake solutions

Figure 1.10: Installing the Eclipse CMake solutions

Compiling and starting the example code can be done in the following steps:

  1. First, open a project from the filesystem:
Figure 1.11: Opening a project in Eclipse

Figure 1.11: Opening a project in Eclipse

  1. Choose Directory... and navigate to the folder with the source code:
Figure 1.12: Navigating to the folder with the Eclipse project

Figure 1.12: Navigating to the folder with the Eclipse project

  1. Click on Finish to open the project. Next, choose Build Project from the context menu. You can do this by clicking on the right mouse button while hovering over the project folder:
Figure 1.13: Building the project in Eclipse

Figure 1.13: Building the project in Eclipse

  1. Sometimes, Eclipse does not automatically refresh the content of the project. You must force this via the context menu. Select Refresh or press F5:
Figure 1.14: Refreshing the Eclipse project

Figure 1.14: Refreshing the Eclipse project

  1. Now the executable is visible and can be run. Choose Run As, and select the second option, Local C/C++ Application:
Figure 1.15: Starting the executable generated by Eclipse

Figure 1.15: Starting the executable generated by Eclipse

  1. In the following dialog, choose the Main.exe (Windows) or Main (Linux) binary file from the list:
Figure 1.16: Selecting the generated executable in Eclipse

Figure 1.16: Selecting the generated executable in Eclipse

The Vulkan SDK

For Vulkan support, you also need to have the Vulkan SDK installed. Get it here: https://vulkan.lunarg.com/sdk/home. Then, do a default installation, and make sure to add GLM and Vulkan Memory Allocator, as we will need both of them later in the book:

Figure 1.17: Adding GLM and VMA during the Vulkan SDK installation

Figure 1.17: Adding GLM and VMA during the Vulkan SDK installation

Code organization in this book

The code for every chapter is stored in the GitHub repository, in a separate folder with the relevant chapter number. The number uses two digits to get the ordering right. Inside each folder, one or more subfolders can be found. These subfolders contain the code of the chapter, depending on the progress of that specific chapter:

Figure 1.18: Folder organization with the chapters in the example code

Figure 1.18: Folder organization with the chapters in the example code

For all chapters, we put the Main.cpp class and the CMake configuration file, CMakeLists.txt, into the project root folder. Inside the cmake folder, helper files for CMake are stored. These files are required to find additional header and library files. All C++ classes are located inside folders, collecting the classes of the objects we create. The Window class will be stored in the window subfolder to hold all files related to the class itself, and the same applies to the logger:

Figure 1.19: Folders and files in one example code project

Figure 1.19: Folders and files in one example code project

In the other chapters, more folders will be created.

The basic code for our application

Our future character rendering application needs some additional code to work.

A program can’t be started without an initial function called by the operating system. On Windows and Linux, this initial function in the code must be named main(). Inside this function, the application window will be created, and the control is moved over to the window.

As long as a graphical output is unavailable, we must have the capability to print text within the application to update the user on its status. Instead of the std::cout call, we will use a simple logging function in a separate class. This extra output will be kept for debugging purposes even after we have completed the rendering, as this makes a programmer’s life much easier.

The main entry point

The main() function is embedded in a C++ class file, but as it has no class definition, it just contains the code to open and close the application window and call the main loop of our Window class.

This is the content of the Main.cpp file, located in the project root:

#include <memory>
#include "Window.h"
#include "Logger.h"
int main(int argc, char *argv[]) {
  std::unique_ptr<Window> w = std::make_unique<Window>();
  if (!w->init(640, 480, "Test Window")) {
    Logger::log(1, "%s error: Window init error\n",
       __FUNCTION__);
    return -1;
  }
  w->mainLoop();
  w->cleanup();
  return 0;
}

The preceding class includes the memory header, as we will use a unique smart pointer here. Additionally, it includes the headers for the Window and Logger classes. Inside the main() function, we create the smart pointer with the w object of the Window class. Next, we try to initialize the window using the width, height, and title text. If this initialization fails, we print out a log message and exit the program with a value of -1 to tell the OS we ran into an error. The log() call has the same verbosity level as the first parameter, followed by a C-style printf string. The __FUNCTION__ macro is recommended to print out the function where the logging call was issued.

If the init() call was successful, we enter the mainLoop() function of the Windows class. This handles all the window events, drawings, and more. Closing the window ends the main loop. After this, we clean up the window and return the value of 0 to signal a successful termination.

The Logger class

Additionally, I added a small and simple Logger class to simplify the debugging process. This allows you to add logging messages with different logging levels, enabling you to control the number of logs being shown. If you encounter problems with the code, you can use the Logger class to print out the content of the variables and success/error messages. In the case of a crash, you will see which part of the code has been reached before the termination of the program.

The following is the content of the Logger.h file:

#pragma once
#include <cstdio>
class Logger {
  public:
    /* log if input log level is equal or smaller to log level set */
    template <typename... Args>
    static void log(unsigned int logLevel, Args ... args) {
      if (logLevel <= mLogLevel) {
        std::printf(args ...);
        /* force output, i.e. for Eclipse */
        std::fflush(stdout);
      }
    }
    static void setLogLevel(unsigned int inLogLevel) {
      inLogLevel <= 9 ? mLogLevel = inLogLevel :
          mLogLevel = 9;
    }
  private:
    static unsigned int mLogLevel;
};

The preceding file starts with the #pragma once directive, which is called a header guard. The header guard line is used to prevent multiple inclusions of the same header file during the compilation. Then, we include the cstdio C++ headers so that the std::printf() and std::fflush() functions are available. Here, I use the old C-style of printing as it is both easy to implement and use. The log() function is implemented as a C++ template to enable us to use a varying number of arguments to print to the screen. Inside the function, the current log level of the call is compared with the stored log level, suppressing all messages with higher log levels. If the log level fits, we use printf to output the arguments to the terminal. Forced flushing with std::fflush() is required for Eclipse; without the line, the output will be displayed after the termination of the program. The setLogLevel() function enables you to change the desired verbosity at runtime. That means you could also add UI elements to set the logging level using mGui controls, which are explained in Chapter 5. The only data member is the global log level.

The Logger.cpp file is only two lines long:

#include "Logger.h"
unsigned int Logger::mLogLevel = 1;

The first line includes the class header, while the second line is responsible for initializing the member variable holding the current log level. This initialization has to be done in the .cpp file, or else we will get a linker error during compilation.

We will come back to debugging in Chapter 4, which discusses different ways in which to show what’s going on in your code.

NULL versus nullptr

As GLFW is a C library, you will see a lot of NULL values in the examples and function calls. Modern C++ has redefined NULL to nullptr, which is still compatible with the pointer type in C code. From the technical perspective, the values of NULL for a pointer and 0 as a number are the same in C, and nullptr helps to avoid ambiguous cases where a pointer was intended but a number was used (and vice versa). I will only use nullptr as there is no reason to stick with ancient definitions in 2023.

Now that you’ve worked through the source code, let’s move on and create our first window!

 

Creating your first window

After all the necessary software products have been installed, we are ready for our first smoke test. We will create a small, non-resizable window, and its only purpose is to check your system for the correct path and configuration. You will be able to move it around, minimize and restore it, and close it… that’s mostly all at this stage.

But believe me, seeing your first test window on the screen will make you smile. For basic window operations, we are going to use GLFW to open and close a window.

GLFW is an open source toolkit that is used to handle the tasks around the application window, and it is available for different OSs and hardware platforms.

GLFW will do the following tasks with a few lines of code, independent of your OS:

  • Create and destroy the application window
  • Handle the window events (such as minimize, resize, or close)
  • Add an OpenGL context or Vulkan support to enable 3D rendering
  • Get the input from the mouse, keyboard, and gamepads/joysticks

If you want to check the source for this example, head to the chapter01 folder in the Git checkout or the extracted source for this book, and then go to the 01_simple_window folder. You can follow the explanation of the code snippets, or in case you have no questions about the intention of the code lines, you can compile the code in advance and check the code snippets only for clarification.

For the window code, start with the Window.h header file:

#pragma once
#include <string>
#include <GLFW/glfw3.h>
class Window {
  public:
    bool init(unsigned int width, unsigned int height,
      std::string title);
    void mainLoop();
    void cleanup();
  private:
    GLFWwindow *mWindow = nullptr;
};

After the include guard, we need to include the std::string header, which we will use to pass the window title to the instance, and the GLFW header for the GLFW functions.

The Window class contains a handle for the GLFW window that we will create as a private member, along with three other public methods.The init() method is used to initialize the new window; the mainLoop() method runs the code of the main loop of the window where we do all the work; and the cleanup() method cleans up the window to shut down the application properly.

The implementation of the three functions is done in the Window.cpp file:

#include "Window.h"
#include "Logger.h"

We include our previously created header file for the Window class, plus the header file for the Logger class to ensure the console logging is available:

bool Window::init(unsigned int width, unsigned int height, std::string title) {
  if (!glfwInit()) {
    Logger::log(1, "%s: glfwInit() error\n",
      __FUNCTION__);
    return false;
  }
  /* set a "hint" for the NEXT window created*/
  glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
  mWindow = glfwCreateWindow(width, height,
    title.c_str(), nullptr, nullptr);
  if (!mWindow) {
    Logger::log(1, "%s: Could not create window\n",
      __FUNCTION__);
    glfwTerminate();
    return false;
  }
  Logger::log(1, "%s: Window successfully initialized\n",
      __FUNCTION__);
  return true;
}

The init() function checks whether GLFW could be initialized at all. If something unexpected happens, it will return false in the main() function and stop the program.

The window hint set with the glfwWindowHint() call is a special property in GLFW, which changes the settings for the creation of the next window. For example, we can disable the ability to resize our window. After this, the creation of the window itself is done, and the result is saved inside our member variable. If the window cannot be created, the process of creating a window will also be aborted and GLFW will be terminated. In a successful window creation, we output a log line to the console and return to the main() function, stating that everything went fine.

The mainLoop() function does nothing special for the first window; it simply checks whether the user generated an event to close the window, that is, by selecting the close button. If this is not the case, it instructs GLFW to poll any events. This call is required to react to anything happening to the window itself – keyboard presses, mouse events, and window operations such as minimizing or even closing:

void Window::mainLoop() {
  while (!glfwWindowShouldClose(mWindow)) {
    /* poll events in a loop */
    glfwPollEvents();
  }
}

Finally, the cleanup() function destroys the window and terminates GLFW, removing our window from the screen and ending the usage of GLFW. At this point, the destroy window operation is slightly redundant, as glfwTerminate() also kills all windows that are still onscreen. But using the explicit destroy function on the application window should remain here, in case of later additions to the termination process of the application:

void Window::cleanup() {
  Logger::log(1, "%s: Terminating Window\n",
      __FUNCTION__);
  glfwDestroyWindow(mWindow);
  glfwTerminate();
}

To compile the preceding code, we also need a file named CMakeLists.txt in our project folder. This file instructs the CMake build system about the configuration of the project; it states which files to compile and how to add the required additional dependencies.

In the following code snippet, at the top of the file, we set the minimum version of CMake to 3.19. This is the first version that provides support to find the shader compiler for Vulkan. We will need this in Chapter 3 for the Vulkan renderer:

cmake_minimum_required(VERSION 3.19)

Setting C++17 as the minimum version might seem a bit overkill for the projects in this book, but as I stated earlier, I will try to get rid of the legacy features of C++ and use the newer ones instead:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

The next lines add the cmake folder inside the project folder to the list of locations CMake uses to store helper scripts for the find_package command:

# use custom file to find libraries
if(WIN32)
  list(APPEND CMAKE_MODULE_PATH "${CMAKE_CUR
  RENT_LIST_DIR}/cmake")
endif()

As the current version of CMake does not search for GLFW, I have added a script to search for it. This extra script requires GLFW to be stored in a fixed location on the system, and by using the location we have chosen at installation time, we are able to use the single GLFW installation for all projects in the book, instead of having a copy per project.The GLFW search script is only needed on Windows, as Linux already includes a helper script in the GLFW package. So,we instruct CMake to only add this on Windows by using a check to WIN32. This variable is only defined on Windows.

Next, we name our project Main. You could use any arbitrary name here, and this could be used in other commands by referencing a variable. Then, we add the C++ (*.cpp) and header (.h) files in the local folder via a GLOB search and add them to the list of files to compile to our main executable, which will also be named Main. Under Windows, this will automatically get an extension, resulting in Main.exe:

project(Main)
file(GLOB SOURCES
     .h
     .cpp)
add_executable(Main ${SOURCES})

Now, the CMake command called find_package is used to locate the GLFW headers in version 3.3 or higher, marking GLFW also as required for the code compilation. The corresponding CMake helper script will set a couple of variables if GLFW has been found – here, the two important ones are GWLF3_LIBRARY and GLFW3_INCLUDE_DIR. Due to the different searches on Windows and Linux, we will reuse the GLFW3_LIBRARY variable to avoid any further splits in the control structures:

find_package(glfw3 3.3 REQUIRED)
#variable is set by FindGLFW3.cmake, reuse for Linux
if(UNIX)
  set(GLFW3_LIBRARY glfw)
endif()

Finally, the last two lines of the following code add the GLFW3 headers to the list of include paths for the compiler and the library to link to the final executable:

include_directories(${GLFW3_INCLUDE_DIR})
target_link_libraries(Main ${GLFW3_LIBRARY})

Now you can build the project, and it should compile the code without any errors or warnings. If the compilation fails, please check the Technical requirements section for all the required tools and libraries.

Start the executable file, Main.exe (Windows) or Main (Linux), and you will see a small window appear on the screen, as shown in the following screenshot:

Figure 1.20: Your first window

Figure 1.20: Your first window

Depending on your OS, the window might be filled in black, white, or even contain some parts of the screen where it was opened. The system does a “cheap” copy when creating the window, and we don’t clear the window content. So, don’t be alarmed if you don’t get exactly the same picture as Figure 1.1. As long as your window has the proper caption and the OS-specific buttons to close and minimize, everything has worked fine.

Now, let’s check out the available 3D-rendering APIs on the system.

 

Adding support for OpenGL or Vulkan to the window

Having a simple window is cool, but we need to go a bit further to draw our models using OpenGL or Vulkan. These changes will add the bare minimum of code to initialize the window for 3D rendering. It is a “smoke test” to see whether you have all the libraries and headers for Chapters 2 and 3, where we will create two triangle renderers, one for OpenGL and one for Vulkan.

GLFW and OpenGL

GLFW includes basic support for OpenGL; you only need a bunch of calls and a link to the OpenGL library. You can find the code in the 02_opengl_window folder.

Add the following lines to the Window.cpp file:

bool Window::init(unsigned int width, unsigned int height, std::string title) {
  if (!glfwInit()) {
  ...
  glfwMakeContextCurrent(mWindow);
  Logger::log(1, "%s: Window successfully initialized\n",
    __FUNCTION__);
  return true;
}

The first call is glfwMakeContextCurrent() – it gets the OpenGL context, which contains the global state of the rendering, and makes it the context of the current thread. This needs to be added to the end of the init() call.

Having the context in place, we can use some simple OpenGL calls inside the main loop of the window. Without an extension loader, this is fairly basic (Windows may be down for OpenGL version 1.x), but for pure initialization, the following is sufficient:

void Window::mainLoop() {
  glfwSwapInterval(1);
  float color = 0.0f;

Before going into the loop, we will activate the wait for the vertical sync with a call to the GLFW function, glfwSwapInterval(). Without waiting for it, the window might flicker, or tearing might occur, as the update and buffer switch will be done as fast as possible. Also, we add a color float variable, which holds our background color.

Inside the while loop, which is, again, waiting for the window to close, the color variable is incremented in small amounts and reset to zero if it reaches a value of one. The value is set using a call to the glClearColor() function as the new color to be used when clearing the draw buffer – setting the red, green, and blue results in a gray color. The call to the glClear() function, with the value set to clear only the color buffer, gives the window a simple gray background:

  while (!glfwWindowShouldClose(mWindow)) {
    color >= 1.0f ? color = 0.0f : color += 0.01f;
    glClearColor(color, color, color, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

By default, GLFW activates double buffering for the OpenGL window. This means we have two separate graphics buffers of the same size, a front buffer and a back buffer. All the changes to the final picture occur in the back buffer while showing the front buffer, which contains the image created by the previous rendering calls. This hides the creation process from the user. After the drawing of the back buffer has finished, glfwSwapBuffers() swaps the two buffers and displays the content of the back buffer, making the previous front buffer the new back buffer for the hidden drawing:

    /* swap buffers */
    glfwSwapBuffers(mWindow);

The event polling stays at the end of the loop, enabling it to move and close the window:

    /* poll events in a loop */
    glfwPollEvents();
  }
}

Note that CMakeLists.txt also needs to be extended for proper usage of OpenGL:

set(OpenGL_GL_PREFERENCE GLVND)
find_package(OpenGL REQUIRED)
target_link_libraries(Main ${GLFW3_LIBRARY} OpenGL::GL)

We have to set a variable to define the type of OpenGL; here, we are using the “vendor neutral dispatch” implementation (hence the name GLVND), and we use the find_package command to locate the OpenGL library. In addition, we have to add the OpenGL library to the command to link the final executable to it.

After compiling and starting the program, you should see a slowly flashing window. This means that your system has all the required libraries for the OpenGL renderer, which will be discussed in Chapter 2:

Figure 1.21: The filled OpenGL window

Figure 1.21: The filled OpenGL window

After checking the OS for OpenGL support to draw our characters, next, we will test whether the Vulkan-rendering API is also available.

GLFW and Vulkan

GLFW also supports the newer Vulkan API, and compared to OpenGL, this is much closer to the GPU. You can get a lot more power out of your graphics card, but with great power comes great responsibility. As you will learn, the first basic steps to initialize the Vulkan system already require a lot of work. And even with this amount of code, we are far, far away from drawing a triangle or just clearing the screen like in the OpenGL code.

The code for this example can be found in the 03_vulkan_window folder.

First, the Window.h file needs to be extended:

#include <string>
/* include Vulkan header BEFORE GLFW */
#include <vulkan/vulkan.h>
#include <GLFW/glfw3.h>

We need to include the Vulkan header, <vulkan/vulkan.h>. This has to be done before the GLFW, as the GLFW switches on specific features if it detects Vulkan.

To encapsulate all of the new Vulkan-specific code, create an initVulkan() function:

  public:
    bool initVulkan();

Two new member variables must be added in the private section of the class. We need a handle for the Vulkan instance and another handle for the Vulkan surface:

  private:
    GLFWwindow *mWindow = nullptr;
    std::string mApplicationName;
    VkInstance mInstance{};
    VkSurfaceKHR mSurface{};

Here, VkInstance stores information about the Vulkan settings in the current application, and VkSurfaceKHR is a drawable “surface” in Vulkan. This will be enhanced in Chapter 3 when we create a Vulkan renderer.

The application name has been stored as std::string since we need it in two positions.

The init() function in the Window.cpp file will be extended by two additional calls:

  if (!glfwVulkanSupported()) {
    glfwTerminate();
    Logger::log(1, "%s: Vulkan is not supported\n",
      __FUNCTION__);
    return false;
  }

The first call, glfwVulkanSupported(), checks whether Vulkan is available at all. If this fails, the machine might be missing the software or hardware capabilities in which to use Vulkan.

The second call is the new initVulkan() function; the program run will also fail if something goes wrong during the initialization process:

  if (!initVulkan()) {
    Logger::log(1, "%s: Could not init Vulkan\n",
      __FUNCTION__);
    glfwTerminate();
    return false;
  }

The new initVulkan() function starts with a data structure called VkApplicationInfo:

  VkApplicationInfo mAppInfo{};
  mAppInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
  mAppInfo.pNext = nullptr;
  ….
  mAppInfo.apiVersion = VK_MAKE_API_VERSION(0, 1, 1, 0);

This contains basic information about the application, such as the name and the version. Most of the fields are optional, but we need at least three of them:

  • You will see .sType in many of the Vulkan data structures. This is required for Vulkan to know what kind of struct you pass to it. The naming is always VK_STRUCTURE_TYPE_*.
  • Here, .pNext will always be nullptr. It could be used to link different Vulkan structures.
  • .apiVersion must be set to the minimum Vulkan API version that we want to use. Here, we generate version 1.1.0.

With a call to glfwGetRequiredInstanceExtensions(), we check whether we have the required extensions to run a Vulkan application:

  uint32_t extensionCount = 0;
  const char** extensions =
      glfwGetRequiredInstanceExtensions(&extensionCount);
  if (extensionCount == 0) {
    Logger::log(1, "%s error: no Vulkan extensions
      found\n", __FUNCTION__);
    return false;
  }

The preceding code block returns the number of extensions and the extension names as a C-style array. We need extension names for the Vulkan initialization, but if we get no extensions at all, then again, there is no proper support for Vulkan, and we terminate the program by returning false from the Vulkan init function.

The next structure is VkInstanceCreateInfo:

  VkInstanceCreateInfo mCreateInfo{};
  mCreateInfo.sType =
    VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  mCreateInfo.pNext = nullptr;
  mCreateInfo.pApplicationInfo = &mAppInfo;
  mCreateInfo.enabledExtensionCount = extensionCount;
  mCreateInfo.ppEnabledExtensionNames = extensions;
  mCreateInfo.enabledLayerCount = 0;
  result = vkCreateInstance(&mCreateInfo, nullptr,
    &mInstance);
  if (result != VK_SUCCESS) {
    Logger::log(1, "%s: Could not create Instance
      (%i)\n", __FUNCTION__, result);
    return false;
  }

The VkInstanceCreateInfo struct also contains the .sType and .pNext fields, along with a link to the application info structure and the extensions we found. Having this information collected, we can call vkCreateInstance() to create a Vulkan instance. The instance includes the storage for the Vulkan state on the application level, and there is no longer a system global state (“context”) like in OpenGL.

Now, let’s see how many graphics cards we can find in the system:

  uint32_t physicalDeviceCount = 0;
  vkEnumeratePhysicalDevices(mInstance,
    &physicalDeviceCount, nullptr);
  if (physicalDeviceCount == 0) {
    Logger::log(1, "%s: No Vulkan capable GPU found\n",
      __FUNCTION__);
    return false;
  }
  std::vector<VkPhysicalDevice> devices;
  vkEnumeratePhysicalDevices(mInstance,
    &physicalDeviceCount, devices.data());

The call to vkEnumeratePhysicalDevices() has to be done twice. The first time, we will only get the number of GPUs, and if we find one or more GPUs, the second call will be used to fill the corresponding array with data about the GPUs.

For the last step, we will create the Vulkan surface using glfwCreateWindowSurface():

  result = glfwCreateWindowSurface(mInstance, mWindow,
    nullptr, &mSurface);
  if (result != VK_SUCCESS) {
    Logger::log(1, "%s: Could not create Vulkan
      surface\n", __FUNCTION__);
    return false;
  }

If this call is successful, full support for Vulkan will become available on your machine.

The Vulkan surface and the instance need to be deleted in the cleanup() function, along with the GLFW window:

void Window::cleanup() {
  Logger::log(1, "%s: Terminating Window\n",
    __FUNCTION__);
  vkDestroySurfaceKHR(mInstance, mSurface, nullptr);
  vkDestroyInstance(mInstance, nullptr);
  glfwDestroyWindow(mWindow);
  glfwTerminate();
}

Additionally, the configuration of CMake needs to be changed, and we have to find Vulkan. Add the respective change specified in the following lines to the CmakeLists.txt file:

find_package(Vulkan REQUIRED)
target_link_libraries(Main ${GLFW3_LIBRARY} Vulkan::Vulkan)

Note that find_package is used to locate the Vulkan SDK, which contains the header and libraries. Also, we have to link the final executable to the Vulkan library to be able to use the Vulkan calls.

The output window that is created when you run the code is similar to Figure 1.1. Again, you will get a simple window, but this time, it will be filled with a static color or with fragments of your current screen. The code from this example does not clear the screen, but in this chapter, we want to check only for the general availability of the Vulkan API. So, we need to rely on the log output. If you see a line saying Found physical device(s) and that the window was successfully initialized after running the code, you are ready to go for the Vulkan renderer in Chapter 3:

initVulkan: Found 2 Vulkan extensions
initVulkan: VK_KHR_surface
initVulkan: VK_KHR_xcb_surface
initVulkan: Found 1 physical device(s)
init: Window successfully initialized

At the very least, you need the VK_KHR_surface extension that is listed in the output. Other extensions might appear too, depending on your OS and the graphics drivers.

After we have checked the OS for support of one or both rendering APIs, we will add some code to the Window class. This code will ensure our application behaves like every other application window on the system.

 

Event handling in GLFW

Many modern OSs are event-based – the programs don’t just sit there and ask the OS over and over if any mouse or keyboard input has occurred or if the window has been moved, minimized, or resized. All these events are stored in an event queue and must be handled by the application code. If you never request the events of that queue, your application window won’t even close in a proper manner, that is, it can only be killed using Task Manager.

You can find the example code for these additions in the 04_event_handling folder.

Let’s have a look at how GLFW handles the events from the OS.

The GLFW event queue handling

You have already seen a bit of the event handling in the code for the Window class – we used these two GLFW calls to close our window and end the application:

int glfwWindowShouldClose(GLFWwindow *win);
void glfwPollEvents();

The first call, glfwWindowShouldClose(), checks whether an application window should be closed. This event is generated after the user clicks on the top-right close icon of the window. We are using this as a condition to step out of our while() loop, end the mainLoop() method of the Window class, and start the cleanup process.

Important note

The call to glfwPollEvents() is required in order to empty the event queue. It will also run any configured callbacks. If you forget this call, your window will do nothing, not even close down.

You should call glfwPollEvents() at the end of the main loop to process the newly arrived events.

There is another call to clear the event queue and fire the callbacks:

void glfwWaitEvents();

This one puts the thread to sleep and waits until at least one event has been generated for the window. Usually, this is used in non-interactive applications that are waiting for any input from the user.

Mixing the C++ classes and the C callbacks

A simple starting point is to react to the window close request and just output a message to the user. To get this to work, we need two parts – the function called by GLFW and a call that sets the function as a callback.

This sounds easy to do, but only at first glance. As GLFW is pure C code, it has no knowledge about C++ classes, member functions, the this pointer, and all the other moving parts. However, there are some solutions to this.

The first way is that we could use a static function of our Window class as it is technically similar to a C function. At the moment, we won’t use more than one application window, but if we add support for a pop-out window later, we might be in trouble with the static class function. It is the same for all objects of that class, and as it can only access static members, you have to take extra steps to avoid even more trouble when starting with multithreaded code.

So, let’s consider the second way and use a “free” function, outside the class, to dispatch the call to the C++ class. However, instead of having to define two separate functions for every callback, we will use a Lambda.

Lambda functions

A lambda is a small piece of code, running as an anonymous function. It has no visible name, takes the number and types of arguments from its definition, and runs the code. Internally, the lambda function is converted into a small class by your compiler; there is no magic applied here. It’s only a convenient way to help reduce the code you write. If you want to know more about lambda functions, you can find a link to a tutorial in the Additional resources section.

The authors of GLFW are aware of this problem and have added a small helper to every window that might be created – a pointer that can be set and read by the user:

void glfwSetWindowUserPointer(GLFWwindow *win, void *ptr);

You can store any arbitrary data in the user pointer – it doesn’t have to be the this pointer of the class object, but it is only a pointer and must be accessible by your code. We will use it to store the pointer in our C++ Window object, and inside the lambda, this pointer will be read and used just like in any other C++ call.

The callback function itself looks a bit weird if you have never used C-style callbacks:

GLFWwindowclosefun glfwSetWindowCloseCallback (GLFWwindow *window, GLFWwindowclosefun callback);

It requires a pointer to a function and returns either NULL, if this is the first call, or the pointer to a previously set callback function. You could change this callback during runtime, which means moving to a different dialog to display any unsaved changes.

The last part of the puzzle is the window close function, which is called by the callback:

typedef void(* GLFWwindowclosefun) (GLFWwindow *window)

The GLFWwindowclosefun function is created using typedef, just like the other functions used for callbacks. This is done to avoid writing the expression in the second braces every time we use the function. As this is still C code, sadly, no modern C++ enhancements are available to change it.

And this is how you should put all the parts together – by adding the following lines to the init() function of the Window.cpp file:

  glfwSetWindowUserPointer(mWindow, this);
  glfwSetWindowCloseCallback(mWindow, [](GLFWwindow *win) {
    auto thisWindow = static_cast<Window*>(
      glfwGetWindowUserPointer(win));
    thisWindow->handleWindowCloseEvents();
  });

Here, the lambda is introduced by the square brackets, [], followed by the parameters the function takes. You could even capture some data from the outside of the function using the brackets, making it available, like in normal functions. We can’t use this capturing method for C-style callbacks, as such captures are not compatible with a function pointer.

Inside the lambda function, we can retrieve the user pointer set by glfwSetWindowUserPointer(), cast it back to a pointer to an instance of our Window class (this is our application window), and call the member function to handle the event. The function does not need to get the GLFWwindow parameter, as we already saved it as a private member in the Window class. The result of glfwSetWindowCloseCallback() can be safely ignored. It returns the address of the callback function that was set in a previous call. This is the first call in the code, so it will simply return NULL.

The class member function needs to be added to Window.cpp:

void Window::handleWindowCloseEvents() {
  Logger::log(1, "%s: Window got close event... bye!\n",
    __FUNCTION__);
}

Currently, the handleWindowCloseEvents() function just prints out a log line and does nothing else. But this is the perfect place to check whether the user really wants to quit or if unsaved changes have been made.

This function has to be declared in the Window.h header file, too:

private:
  void handleWindowCloseEvents();

If you start the compiled code and close the window, you should get an output like this:

init: Window successfully initialized
handleWindowCloseEvents: Window got close event... bye!
cleanup: Terminating Window

You can check the other events in the GLFW documentation and add other callback functions plus the respective lambdas. Additionally, you can check the example code for more calls – it has simple support for window movement, minimizing and maximizing, and printing out some log messages when the events are processed.

Important note

Some OSs stall the window content update if your application window has been moved or resized. So, don’t be alarmed if this happens – it is not a bug in your code. Workarounds are available to keep the window content updated on these window events, and you can check the GLFW documentation to find a way to solve this.

Now that our application window behaves in the way we would expect, we should add methods for a user to control what happens in our program.

 

The mouse and keyboard input for the game window

Adding support for the keys pressed on the keyboard, the buttons on the mouse, or moving the mouse around is a simple copy-and-paste task from the window events – create a member function to be called and add the lambda-encapsulated call to GLFW. The next time you press a key or move the mouse after a successful recompilation, the new callbacks will run.

You can find the enhanced example code in the 05_window_with_input folder.

Let’s start by retrieving the key presses before we add the keyboard callbacks and functions. After this, we will continue to get mouse events and also add the respective functions for them to the code.

Key code, scan code, and modifiers

To get the events for the keys the user presses or releases on their keyboard, GLFW offers another callback. The following callback for a plain key input receives four values:

glfwSetKeyCallback(window, key_callback);
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)

These values are listed as follows:

  • The ASCII key code of the key
  • The (platform-specific) scan code of that key
  • The action you carried out (press the key, release it, or hold it until the key repeat starts)
  • The status of the modifier key, such as Shift, Ctrl, or Alt

The key can be compared with internal GLFW values such as GLFW_KEY_A, as they emit the 7-bit ASCII code of the letter you pressed. The function keys, the separate keypad, and the modifier keys return values >256.

The scan code is specific to your system. While it stays the same on your system, the code may differ on another platform. So, hardcoding it into your code is a bad idea.

The action is one of the three values GLFW_PRESS, GLFW_RELEASE, or GLFW_REPEAT, if the key is pressed for longer, but note that the GLFW_REPEAT action is not issued for all keys.

The modifier status is a bitmap to see whether the users pressed keys such as Shift, Ctrl, or Alt. You can also enable the reporting of Caps Lock and Num Lock – this is not enabled in the normal input mode.

For example, we could add a simple keyboard logging to the code. First, add a new function to the Window.h header file:

public:
  void handleKeyEvents(int key, int scancode, int action,
    int mods);

As you can see in the preceding code, we don’t need GLFWwindow in our functions, as we already saved it as a private data member of the class.

Next, add the callback to the GLFW function using a lambda:

  glfwSetKeyCallback(mWindow, [](GLFWwindow *win, int key,
    int scancode, int action, int mods) {
    auto thisWindow = static_cast<Window*>(
      glfwGetWindowUserPointer(win));
    thisWindow->handleKeyEvents(key, scancode, action,
      mods);
    }
  );

This is the same as it was for the window event – get the this pointer of the current instance of the Window class from the user pointer set by glfwSetWindowUserPointer() and call the new member functions of the class.

For now, the member function for the keys can be simple:

void Window::handleKeyEvents(int key, int scancode, int action, int mods) {
  std::string actionName;
  switch (action) {
    case GLFW_PRESS:
      actionName = "pressed";
      break;
    case GLFW_RELEASE:
      actionName = "released";
      break;
    case GLFW_REPEAT:
      actionName = "repeated";
      break;
    default:
      actionName = "invalid";
      break;
  }
  const char *keyName = glfwGetKeyName(key, 0);
  Logger::log(1, "%s: key %s (key %i, scancode %i) %s\n",
    __FUNCTION__, keyName, key, scancode,
    actionName.c_str());
}

Here, we use a switch() statement to set a string depending on the action that has occurred and also call glfwGetKeyName() to get a human-readable name of the key. If no name has been set, it prints out (null). You will also see the key code, which is the ASCII code for letters and numbers, as mentioned earlier in this section, and the platform-specific scan code of the key. As a last field, it will print out if the key was pressed, released, or held until the key repeat from the OS started. The default option is used for completeness here; it should never be called in the current GLFW version as it would indicate a bug.

Different styles of mouse movement

GLFW knows two types of mouse movement: the movement adjusted by the OS and a raw movement.

The first one returns the value with all the optional settings you might have defined, such as mouse acceleration, which speeds up the cursor if you need to move the cursor across the screen.

The following is a callback function, which gets informed if the mouse position changes:

glfwSetCursorPosCallback(window, cursor_position_callback);
void cursor_position_callback(GLFWwindow* window,
  double xpos, double ypos)

Alternatively, you can poll the current mouse position in your code manually:

double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);

The raw mode excludes these settings and provides you with the precise level of movement on your desk or mouse mat. To enable raw mode, first, you have to disable the mouse cursor in the window (not only hide it), and then you can try to activate it:

glfwSetInputMode(window, GLFW_CURSOR,
  GLFW_CURSOR_DISABLED);
if (glfwRawMouseMotionSupported()) {
    glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION,
      GLFW_TRUE);
}

To exit raw mode, go back to the normal mouse mode:

glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);

Keeping both movement styles apart will be interesting for the kind of application we are creating. If we want to adjust the settings using an onscreen menu, having the mouse pointer react like it would in other applications on your computer is perfect. But once we need to rotate or move the model, or change the view in the virtual world, any acceleration could lead to unexpected results. For this kind of mouse movement, we should use the raw mode instead.

To add a mouse button callback, add the function call to Window.h:

private:
  void handleMouseButtonEvents(int button, int action,
    int mods);

And in Window.cpp, add the callback handling and the function itself:

  glfwSetMouseButtonCallback(mWindow, [](GLFWwindow *win,
    int button, int action, int mods) {
    auto thisWindow = static_cast<Window*>(
      glfwGetWindowUserPointer(win));
    thisWindow->handleMouseButtonEvents(button, action,
      mods);
    }
  );

This is similar to the keyboard callback discussed earlier in this chapter; we get back the pressed button, the action (GLFW_PRESS or GLFW_RELEASE), and also any pressed modifiers such as the Shift or Alt keys.

The handler itself is pretty basic in the first version. The first switch() block is similar to the keyboard function, as it checks whether the button has been pressed or released:

void Window::handleMouseButtonEvents(int button,
  int action, int mods) {
  std::string actionName;
  switch (action) {
    case GLFW_PRESS:
      actionName = "pressed";
      break;
    case GLFW_RELEASE:
      actionName = "released";
      break;
    default:
      actionName = "invalid";
      break;
  }

The second switch() block checks which mouse button was pressed, and it prints out the names of the left, right, or middle buttons. GLFW supports up to eight buttons on the mouse, and more than the basic three are printed out as "other":

  std::string mouseButtonName;
  switch(button) {
    case GLFW_MOUSE_BUTTON_LEFT:
      mouseButtonName = "left";
      break;
    case GLFW_MOUSE_BUTTON_MIDDLE:
      mouseButtonName = "middle";
      break;
    case GLFW_MOUSE_BUTTON_RIGHT:
      mouseButtonName = "right";
      break;
    default:
      mouseButtonName = "other";
      break;
  }
  Logger::log(1, "%s: %s mouse button (%i) %s\n",
    __FUNCTION__, mouseButtonName.c_str(), button,
    actionName.c_str());
}

When running the code, you should see messages like this:

init: Window successfully initialized
handleWindowMoveEvents: Window has been moved to 0/248
handleMouseButtonEvents: left mouse button (0) pressed
handleMouseButtonEvents: left mouse button (0) released
handleMouseButtonEvents: middle mouse button (2) pressed
handleMouseButtonEvents: middle mouse button (2) released
handleWindowCloseEvents: Window got close event... bye!
cleanup: Terminating Window

You could add more handlers. The example code also uses the callbacks for mouse movement, which gives you the current mouse position inside the window, and the callback for entering and leaving the window.

 

Summary

In this chapter, we made the first steps toward a much bigger project. We started with a simple window, whose only task was to be closed again. This showed us the general usage of GLFW. In the next section, we added OpenGL support, and we also tried to detect support for the Vulkan API. If one of them fails (most probably Vulkan), you could continue with OpenGL and skip Chapter 3. The remaining code in this book will be built independently of the renderer and run with OpenGL and Vulkan as the rendering APIs. After the 3D rendering capabilities, we added the handling of the basic window events. Finally, we added the handling of the keyboard for mouse events, allowing us to build view controls and movement in our virtual 3D world.

With these building blocks, you can now create application windows using only a few lines of code. Additionally, you can retrieve input from the mouse and keyboard and prepare the window to display hardware-accelerated graphics. What is shown inside this window is up to your imagination.

In Chapter 2, we will create a basic OpenGL renderer.

 

Practical sessions

You will see this section at the end of every chapter in the book. Here, I will add a bunch of suggestions and exercises that you can try out with the code on GitHub.

Usually, there’s no danger in doing something wrong while experimenting. Changing lines, deleting, or adding new code may end in your program no longer compiling or even crashing, but your computer will not explode if you make mistakes. In the few cases where hazardous behavior can occur (such as overwriting some of your files), I will attach a big red warning sticker.

So, here’s something for you to try. After you have created the window, you might notice that you still can’t resize it (the setting was done intentionally). You might also want to change the title of the window to make it more like your very own application. And the handling of the mouse and keyboard could also use a little bit of polish.

You could try to do the following:

  • Play around with the window title. You can change it at any time after its creation, and it can store a lot of information in an easily accessible place. You could use it for the name of the model you loaded, the animation replay speed, and more.
  • Set a callback for the handling of window resizing. This will be handy once we have enabled 3D rendering, and you will need to adjust the sizes of the other buffers too.
  • Store information about some keys, such as W, A, S, and D or the cursor keys. Set the status when pressed and clear it on release. We will need the stored status of the keys in Chapter 5 to move around inside the virtual world.
  • Add support for mouse movement on a mouse button press only. Imagine you would like to rotate the view around your animated model while the left button is being pressed or zoom in and out while the right button is being pressed.
 

Additional resources

For further reading, please take a look at the following resources:

About the Authors
  • Michael Dunsky

    Michael Dunsky is an educated electronics technician, game developer, and console porting programmer with more than 20 years of programming experience. He started at the age of 14 with BASIC, adding on his way Assembly language, C, C++, Java, Python, VHDL, OpenGL, GLSL, and Vulkan to his portfolio. During his career, he also gained extensive knowledge in virtual machines, server operation, infrastructure automation, and other DevOps topics. Michael holds a Master of Science degree in Computer Science from the FernUniversität in Hagen, focused on computer graphics, parallel programming and software systems.

    Browse publications by this author
  • Gabor Szauer

    Gabor Szauer has been making games since 2010. He graduated from Full Sail University in 2010 with a bachelor's degree in game development. Gabor maintains an active Twitter presence, and maintains a programming-oriented game development blog. Gabor's previously published books are Game Physics Programming Cookbook and Lua Quick Start Guide, both published by Packt.

    Browse publications by this author
Latest Reviews (2 reviews total)
good book with clear instruction, a lot of useful, realistic applications. good author, detailed.
C++ Game Animation Programming - Second Edition
Unlock this book and the full library FREE for 7 days
Start now