Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
C++ Game Animation Programming
C++ Game Animation Programming

C++ Game Animation Programming: Learn modern animation techniques from theory to implementation using C++, OpenGL, and Vulkan , Second Edition

Arrow left icon
Profile Icon Michael Dunsky Profile Icon Gabor Szauer
Arrow right icon
€37.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.6 (12 Ratings)
Paperback Dec 2023 480 pages 2nd Edition
eBook
€20.98 €29.99
Paperback
€37.99
Subscription
Free Trial
Renews at €18.99p/m
Arrow left icon
Profile Icon Michael Dunsky Profile Icon Gabor Szauer
Arrow right icon
€37.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.6 (12 Ratings)
Paperback Dec 2023 480 pages 2nd Edition
eBook
€20.98 €29.99
Paperback
€37.99
Subscription
Free Trial
Renews at €18.99p/m
eBook
€20.98 €29.99
Paperback
€37.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Colour book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

C++ Game Animation Programming

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:

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Learn how to create a game skeleton with keyboard and mouse controls along with modern graphics
  • Gain insights into model loading, character animations, inverse kinematics, and debugging techniques
  • Master the art of creating animated characters and controlling their various aspects
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

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 find expanded content on high-performance graphics and modern animation techniques, along with improved workflows and enhanced guidance on using OpenGL and Vulkan. 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.

Who is this book for?

This book is for curious C++ developers, game programmers, game designers, and character animators, either pursuing this as a hobby or profession, who have always wanted to look behind the curtain and see how character animation in games works. The book assumes basic C++ and math knowledge, and you should be able to read code and math formulas to get the most out of this book.

What you will learn

  • Create simple OpenGL and Vulkan applications and work with shaders
  • Explore the glTF file format, including its design and data structures
  • Design an animation system with poses, clips, and skinned meshes
  • Find out how vectors, matrices, quaternions, and splines are used in game development
  • Discover and implement ways to seamlessly blend character animations
  • Implement inverse kinematics for your characters using CCD and FABRIK solvers
  • Understand how to render large, animated crowds efficiently
  • Identify and resolve performance issues
Estimated delivery fee Deliver to Italy

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Dec 01, 2023
Length: 480 pages
Edition : 2nd
Language : English
ISBN-13 : 9781803246529
Languages :
Concepts :
Tools :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Colour book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Estimated delivery fee Deliver to Italy

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Publication date : Dec 01, 2023
Length: 480 pages
Edition : 2nd
Language : English
ISBN-13 : 9781803246529
Languages :
Concepts :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 105.97
Mastering Graphics Programming with Vulkan
€33.99
Building Low Latency Applications with C++
€33.99
C++ Game Animation Programming
€37.99
Total 105.97 Stars icon

Table of Contents

21 Chapters
Part 1:Building a Graphics Renderer Chevron down icon Chevron up icon
Chapter 1: Creating the Game Window Chevron down icon Chevron up icon
Chapter 2: Building an OpenGL 4 Renderer Chevron down icon Chevron up icon
Chapter 3: Building a Vulkan Renderer Chevron down icon Chevron up icon
Chapter 4: Working with Shaders Chevron down icon Chevron up icon
Chapter 5: Adding Dear ImGui to Show Valuable Information Chevron down icon Chevron up icon
Part 2: Mathematics Roundup Chevron down icon Chevron up icon
Chapter 6: Understanding Vector and Matrix Chevron down icon Chevron up icon
Chapter 7: A Primer on Quaternions and Splines Chevron down icon Chevron up icon
Part 3: Working with Models and Animations Chevron down icon Chevron up icon
Chapter 8: Loading Models in the glTF Format Chevron down icon Chevron up icon
Chapter 9: The Model Skeleton and Skin Chevron down icon Chevron up icon
Chapter 10: About Poses, Frames, and Clips Chevron down icon Chevron up icon
Chapter 11: Blending between Animations Chevron down icon Chevron up icon
Part 4: Advancing Your Code to the Next Level Chevron down icon Chevron up icon
Chapter 12: Cleaning Up the User Interface Chevron down icon Chevron up icon
Chapter 13: Implementing Inverse Kinematics Chevron down icon Chevron up icon
Chapter 14: Creating Instanced Crowds Chevron down icon Chevron up icon
Chapter 15: Measuring Performance and Optimizing the Code Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.6
(12 Ratings)
5 star 83.3%
4 star 8.3%
3 star 0%
2 star 0%
1 star 8.3%
Filter icon Filter
Top Reviews

Filter reviews by




Salim Pamukcu Feb 06, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
good book with clear instruction, a lot of useful, realistic applications. good author, detailed.
Feefo Verified review Feefo
CireWire Dec 05, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Since I'm working in the game industry, I thought this book was a great resource to have next to me while I worked on my projects.It is very in-depth with examples, code snippets, and a github repository to make sure you get the most out of this resource.The math behind the theories are just as beautiful. Vectors, Matrices, Splines, everything needed to make sure you succeed in this skill are in this book.For anyone interested in learning more about C++ animation techniques or want to level up your established skillset, I highly recommend this book!
Amazon Verified review Amazon
Zach Peterson Dec 01, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I've been a game engine developer for 3 years now, and yet I've never worked on an animation system before. Reading this book has been a really great insight on everything animation, from the math involved to writing an efficient animation system in Vulkan. The entire process is easy to follow, which was a pleasant surprise for learning a new field. And I bet you'll learn a lot more than animation with the last few chapters of the book on optimization.
Amazon Verified review Amazon
Kirito Dec 28, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
If this book sounds scary, it could be. A good level of understanding of how C++ works and a decent working knowledge on math will help you understand the concepts quicker.That being said, I believe how this book is taught, works with learners of all styles whether they are a beginner or an advanced user.It's not easy to cover such complex topics in a condensed form, but for the size of this book, I think it does it well and conveys the concepts of OpenGL, Vulkan, C++, how the GPU runs, and how Animation works in the background.From someone who is an Unreal and Unity developer, this, I believe, would help you better PoC (Proof of Concept) animation a lot easier in engines like those. Especially in Unreal Engine as it uses C++.The provided abundant snippets and concise content brings the users learning experience to be focused and direct. A good amount of practice for a beginner and an extensible modular approach for the more advanced user, this book would be a great addition to your arsenal as a game developer of all degrees interested in Animation.
Amazon Verified review Amazon
Lifegood Dec 02, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
The book gives many insights into the basics of creating graphics for the game but delves into the mathematical aspects of it. Matrix and Vectors are the essential parts of creating animations. The information about animation and modeling is great and a good starting point if you want to get into it. A plus is that gives you information on optimized C++ too. So overall, great book
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela