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. Speichern Sie die Datei unverändert in Ihren Projektordner (Computergrafik/cgm_05/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 <glad/glad.h> #include <GLFW/glfw3.h> #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 "stb_image.h" 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 { float x; float y; Vector2(float x, float 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::Mesh(const std::vector<Vertex> &vertices) : vertices(vertices) { ... 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> class Shader { public: ... void setTexture(const std::string &textureName, Texture* texture); private: std::array<std::string, 16> textureUnits = {}; };

shader.cpp

void Shader::setTexture(const std::string &uniformName, Texture* texture) { 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" class Renderer { public: ... private: ... Texture *texture = nullptr; };

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 = new Texture("textures/thm2k.png"); } Renderer::~Renderer() { delete texture; ... } 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.

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.
  • Schreiben Sie eine Frage zu diesem Kapitel auf und reichen Sie diese zu Beginn der nächsten Veranstaltung ein.