Ziel dieses Kapitels ist es ein Fenster incl. Open GL 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 .ccp 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 wahrend der Laufzeit unserer Anwendung irgendetwas bei der Kommunikation mit der Grafikkarte schief lä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 entgegen zu nehmen, müssen wir im Anschluss an die Erstellung des Fensters ein KeyCallback 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 die folgenden beiden Befehle einzugeben.
$ make run_cgb_02
Hausaufgabe
- Räumen Sie Ihren Code auf, schreiben Sie Kommentare und machen Sie sich Notizen für die Prüfung.
- Bereiten Sie eigenständig bis zur nächsten Veranstaltung Kapitel 3 vor: Implementieren Sie dazu das Tutorial von meiner Website und ziehen Sie weitere Quellen zu Rate um exakt zu verstehen was die Software macht und warum.