Fenster öffnen und OpenGL initialisieren
Ziel dieses Kapitels ist es, ein Fenster inkl. OpenGL-Kontext zu initialisieren. Dieses Fenster soll per Tastenkombination geschlossen oder maximiert werden können.

Fehlerbehandlung in der Main
Zuerst aktualisieren wir unsere main.cpp. Hier wird einerseits die Datei renderer.h eingebunden, um Zugriff auf die Renderer-Klasse zu bekommen. Andererseits wird per try+catch auf Fehler geprüft.
main.cpp
#include "renderer.h"
#include <iostream>
int main()
{
try
{
Renderer renderer("Grundlagen der Computergrafik", 1280, 720);
renderer.start();
}
catch (const std::exception &e)
{
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Die Klasse Renderer
In C++ bestehen Klassen üblicherweise aus zwei Dateien. In der Header-Datei .h wird die Klasse definiert und die Methoden der Klasse deklariert. In der passenden .cpp-Datei werden die einzelnen Methoden definiert bzw. implementiert.
Hier ist die Header-Datei unserer Renderer-Klasse.
renderer.h
#pragma once
#define GLFW_INCLUDE_GLEXT
#include <GLFW/glfw3.h>
#include <string>
class Renderer
{
public:
Renderer(const std::string &title, uint32_t width, uint32_t height);
~Renderer();
void start();
void onKeyboardInput(GLFWwindow *window, int key, int scancode, int action, int mods);
private:
GLFWwindow *window = nullptr;
};
Konstruktor
Die Implementierung der einzelnen Methoden kommt in die .cpp-Datei. Hier im Konstruktor wird GLFW initialisiert und im Fehlerfall eine Exception geworfen.
renderer.cpp
#include "renderer.h"
#include <iostream>
Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
glfwSetErrorCallback([](int error, const char *description)
{
std::cerr << "GLFW Error: " << description << std::endl;
});
if (!glfwInit())
{
throw std::runtime_error("Failed to initialize GLFW");
}
window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!window)
{
glfwTerminate();
throw std::runtime_error("Failed to open window");
}
glfwMakeContextCurrent(window);
}
Fehlerbehandlung
Damit wir mitbekommen, ob während der Laufzeit unserer Anwendung irgendetwas bei der Kommunikation mit der Grafikkarte schiefläuft, benötigen wir eine Ausgabe von Fehlermeldungen.
GLFW gibt uns hier die sehr einfache Möglichkeit, ein Fehler-Callback mittels glfwSetErrorCallback zu implementieren. Sie können dies oben im Konstruktor sehen. Das Prinzip kennen Sie bereits von Hello World: Wir machen eine Ausgabe auf die Kommandozeile. Statt std::cout verwenden wir hier allerdings std::cerr und klassifizieren unsere Ausgabe damit als Fehlermeldung.
Destruktor
Analog dazu werden im Destruktor alle Ressourcen von GLFW wieder freigegeben:
renderer.cpp
Renderer::~Renderer()
{
glfwDestroyWindow(window);
glfwTerminate();
}
Message Loop
Um Echtzeitgrafik anzuzeigen, muss der Computer viele Einzelbilder generieren und in schneller Folge anzeigen. Würde man dafür ein einfaches while(true) verwenden, wäre die Anwendung vollkommen ausgelastet und nicht mehr in der Lage, beispielsweise auf Maus oder Tastatur zu reagieren.
Aus diesem Grunde implementiert man einen sogenannten Message Loop. Hier in der start()-Methode des Renderers sehen wir das. Unsere Anwendung wird in einer Endlosschleife immer wechselseitig ein Bild rendern und dann beim Betriebssystem nachfragen, ob eine Nachricht vorliegt. Eine solche Nachricht könnte z.B. sein “Benutzer möchte Fenster maximieren”. Solange Nachrichten vorliegen, werden diese abgearbeitet und anschließend ein weiteres Bild gezeichnet.
renderer.cpp
void Renderer::start()
{
while (!glfwWindowShouldClose(window))
{
glfwSwapBuffers(window);
glfwPollEvents();
}
}
Tastatureingaben
Um Tastatureingaben entgegenzunehmen, müssen wir im Anschluss an die Erstellung des Fensters ein Key-Callback registrieren. Das Prinzip ist das gleiche wie beim Fehler-Callback. Wir implementieren nun allerdings eine Klassenmethode, um die Eingaben zu verarbeiten.
renderer.cpp
void Renderer::onKeyboardInput(GLFWwindow *window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, GL_TRUE);
}
else if (key == GLFW_KEY_M && action == GLFW_PRESS)
{
if (glfwGetWindowAttrib(window, GLFW_MAXIMIZED))
{
glfwRestoreWindow(window);
}
else
{
glfwMaximizeWindow(window);
}
}
}
Callback registrieren
Wir erinnern uns: GLFW ist eine C-Bibliothek und hat keine Ahnung von Objektorientierung. Entsprechend können wir nicht so einfach die eben definierte Methode Renderer::onKeyboardInput als Callback übergeben. Stattdessen verwenden wir glfwSetWindowUserPointer(), um eine Verbindung zwischen dem von GLFW verwalteten Fenster und unserer Instanz des Renderers zu hinterlegen. Mit glfwGetWindowUserPointer() im Callback können wir dann wieder auf den Renderer zugreifen und das eigentliche Callback aufrufen.
renderer.cpp
Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
...
glfwSetWindowUserPointer(window, this);
glfwSetKeyCallback(window, [](GLFWwindow *window, int key, int scancode, int action, int mods)
{
Renderer *self = static_cast<Renderer *>(glfwGetWindowUserPointer(window));
self->onKeyboardInput(window, key, scancode, action, mods);
});
}
Anwendung starten
Um zu testen, ob alles geklappt hat, ist es das Einfachste, im Terminal den folgenden Befehl einzugeben.
make run_cgb_02
Lernziele
Nach diesem Kapitel sollten Sie die folgenden Fragen beantworten können:
- Warum teilt man C+±Klassen in eine Header-Datei (
.h) und eine Implementierungsdatei (.cpp) auf? - Was bewirkt
#pragma onceund welches Problem löst es? - Was ist RAII und wie setzen Konstruktor und Destruktor dieses Prinzip um?
- Wie funktioniert die Ausnahmebehandlung mit
tryundcatchin C++ und wie unterscheidet sie sich von Java? - Was ist ein OpenGL-Kontext und warum muss er explizit erstellt und aktiviert werden?
- Was ist ein Message Loop (Game Loop) und warum ist eine einfache
while(true)-Schleife ohneglfwPollEvents()problematisch? - Was ist Double Buffering und welches Problem löst
glfwSwapBuffers()? - Warum kann man eine C+±Methode nicht direkt als C-Callback übergeben, und wie umgeht man das mit
glfwSetWindowUserPointer()?
Bearbeitungshinweise
Arbeiten Sie dieses Kapitel eigenständig vor dem zugehörigen Veranstaltungstermin durch. Wie die Vorbereitung abläuft und worauf es dabei ankommt, erfahren Sie im Konzept der Veranstaltung.