Der 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 sogeannnen Message Loop. 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, wird unsere Anwendung dem Betriebssystem die nötige Zeit geben um auf diese Anfragen zu reagieren und erst anschließend wird ein weiteres Bild gezeichnet.

Um das möglichst sauber zu implementieren, werden wir alle Funktionen die mit der Erstellung und Verwaltung des Betriebssystem-Fensters zu tun haben im unsere window.cpp kapseln und sicherstellen, dass an keiner anderen Selle der Engine direkt auf das Fenster zugegriffen werden kann. Wie nennen diese Komponente Fenstereinheit oder Window Unit da es sich um eine translation unit im Sinne von C/C++ handelt.

window.cpp

Zuerst benötigen wir natürlich wieder Zugriff auf die wichtigsten Funktionen von GLFW. Außerdem nutzen wir iostream um Fehlermeldungen auszugeben und wir binden die settings.h ein, damit wir beim Erstellen des Fensters wissen wie groß das Fenster sien soll und ob es im Vollbildmodus gestartet werden soll oder nicht.

Dies bringt uns zu den folgenden Include-Statements:

#define GLFW_INCLUDE_GLEXT
#include <GLFW/glfw3.h>
#include <iostream>
#include "settings.h"

Vorwärtsdeklarationen

Damit der Kompiler in der Lage ist zu prüfen ob wir eine Funktion mit den richtigen Argumenten aufrufen, muss er die Signaturen aller aufgerufenen Funktionen kennen. Das gilt auch für Funktionen die in anderen Übersetzungseinheiten, also anderen .cpp Dateien implementiert wurden. Dafür gibt es zwei Möglichkeiten. Im Bachelorkurs nutzen wir Header Dateien. Hier nutzen wir die klassische Forward Deklaration ohne Header Dateien. Das heißt wir vermerken im Kopf der Datei die Signaturen aller Funktionen die wir aus anderen Dateien aufrufen wollen. Dies bietet sich hier an, das es nur eine einzige Funktion betrifft und das Verwalten einer .h Datei in keinem Verhältnis zum nutzen stehen würde.

void graphicsSetWindowSize(int, int);

Wir nutzen graphicsSetWindowSize dazu unsere Grafikeinheit mitzuteilen wann immer sich die Größe des Viewports geändert hat. Das passiert üblicherweise einmal beim Start der Anwendung und danach bei jeder Änderung der Fenstergröße.

Statische Variablen

Die folgenden 4 Variablen nutzen wir um uns den Fensterzustand zu merken. Sie sind als static markiert damit andere Programmteile keinen Zugriff darauf haben. Wir erinnern uns: static global variables in c/c++ verhalten sich so ähnlich wie private variables in Java.

static GLFWwindow* window;
static int windowWidth, windowHeight;
static bool isFullscreen = false;

Error Callback

Im Fehlerfall wollen wir eine Meldung ausgeben.

static void onError(int error, const char* description)
{
    std::cout << "Error: " << description << "\n";
}

Tastatureingaben

Ein weiteres Callback benötigen wir für die Behandlung von Tastatureingaben. Hier reagieren wir auf die Tasten ESC und M.

Escape schließt das Fenster.

M wechselt zwischen Vollbildmodus und Fenstermodus hin und her.

static void 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 (isFullscreen)
        {
            GLFWmonitor * monitor = glfwGetPrimaryMonitor();
            const GLFWvidmode* mode = glfwGetVideoMode(monitor);
            glfwSetWindowMonitor(window, NULL, (mode->width - windowWidth) / 2, (mode->height - windowHeight) / 2, windowWidth, windowHeight, GLFW_DONT_CARE);
        }
        else
        {
            glfwGetWindowSize(window, &windowWidth, &windowHeight);
            GLFWmonitor * monitor = glfwGetPrimaryMonitor();
            const GLFWvidmode* mode = glfwGetVideoMode(monitor);
            glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
        }
        isFullscreen = !isFullscreen;
    }
}

Weitere Infos was hier geschieht finden Sie im GLFW Window Guide.

Callback bei Änderung der Framebuffer Größe

Wie oben schon erwähnt, müssen wir unserer Grafikeinheit mitteilen wenn sich die Fenstergröße ändert. Dazu dient diese Callback Funktion.

static void onFramebufferSizeChanged(GLFWwindow* window, int width, int height)
{
    graphicsSetWindowSize(width, height);
}

Framerate (FPS) anzeigen

Die Ausgabe der Framerate klauen wir uns direkt aus dem Kapitel Framerate (FPS) anzeigen des Bachelor Kurses.

static void printFps()
{
    static double previousTime = 0;
    static int frameCount = 0;
    
    double currentTime = glfwGetTime();
    if (currentTime - previousTime >= 1.0)
    {
        std::cout << "FPS: " << frameCount << "\n";

        frameCount = 0;
        previousTime = currentTime;
    }
    frameCount++;
}

Die öffentliche Schnittstelle

Genau wie beim Design von Klassen, wollen wir auch nur bestimmte Funktionen unserer Fenstereinheit nach außen sichtbar machen. Bei allen oben defnierten Funktionen haben wir daher das Schlüsselwort static voran gestellt. Diese Funktionen sind nur innerhalb der Fenstereinheit nutzbar.

Nun definieren wir 3 öffentliche Funktionen die später von unserer Main aufgerufen werden sollen.

Window Creation

windowCreate wird einmalig aufgerufen wenn die Anwendung startet. Hier bauen wir die Einzelteile zusammen. Wir erzeugen ein Fenster, übergeben die nötigen Callbacks und geben entsprechende Fehlermeldungen aus, wenn etwas nicht klappt.

bool windowCreate(Settings props)
{
    windowWidth = props.width;
    windowHeight = props.height;
    isFullscreen = props.fullscreen;

    glfwSetErrorCallback(onError);
    
    if (!glfwInit())
    {
        std::cout << "Error initilizing graphics.";
        return false;
    }

    if (props.msaa) glfwWindowHint(GLFW_SAMPLES, 4);

    if (props.fullscreen)
    {
        GLFWmonitor * monitor = glfwGetPrimaryMonitor();
        const GLFWvidmode* mode = glfwGetVideoMode(monitor);
        window = glfwCreateWindow(mode->width, mode->height, "CGM", monitor, NULL);
    }
    else
    {
        window = glfwCreateWindow(props.width, props.height, "CGM", NULL, NULL);
    }
    if (!window)
    {
        glfwTerminate();
        std::cout << "Error opening window.";
        return false;
    }

    glfwMakeContextCurrent(window);
    glfwSetKeyCallback(window, onKeyboardInput);
    glfwSetFramebufferSizeCallback(window, onFramebufferSizeChanged);
    int w, h;
    glfwGetFramebufferSize(window, &w, &h);
    graphicsSetWindowSize(w, h);
    return true;
}

Window Loop

windowLoop wird mit jedem gezeichneten Bild aufgerufen.

Wie ganz am Anfang erklärt, müssen wir die Betriebssystem die Möglichkeit geben, Events zu verarbeiten. Das tun wir in der Funktion windowLoop. Ebenfalls wird hier geprüft ob das Fenster geschlossen werden soll. Lesen sie sich ebenfalls ein in das Thema von Swap Chains damit Sie verstehen was glfwSwapBuffers tut.

bool windowLoop()
{
    if (glfwWindowShouldClose(window)) return false;

    glfwSwapBuffers(window);
    glfwPollEvents();
    printFps();

    return true;
}

Windows Destruction

windowDestroy wird einmalig aufgerufen bevor sich unsere Anwendung beendet. Sie gibt uns die Möglichkeit aufzuräumen.

void windowDestroy()
{
    glfwDestroyWindow(window);
    glfwTerminate();
}