Einfache Figuren zeichnen

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

Screenshot 2024-10-22 at 200330.png

Ein Quadrat zeichnen

Erstellen Sie eine neue Klasse 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 Implementierungsdatei: scene.cpp
  • Fügen Sie eine #include-Anweisung in der renderer.h-Datei ein, damit die Scene dort verwendet werden kann.

Die Szene

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

scene.h

#pragma once

#define GLFW_INCLUDE_GLEXT

#include <GLFW/glfw3.h>

class Scene
{
  public:
    void render(GLFWwindow *window) const;
};

scene.cpp

#include "scene.h"

void Scene::render(GLFWwindow *window) const
{
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    double ratio = width / static_cast<double>(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.0, 1.0, 1.0, -1.0);

    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 erstellen Sie eine Szene und rendern diese in der While-Loop.

renderer.cpp

void Renderer::start()
{
    Scene foreground;

    while (!glfwWindowShouldClose(window))
    {
        foreground.render(window);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
}

Multisampling aktivieren

Anti-Aliasing ist eine Methode, um Alias-Effekte zu verringern. Ein Verfahren 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 genügend Speicherplatz für die zusätzlichen Pixel bereitgestellt werden soll.

Binden Sie den folgenden Code im Renderer-Konstruktor ein, bevor das Fenster erzeugt wird. Anschließend müssen Sie Multisampling als Feature aktivieren.

renderer.cpp

Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
    ...

    glfwWindowHint(GLFW_SAMPLES, 4);

    window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
    
    ...

    glEnable(GL_MULTISAMPLE);
}

Unterschiede in der Implementierung

Ob und wie stark Multisampling sichtbar wird, hängt von einer ganzen Kette von Faktoren ab: was die Anwendung anfordert, was Hardware und Treiber unterstützen, wie das Betriebssystem die Anforderung umsetzt, und welche Override-Einstellungen Nutzerinnen und Nutzer im Treiber-Panel oder in den Systemeinstellungen gesetzt haben. Dazu kommen die üblichen Verdächtigen: Batteriesparmodus, Thermal Throttling, automatischer Wechsel zwischen integrierter und dedizierter GPU oder anwendungsspezifische Profile im Grafiktreiber. Testen Sie, ob Sie auf Ihrem System einen Unterschied in der Qualität sehen. Bleibt das Bild gleich, greift Ihre OpenGL-Implementierung dieses Feature vermutlich nicht direkt ab oder es wird an anderer Stelle übersteuert.

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 Renderer-Klasse eine neue Funktion printFps und die nötigen privaten Membervariablen.

renderer.h

class Renderer
{
  public:
    ...
  private:
    ...
    double previousTime = 0.0;
    uint32_t frameCount = 0;
    void printFps();
};

renderer.cpp

void Renderer::printFps()
{
    double currentTime = glfwGetTime();
    if (currentTime - previousTime >= 1.0)
    {
        std::cout << "FPS: " << frameCount << std::endl;

        frameCount = 0;
        previousTime = currentTime;
    }

    frameCount++;
}

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

renderer.cpp

void Renderer::start()
{
    Scene foreground;

    while (!glfwWindowShouldClose(window))
    {
        foreground.render(window);
        glfwSwapBuffers(window);
        glfwPollEvents();
        printFps();
    }
}

Vertical Sync aktivieren

Das Rendering der Bilder ist in den meisten Spielen die aufwändigste Aufgabe pro Frame und dominiert sowohl Energieverbrauch als auch Wärmeentwicklung der Grafikkarte. Werden dabei mehr Bilder pro Sekunde erzeugt, als der Monitor anzeigen kann, bleibt ein Teil davon schlicht unsichtbar; schlimmer noch, ohne Synchronisation mit dem Bildaufbau des Monitors kann der Frame-Inhalt mitten im Auslesen wechseln, sodass oberes und unteres Bilddrittel aus unterschiedlichen Frames stammen — sichtbar als horizontaler Riss, das sogenannte Tearing.

Gut entworfene Engines entkoppeln aus genau diesem Grund die Render-Frequenz von der Simulations-Frequenz: Die Physik läuft mit hoher, fester Rate für geringe Eingabelatenz und stabile Numerik, während die Bildausgabe mit der Bildwiederholrate des Monitors synchronisiert wird. Wir aktivieren diese Synchronisation, indem wir die folgende Zeile im Renderer-Konstruktor einfügen, nachdem das Fenster erzeugt wurde.

renderer.cpp

Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
    ...
    glfwSwapInterval(1);
}

Eine interessante Besonderheit: glfwSwapInterval teilt dem Grafikkartentreiber lediglich unsere Präferenz als Entwickler mit — eine verbindliche Anweisung ist es nicht. Das Betriebssystem kann diese Präferenz überschreiben, etwa im Akkubetrieb zugunsten der Laufzeit, und auch der Benutzer kann sie übersteuern, beispielsweise über die Qualitätseinstellungen in der NVIDIA Systemsteuerung. Was zur Laufzeit tatsächlich passiert, ergibt sich also erst aus dem Zusammenspiel dieser drei Ebenen.


Lernziele

Nach diesem Kapitel sollten Sie die folgenden Fragen beantworten können:

  • Was ist der Unterschied zwischen Immediate Mode und Retained Mode in OpenGL?
  • Was beschreibt ein Vertex und welche Attribute kann er neben der Position haben?
  • Was ist eine orthografische Projektion und wie unterscheidet sie sich von einer perspektivischen Projektion?
  • Warum muss das Seitenverhältnis (Aspect Ratio) des Fensters bei der Projektion berücksichtigt werden?
  • Was ist Aliasing und wie reduziert Multisampling diesen Effekt?
  • Was ist Vertical Sync (VSync) und welchen Einfluss hat es auf Framerate und Energieverbrauch?
  • Was ist eine Game Loop und warum ist die Reihenfolge von Render-, Swap- und Poll-Aufrufen wichtig?

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.