In diesem Kapitel erweitern wir die Engine um die Möglichkeit Texturen zu laden und anzuzeigen.

Neue Klasse Texture

In diesem Abschnitt werden wir eine Klasse erstellen aus der wir später pro geladener Textur eine Instanz erzeugen können.

Image Loader

Das Laden von Bilddateien erfordert individuelle Implementierungen je nach verwendetem Bildformat. Der Einfachheit halber verwenden wir den weit verbreiteten Image Loader von Sean Barrett.

Hierfür benötigen wir die Datei stb_image.h. Falls Sie die Datei noch nicht haben, speichern Sie die Datei unverändert in Ihren libraries-Ordner (Computergrafik/libraries/stb/stb_image.h)

Texturdatei

Damit der ImageLoader auch etwas zu tun hat, benötigen wir eine Texturdatei. Laden Sie sich die Bilddatei thm2k.png herunter und speichern Sie sie unter Computergrafik/cgm_05/textures/thm2k.png

Die eigentliche Klasse...

texture.h

#pragma once #include <cstdint> #include <string> class Texture { public: Texture(const std::string &filename); ~Texture(); uint32_t getTextureID() const; private: uint32_t textureID = 0; };

...und ihre Implementierung

Zu beachten ist hier, dass wir den oben heruntergeladenen ImageLoader einbinden. Das #define Statement ist dafür notwendig, siehe Doku im Quellcode der Datei.

Ansonsten haben wir es hier lediglich mit einem Konstruktor, Destruktor und einer Funktion zu tun, die dafür sorgen, dass eine Textur geladen oder entladen wird. Die Membervariable textureID beinhaltet die von OpenGL zurückgegebene Textur-ID damit man im weiteren Verlauf auf die Textur zugreifen kann. Es ist daher nötig, dass diese ID über eine Funktion verfügbar ist

Beachten Sie außerdem, dass wir zum Laden der Textur die Texture-Unit GL_TEXTURE15 verwenden. Jede OpenGL Implementierung muss mindestens 16 Texturen unterstützen. Es ist daher garantiert, dass diese Texture-Unit verfügbar ist. Solange wir in unserer restlichen Applikation diese Texture-Unit nicht verwenden, können wir hier Texturen im Hintergrund laden, ohne den aktiven Rendervorgang zu stören.

texture.cpp

#define STB_IMAGE_IMPLEMENTATION #include "texture.h" #include <glad/glad.h> #include <stb_image.h> #include <stdexcept> Texture::Texture(const std::string &filename) { glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE15); glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); int width = 0; int height = 0; int channels = 0; stbi_set_flip_vertically_on_load(true); unsigned char *data = stbi_load(filename.c_str(), &width, &height, &channels, 0); if (!data) { stbi_image_free(data); glBindTexture(GL_TEXTURE_2D, 0); throw std::runtime_error("Failed to load image " + filename); } if (channels == 1) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, data); } else if (channels == 2) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, width, height, 0, GL_RG, GL_UNSIGNED_BYTE, data); } else if (channels == 3) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); } else if (channels == 4) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); } glGenerateMipmap(GL_TEXTURE_2D); stbi_image_free(data); glBindTexture(GL_TEXTURE_2D, 0); } Texture::~Texture() { glDeleteTextures(1, &textureID); } uint32_t Texture::getTextureID() const { return textureID; }

Die verschiedenen Methoden und Parameter werden sehr schön in diesem Kapitel von LearnOpenGL erklärt.

Vertex um Texturkoordinaten erweitern

Damit geladene Texturen auch an der richtigen Position auf der Oberfläche von Modellen erscheinen, müssen wir unserem Shader Texturkoordinaten übergeben. Dazu sind ein paar Schritte nötig.

Neue Struktur Vector2

Zuerst benötigen wir eine neue Struktur um die Texturkoordinaten in unserem Code verwalten zu können. Wir erzeugen hierfür analog zu unserem Vector3 einen neuen Vector2.

cgmath.h

struct Vector2 { double x; double y; Vector2(double x, double y) : x(x), y(y) { } };

Anschließend erweitern wir unseren Vertex um diesen neuen Typ und fügen zusätzlich zur Position eine Membervariable für die Texturkoordinate hinzu.

vertex.h

#pragma once #include "cgmath.h" struct Vertex { Vertex(const Vector3 &position, const Vector2 &texCoord); ~Vertex(); float position[3] = {}; float texCoord[2] = {}; };

vertex.cpp

Vertex::Vertex(const Vector3 &position, const Vector2 &texCoord) : position{static_cast<float>(position.x), static_cast<float>(position.y), static_cast<float>(position.z)}, texCoord{static_cast<float>(texCoord.x), static_cast<float>(texCoord.y)} { }

Und zuletzt muss auch die Vertex Spezifikation in der Mesh-Klasse aktualisiert werden, damit diese neue Variable korrekt übermittelt wird.

mesh.cpp

#include "mesh.h" #include <cstddef> #include <glad/glad.h> ... Mesh::Mesh(const std::vector<Vertex> &vertices) : vertices(vertices) { ... glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, position)); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, texCoord)); glEnableVertexAttribArray(1); }

Integration von Texturen in den Shader

Nun haben wir die Möglichkeit Texturen zu laden und deren Koordinaten an den Shader zu übermitteln. Jetzt widmen wir uns den Shadern selbst, damit diese mit diesen neuen Inputs auch etwas anfangen können.

Zuerst erweitern wir die Klasse Shader so, dass wir dem kompilierten Shaderprogramm eine Textur übergeben können.

shader.h

... #include "texture.h" #include <array> ... #include <optional> ... class Shader { public: ... void setTexture(const std::string &textureName, const std::optional<Texture> &texture); private: std::array<std::string, 16> textureUnits = {}; };

shader.cpp

void Shader::setTexture(const std::string &uniformName, const std::optional<Texture> &texture) { if (!texture.has_value()) return; GLint uniformLocation = glGetUniformLocation(shaderProgramID, uniformName.c_str()); if (uniformLocation >= 0) { for (int i = 0; i < 16; i++) { if (textureUnits[i].empty()) { textureUnits[i] = uniformName; glUniform1i(uniformLocation, i); } if (textureUnits[i] == uniformName) { glActiveTexture(GL_TEXTURE0 + i); glBindTexture(GL_TEXTURE_2D, texture->getTextureID()); break; } } } }

Dann erweitern wir den Vertexshader um die zugehörige Logik, Texturkoordinaten entgegenzunehmen und an den Fragmentshader weiterzuleiten.

vertex_shader.glsl

#version 330 core layout (location = 0) in vec3 VertPos; layout (location = 1) in vec2 TexCoordIn; out vec2 TexCoord; uniform mat4 WorldMatrix; uniform mat4 ViewMatrix; uniform mat4 ProjectionMatrix; void main() { mat4 WvpMatrix = ProjectionMatrix * ViewMatrix * WorldMatrix; TexCoord = TexCoordIn; gl_Position = WvpMatrix * vec4(VertPos, 1.0); }

Und im Fragmentshader implementieren wir schließlich die Logik, die die Textur ausliest und den gelesenen Farbwert zurück gibt.

fragment_shader.glsl

#version 330 core out vec4 FragColor; in vec2 TexCoord; uniform sampler2D thmTexture; void main() { FragColor = texture(thmTexture, TexCoord); }

Renderer anpassen

Im letzten Abschnitt dieses Kapitels geht es nun darum diese Änderungen zum Leben zu erwecken. Dafür muss der Renderer wie folgt angepasst werden.

renderer.h

... #include "texture.h" ... #include <optional> class Renderer { public: ... private: ... std::optional<Texture> texture = std::nullopt; };

renderer.cpp

Renderer::Renderer(const Settings &settings, Window &window) { ... std::vector<Vertex> vertices = { Vertex({-0.6, 0.0, 0.0}, {0.0, 0.5}), Vertex({0.0, -0.6, 0.0}, {0.5, 0.0}), Vertex({0.6, 0.0, 0.0}, {1.0, 0.5}), Vertex({-0.6, 0.0, 0.0}, {0.0, 0.5}), Vertex({0.6, 0.0, 0.0}, {1.0, 0.5}), Vertex({0.0, 0.6, 0.0}, {0.5, 1.0})}; ... texture.emplace("textures/thm2k.png"); } Renderer::~Renderer() { texture.reset(); ... } void Renderer::loop() { ... shader->setTexture("thmTexture", texture); mesh->draw(); }

Die einzelnen Abschnitte sollten nach der umfassenden Vorbereitung fast selbsterklärend sein. Wir binden die texture.h ein, definieren eine Variable in der wir die Textur aufbewahren können, laden die Textur im Konstruktor und sorgen schließlich dafür, dass sie vor jedem Renderaufruf im Shaderprogramm aktiviert wird. Diese Aktivierung pro Frame ist zwar im aktuellen Beispiel noch nicht zwingend nötig, hilft aber zu verstehen wir wir mehrere Modelle rendern könnten.

Wichtig ist ebenfalls die Vertexkoordinaten um sinnvolle Werte zu ergänzen, damit die Textur richtig aussieht und natürlich auch das Zerstören der Textur im Destruktor.

Lernziele dieses Kapitels

In diesem Kapitel geht es um Texturen

  • Sie verstehen den grundlegenden Aufbau gängiger Bildformate (z.B. PNG, JPEG oder BMP) und wissen welche Pixel am Anfang einer Datei gespeichert sind und welche Pixel am Ende der Datei gespeichert sind.
  • Sie wissen was stbi_set_flip_vertically_on_load macht und können den damit verbundenen Performance Overhead einschätzen. Evtl. können sie sogar eine günstigere Alternative vorschlagen.
  • Sie wissen welche Texturfilter (GL_TEXTURE_WRAP_?, GL_TEXTURE_?_FILTER) es gibt und können für ein gegebenes Anwendungsbeispiel den richtigen Filter empfehlen. (exakte Syntax ist dabei nicht wichtig)
  • Sie können erklären was Mipmaps sind und den optischen Effekt beschreiben wenn man zu große Texturen ohne Mipmaps verwendet.
  • Sie können Texturkoordiaten erklären, kennen deren Koordinatensystem und wissen wie die Zuordnung zwischen einem Vertex und einem Punkt in der Textur funktioniert.
  • Sie können erklären welchen Weg Texturkoordinaten von der Mesh-Definition bis in den Fragment Shader nehmen und was auf dem Weg mit ihnen passiert.
  • Sie kennen die Einschränkungen von OpenGL bzgl. der Anzahl der Texture Units und können erklären was diese Einschränkung in einem echten Spiel bedeutet.

Hausaufgabe

  • Bereiten Sie dieses Kapitel eigenständig bis zum nächsten Termin vor.
  • Ziehen Sie weitere Quellen zu Rate um wirklich zu verstehen was hier passiert und wozu das nötig ist.
  • Bereiten Sie Themen für das Plenum vor. Das können offene Fragen sein oder auch Themen die sie bereits verstanden haben und gerne nochmal vertiefen möchten.