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

Texturen laden

In diesem Abschnitt werden wir Texturen in den Speicher laden damit wir sie anschließend beim Rendern unseres Modells verwenden 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.

Wenn sie ihren Projektordner mit Hilfe des Starter Sets aus Kapitel 1 erzeugt haben, befindet sich die benötigte Headerdatei bereits in ihrem Projektordner unter libraries/stb/stb_image.h

Neue Klasse Texture

Beginnen wir mit einem Blick in die Header Datei.

texture.h

#pragma once #define GLFW_INCLUDE_GLEXT #include <GLFW/glfw3.h> #include <string> class Texture { public: Texture(const std::string &filename); ~Texture(); GLuint id; private: std::string filename; };

texture.cpp

#define STB_IMAGE_IMPLEMENTATION #include "texture.h" #include "stb_image.h" Texture::Texture(const std::string &filename) : filename(filename) { glGenTextures(1, &id); glBindTexture(GL_TEXTURE_2D, id); 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); glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); int width, height, channels; stbi_set_flip_vertically_on_load(1); unsigned char *data = stbi_load(filename.c_str(), &width, &height, &channels, 0); if (!data) { stbi_image_free(data); throw std::runtime_error("Failed to load texture " + filename); } glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); stbi_image_free(data); } Texture::~Texture() { }

Texturdateien einbinden

Erstellen Sie einen neuen Ordner “res” in Ihrem Projektordner. Hier speichern wir zukünftig Bilder und ähnliche Ressourcen ab. Für dieses Kapitel benötigen wir 2 Texturen.

Textur der Erde

Als Textur für die Kugel nutzen wir diese Erdtextur von der NASA. Speichern Sie die Textur unter Computergrafik/textures/earth_diffuse.jpg

Textur für einen THM-Würfel

Für die beiden Würfel nutzen wir eine THM Textur. Speichern Sie die Datei unter Computergrafik/textures/thm2k.png

Vertex um Texturkoordinaten erweitern

Texturkoordinaten dienen dazu, unsere Texturen auf den Objekten richtig auszurichten. Diese Koordinaten geben für jeden Vertex an, welcher Pixel der Textur an dieser Stelle gezeichnet werden soll. Zwischen den einzelnen Vertices werden die Werte von der Grafikkarte interpoliert, sodass an jeder Stelle des Objektes ein Teil der Textur sichtbar ist. Die Texturkoordinaten werden in Form von zweidimensionalen Vektoren angegeben, die normalerweise Werte zwischen 0 und 1 haben. Ein Wert von 0 bedeutet, dass der Punkt auf der linken oder unteren Seite der Textur liegt, während ein Wert von 1 auf der rechten oder oberen Seite liegt. Zuerst erweitern wir unsere Mathe Bibliothek um einen neuen Datentyp Vector2 und fügen anschließend unserem Vertex Datentyp eine neue Variable texcoord hinzu. Hier werden die Texturkoordinaten eines jeden Vertex gespeichert.

cgmath.h

struct Vector2 { double x; double y; }; ... struct Vertex { Vertex(const Vector3 &position, const Vector3 &norm, const Vector2 &texcoord) : position(static_cast<float>(position.x), static_cast<float>(position.y), static_cast<float>(position.z)), normal(static_cast<float>(norm.x), static_cast<float>(norm.y), static_cast<float>(norm.z)), texcoord(static_cast<float>(texcoord.x), static_cast<float>(texcoord.y)) { } float position[3]; float normal[3]; float texcoord[2]; };

Mesh Klasse anpassen

Unsere Texturen müssen irgendwo aufbewahrt werden. Dazu nutzen wir die Mesh Klassen. Eine Besonderheit dabei ist, dass mehrere Meshes die gleiche Textur verwenden sollen. Um das möglichst einfach möglich zu machen, übergeben wie die Textur an das Mesh als Shared Pointer. So ist sichergestellt, dass die Ressourcen der Texture freigegeben werden sobald das letzte Mesh zerstört wird, welches eine Textur verwendet.

Außerdem wollen wir die Materialeigenschaften eines Meshes nicht mehr für jeden Vertex individuell setzen. Sobald die Details aus der Textur kommen ist dies nicht mehr notwendig und wird nun einmalig pro Mesh definiert.

mesh.h

#pragma once #include "cgmath.h" #include "texture.h" #include <memory> #include <vector> class Mesh { public: Mesh(std::shared_ptr<Texture> &texture); virtual ~Mesh() = default; virtual void render() const; void setPosition(const Vector3 &position); void setRotation(const Vector3 &rotation); void setMaterial(const Color &diffuse, const Color &specular, const Color &emission, const Color &ambient, const float shininess); protected: Matrix4 position = Matrix4::translate(0, 0,0); Matrix4 rotation = Matrix4::rotateX(0.0); std::vector<Vertex> vertices = {}; std::shared_ptr<Texture> texture = nullptr; float diffuse[3] = {1.0f, 1.0f, 1.0f}; float specular[3] = {1.0f, 1.0f, 1.0f}; float emission[3] = {0.0f, 0.0f, 0.0f}; float ambient[3] = {1.0f, 1.0f, 1.0f}; float shininess = 30.0f; };

mesh.cpp

#include "mesh.h" #include "texture.h" Mesh::Mesh(std::shared_ptr<Texture> &texture) : texture(texture) { } void Mesh::render() const { Matrix4 worldMatrix = position * rotation; if (texture) { glBindTexture(GL_TEXTURE_2D, texture->id); } glPushMatrix(); float worldMatrixF[16]; worldMatrix.toColumnMajor(worldMatrixF); glMultMatrixf(worldMatrixF); glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, (float *)&ambient); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, (float *)&diffuse); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (float *)&specular); glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, (float *)&emission); glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, shininess); glBegin(GL_QUADS); for (auto vertex : vertices) { glNormal3fv((float *)&vertex.normal); glTexCoord2fv((float *)&vertex.texcoord); glVertex3fv((float *)&vertex.position); } glEnd(); glPopMatrix(); if (texture) glBindTexture(GL_TEXTURE_2D, 0); } void Mesh::setPosition(const Vector3 &position) { this->position = Matrix4::translate(position.x, position.y, position.z); } void Mesh::setRotation(const Vector3 &rotation) { this->rotation = Matrix4::rotateX(rotation.x) * Matrix4::rotateY(rotation.y) * Matrix4::rotateZ(rotation.z); } void Mesh::setMaterial(const Color &diffuse, const Color &specular, const Color &emission, const Color &ambient, const float shininess) { std::copy(&diffuse.r, &diffuse.r + 3, this->diffuse); std::copy(&specular.r, &specular.r + 3, this->specular); std::copy(&emission.r, &emission.r + 3, this->emission); std::copy(&ambient.r, &ambient.r + 3, this->ambient); this->shininess = shininess; }

Cube und Sphere anpassen

Anschließend müssen die beiden Stellen an denen wir die Würfel und Kugel erzeugen so angepasst werden, dass dort zusätzlich zu Vertexposition und Normalenvektor auch die Texturkoordinate gesetzt wird.

cube.h

class Cube : public Mesh { public: Cube(std::shared_ptr<Texture> &texture); };

cube.cpp

Cube::Cube(std::shared_ptr<Texture> &texture) : Mesh(texture) { ... Vector2 t1(0, 0); Vector2 t2(1, 0); Vector2 t3(1, 1); Vector2 t4(0, 1); vertices.emplace_back(p1, normal, t1); vertices.emplace_back(p2, normal, t2); vertices.emplace_back(p3, normal, t3); vertices.emplace_back(p4, normal, t4); for (int i = 1; i < 6; i++) { ... Vector4 result = rotationMatrix * Vector4(p1, 1.0); vertices.emplace_back(result.xyz(), normalRotated, t1); result = rotationMatrix * Vector4(p2, 1.0); vertices.emplace_back(result.xyz(), normalRotated, t2); result = rotationMatrix * Vector4(p3, 1.0); vertices.emplace_back(result.xyz(), normalRotated, t3); result = rotationMatrix * Vector4(p4, 1.0); vertices.emplace_back(result.xyz(), normalRotated, t4); } }

sphere.h

class Sphere : public Mesh { public: Sphere(std::shared_ptr<Texture> &texture); };

sphere.cpp

Sphere::Sphere(std::shared_ptr<Texture> &texture) : Mesh(texture) { ... int i = 0; for (int y = 0; y < rings; y++) { for (int x = 0; x < segments; x++) { double tw = 1.0 / segments; double th = 1.0 / rings; double ty = rings - y; vertices.emplace_back(vectors[x][y + 1], vectors[x][y + 1], Vector2(x * tw, (ty - 1) * th)); vertices.emplace_back(vectors[x + 1][y + 1], vectors[x + 1][y + 1], Vector2((x + 1) * tw, (ty - 1) * th)); vertices.emplace_back(vectors[x + 1][y], vectors[x + 1][y], Vector2((x + 1) * tw, (ty)*th)); vertices.emplace_back(vectors[x][y], vectors[x][y], Vector2(x * tw, (ty)*th)); } } }

Objekte mit Texturen laden und rendern

Damit Texturen überhaupt von unserer Grafikpipeline unterstützt werden, müssen wir das Feature einschalten. Das tun wir im Konstruktor der Renderer Klasse.

renderer.cpp

Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height) { glEnable(GL_MULTISAMPLE); glfwSwapInterval(1); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glEnable(GL_TEXTURE_2D); ... }

Nun passen wir unsere Scene so an, dass die Modelle mitsamt ihrer Texturen geladen und gerendert werden. Wir beginnen mit dem Laden der Texturen und übergeben diese dann an den Konstruktor von Würfel und Kugel.

Dies passiert in der Funktion Renderer::start(). Schauen Sie sich unbedingt std::make_shared<Texture>(...) an und informieren Sie sich was es damit auf sich hat.

renderer.cpp

void Renderer::start() { auto thmTexture = std::make_shared<Texture>("textures/thm2k.png"); auto earthTexture = std::make_shared<Texture>("textures/earth_diffuse.jpg"); auto cube1 = std::make_shared<Cube>(thmTexture); cube1->setPosition(Vector3(3.0, 0.0, 0.0)); auto cube2 = std::make_shared<Cube>(thmTexture); cube2->setPosition(Vector3(-3.0, 0.0, 0.0)); auto sphere = std::make_shared<Sphere>(earthTexture); Scene foreground; foreground.addMesh(cube1); foreground.addMesh(cube2); foreground.addMesh(sphere); ... }

Mögliche Probleme

Falls Sie die Texturen auf den Würfeln sehen, Ihre Kugel aber weiß bleibt, dann liegt es wahrscheinlich daran, dass Ihre Grafikkarte die 8096x4096 Pixel große Erdtextur nicht unterstützt. In diesem Falle können Sie einfach die Texturgröße in einem Bildbearbeitungsprogramm halbieren. Danach sollten Sie die Erde sehen.

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 sich auf das nächste Plenum vor.