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.

Screenshot 2024-10-09 at 124529.png

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 once und welches Problem löst es?
  • Was ist RAII und wie setzen Konstruktor und Destruktor dieses Prinzip um?
  • Wie funktioniert die Ausnahmebehandlung mit try und catch in 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 ohne glfwPollEvents() 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.