Wir zeichnen ein erstes 3D Objekt, bestimmen Vertex Koordinaten und Farben.

Inhalt

Ein Quadrat zeichnen

Erstellen Sie eine neue Translation Unit scene. Hier wollen wir künftig alles kapseln was mit dem Inhalt unserer 3D Szene zu tun hat. Denken Sie dabei an die folgenden Schritte:

  • Erstellen Sie eine neue Header Datei scene.h
  • Erstellen Sie eine neue Quellcode Datei scene.c
  • Fügen Sie einen neuen Eintrag in Ihr makefile ein und ergänzen Sie die Liste der Abhängigkeiten für die Hauptregel.
  • Fügen Sie ein #include statement in der .c Datei ein, in der Sie auf die Scene zugreifen.

Die Szene

Füllen Sie die neue scene.h und scene.c mit Inhalt…

scene.h

#include <GLFW/glfw3.h>

void renderScene(GLFWwindow* window);

scene.c

#include "scene.h"

void renderScene(GLFWwindow* window)
{
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    float ratio = width / (float)height;
    glViewport(0, 0, width, height);
    
    glClearColor(0.29f,0.36f,0.4f,1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-ratio, ratio, -1.0f, 1.0f, 1.0f, -1.0f);

    glBegin(GL_QUADS);
    glColor3f(0.5f, 0.73f, 0.14f);
    glVertex3f(-0.6f, 0.0f, 0.0f);
    glVertex3f(0.0f, -0.6f, 0.0f);
    glVertex3f(0.6f, 0.0f, 0.0f);
    glVertex3f(0.0f, 0.6f, 0.0f);
    glEnd();
}

Selbststudium

Untersuchen Sie den Code Zeile für Zeile und nutzen Sie eine Suchmaschine Ihrer Wahl um die einzelnen OpenGL Funktionsaufrufe nachzuschlagen. Auch GitHub Copilot oder ChatGPT können dabei nützlich sein. Diesen Code, so oder so ähnlich finden Sie in diversen Online-Tutorials. Stellen Sie sicher, dass Sie alles verstehen.

Anschließend rufen Sie innerhalb Ihres render loop die neu erstellte Funktion auf.

int startGraphics(int width, int height)
{
    glfwSetErrorCallback(errorCallback);
    
    if (!glfwInit())
    {
        printf("Error initilizing graphics.");
        return 1;
    }
    
    window = glfwCreateWindow(width, height, "Engine", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        printf("Error opening window.");
        return 1;
    }

    glfwMakeContextCurrent(window);
    glfwSetKeyCallback(window, keyCallback);

    while (!glfwWindowShouldClose(window))
    {
        renderScene(window);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}

Multisampling aktivieren

Anti-Aliasing ist eine Methode um Alias-Effekte zu verringern. Eine Methode dazu ist das sog. Multisampling. Dazu werden pro Bildschirmpixel mehrere Subpixel berechnet. Die beiden verlinkten Artikel helfen dabei das Konzept zu verstehen.

Dieser Vorgang geschieht in zwei einfachen Schritten. Zuerst müssen Sie der API mitteilen, dass beim Erzeugen des Fensters zusätzlicher Platz für die zusätzlichen Pixel bereitgestellt werden soll.

Binden Sie den folgenden Code in der graphics.c ein, bevor das Fenster erzeugt wird. Anschließend müssen Sie Multisampling als Feature aktivieren.

graphics.c

int startGraphics(int width, int height)
{
    glfwSetErrorCallback(errorCallback);
    
    if (!glfwInit())
    {
        printf("Error initilizing graphics.");
        return 1;
    }
    glfwWindowHint(GLFW_SAMPLES, 4);
    
    window = glfwCreateWindow(width, height, "Engine", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        printf("Error opening window.");
        return 1;
    }

    glfwMakeContextCurrent(window);
    glEnable(GL_MULTISAMPLE);
    glfwSetKeyCallback(window, keyCallback);

    while (!glfwWindowShouldClose(window))
    {
        renderScene(window);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
}

Systemabhängige Besonderheiten

Unter Windows erhalten Sie möglicherweise einen Fehler weil GL_MULTISAMPLE undefiniert sei. Um dies zu beheben müssen Sie zusätzlich GLFW_INCLUDE_GLEXT definieren bevor Sie GLFW einbinden. Auch unter macOS und Linux schadet es nicht, diese Anpassung vorzunehmen, Ihr Code bleibt damit cross-plattform-fähig.

Passen Sie den Anfang Ihrer graphics.h wie folgt an:

graphics.h

#define GLFW_INCLUDE_GLEXT
#include <GLFW/glfw3.h>

static void errorCallback(int error, const char* description);
static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);

int startGraphics(int, int);

Unterschiede in der Implementierung

Je nachdem was Ihre Hardware kann und welche Treiber Sie installiert haben, kann das Ergebnis variieren. Testen Sie ob Sie auf Ihrem System einen Unterschied in der Qualität sehen können. Falls Sie keinen Unterschied sehen, ist es möglich, dass Ihre OpenGL Implementierung dieses Feature nicht von Hause aus mitbringen.

Für diese Veranstaltung ist es nicht erforderlich, dass AntiAliasing korrekt funktioniert. Falls Sie dennoch – zum Beispiel für eigene Projekte – auf die Suche nach einer Lösung gehen wollen, dann empfehle ich Ihnen das Kapitel über GLAD im Masterkurs Computergrafik. Kurz gesagt geht es dabei um eine Bibliothek die dabei hilft verfügbare Features der installierten Grafikkarte auszulesen und verfügbar zu machen, auch wenn das Betriebssystem von diesen scheinbar nichts weiß.


Framerate (FPS) anzeigen

Damit wir auf der Konsole ein Lebenszeichen unserer Engine bekommen, wollen wir dort die aktuelle Bildwiederholrate ausgeben.

Wir definieren dazu in unserer graphics.c Datei eine neue Funktion printFps.

graphics.h

#define GLFW_INCLUDE_GLEXT
#include <GLFW/glfw3.h>

static void errorCallback(int error, const char* description);
static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
static void printFps();

int startGraphics(int, int);

graphics.c

static void printFps()
{
    static double previousTime = 0;
    static int frameCount = 0;

    double currentTime = glfwGetTime();
    if (currentTime - previousTime >= 1.0)
    {
        printf("FPS: %i\n", frameCount);

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

Wie Sie sehen ist diese Funktion als static definiert. Außerdem nutzen zwei Variablen innerhalb der Funktion ebenfalls das Schlüsselwort static. Wissen Sie schon, was das genau bedeutet?

Anders als in vielen anderen Programmiersprachen bedeutet das Schlüsselwort static in C etwas ähnliches wie in Java oder C# private bedeutet:

  • Funktionen die als static markiert sind, sind nur innerhalb der selben Quellcode Datei zugreifbar. Die Funktion printFps kann also nur innerhalb der graphics.c aufgerufen werden.
  • Variablen auf der obersten Ebene, die mit static gekennzeichnet sind, sind ebenfalls in ihrem Zugriff auf die aktuelle Quellcodedatei beschränkt.
  • Variablen innerhalb einer Funktion die als static markiert sind, sind nur innerhalb der Funktion definiert und erhalten zusätzlich ihren Wert zwischen zwei Funktionsaufrufen. Wenn hier also frameCount hochgezählt wird, dann behält die Variable Ihren Wert bis zum nächsten Aufruf.

Rufen Sie printFps() in Ihrem Renderloop auf und Sie sollten eine Ausgabe auf der Konsole erhalten.

int startGraphics(int width, int height)
{
    ...

    while (!glfwWindowShouldClose(window))
    {
        renderScene(window);
        glfwSwapBuffers(window);
        glfwPollEvents();
        printFps();
    }

    ...
}

Denken Sie daran, die Signatur der Funktion, ebenfalls mit dem static Keyword in Ihre Header Datei einzufügen damit Sie von den übrigen Funktionen innerhalb der graphics.c gefunden werden kann.


Vertical Sync aktivieren

Zuletzt wollen wir noch etwas Energie sparen indem wir der Grafikkarte mitteilen, dass wir unseren Rendervorgang mit der Bildwiederholrate des Monitors synchronisieren wollen. Fügen Sie dazu die folgende Zeile in der graphics.c ein, nachdem das Fenster erzeugt wurde.

Wichtig: Dies ist lediglich eine Anweisung die dem Grafikkartentreiber unsere Vorliebe mitteilt. Der Benutzer kann in anderen Tools, beispielsweise in der NVIDIA Systemsteuerung dieses Verhalten überschreiben.

graphics.c

int startGraphics(int width, int height)
{
    ...

    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);
    glEnable(GL_MULTISAMPLE);
    glfwSetKeyCallback(window, keyCallback);

    ...
}

Prüfungsvorbereitung

  • Können Sie erklären was glfwGetFramebufferSize macht? Was sind die Eingabewerte dieser Funktion, was ist die Ausgabe und was bedeuten die zurückgegebenen Werte?
  • Wenn Sie eine Funktion aufrufen, die einen Parameter vom Typ (int*) erwartet und sie eine lokale Variable vom Typ int übergeben wollen, können Sie erklären was genau das &-Zeichen vor dem Variablen Namen bedeutet?
  • Was macht glClear allgemein und was macht glClear in der Form wie wir es aufrufen?
  • Können Sie erklären was glMatrixMode tut, wleche Modes es gibt und was genau unser Code hier tut?
  • Ist es notwending glLoadIdentity vor glOrtho aufzurufen? Falls ja, warum? Falls nein, warum nicht?
  • An mehreren Stellen verwenden wir Farbwerte. Können Sie erklären wie diese Werte entstehen und wie Sie sie z.b. in einem Bildverarbeitungsprogramm eingeben können?
  • Was ist Aliasing und was kann man dagegen tun?
  • Können Sie erklären was die Bildwiederholrate ist? Wieviele Bilder pro Sekunde können Menschen wahrnehmen und wieviele Millisekunden hat Ihr Code auf Ihrem Computer mit Ihren FPS aktuell Zeit um ein Bild zu zeichnen?
  • Beschreiben Sie welche 3 Funktionen das static Keyword in C hat.