In diesem Kapitel beschäftigen wir uns damit die Vertices von verschiedenen 3D Modellen rechnerisch zu ermitteln.
Mathebibliothek erweitern
Zuerst fügen wir unserem Header ein paar zusätzliche Datentypen hinzu.
cgmath.h
struct Vector3
{
double x;
double y;
double z;
};
struct Vector4
{
double x;
double y;
double z;
double w;
Vector4(double x, double y, double z, double w)
: x(x), y(y), z(z), w(w)
{
}
Vector4(Vector3 v, double w)
: x(v.x), y(v.y), z(v.z), w(w)
{
}
Vector3 xyz()
{
return {x, y, z};
}
};
struct Color
{
double r;
double g;
double b;
double a;
};
struct Vertex
{
Vertex(const Vector3 &position, const Color &color)
: position(static_cast<float>(position.x), static_cast<float>(position.y), static_cast<float>(position.z)),
color(static_cast<float>(color.r), static_cast<float>(color.g), static_cast<float>(color.b))
{
}
float position[3];
float color[3];
};
Vector3 ist eine Struktur mit 3 double-Komponenten (x,y,z) zur Darstellung von Vektoren im Raum.
Vector4 ist eine Struktur mit 4 double-Komponenten (x,y,z,w) zur Darstellung von Vektoren in homogenen Koordinaten im Raum.
Color ist eine Struktur mit 4 double-Komponenten (red,green,blue,alpha) zur Darstellung von Farben.
Vertex ist eine Struktur, bestehend aus einer Position und einer Farbe als float-Komponenten. Hiermit werden wir zukünftig die Eckpunkte unserer 3D Modelle definieren.
Multiplikation Matrix ∙ Vector
cgmath.h
struct Matrix4
{
...
Vector4 operator*(const Vector4 &v) const
{
Vector4 result = {
m11 * v.x + m21 * v.y + m31 * v.z + m41 * v.w,
m12 * v.x + m22 * v.y + m32 * v.z + m42 * v.w,
m13 * v.x + m23 * v.y + m33 * v.z + m43 * v.w,
m14 * v.x + m24 * v.y + m34 + v.z + m44 * v.w
};
return result;
}
...
}
Mit dieser Funktion sind wir in der Lage eine Matrix mit einem Vektor zu multiplizieren. Das Ergebnis ist wiederum ein Vektor. Dies ist super hilfreich, weil wir damit genau das tun können was die GPU intern tut. Wir können auf diese Weise Vektoren rotieren oder verschieben.
Lesen Sie hierzu auch den Artikel auf Wikipedia. Insbesondere werden Sie dort auch Hinweise zu verschiedenen Implementierungen und deren Laufzeiten finden. Wie gesagt: Uns geht es hier in erster Linie um Lesbarkeit des Codes. Wir werden aber auf dieses Thema in einem zukünftigen Kapitel nochmal zu sprechen kommen.
Multiplikation Matrix ∙ Matrix
cgmath.h
struct Matrix4
{
...
Matrix4 operator*(const Matrix4 &b) const
{
Matrix4 result = {
m11 * b.m11 + m21 * b.m12 + m31 * b.m13 + m41 * b.m14,
m11 * b.m21 + m21 * b.m22 + m31 * b.m23 + m41 * b.m24,
m11 * b.m31 + m21 * b.m32 + m31 * b.m33 + m41 * b.m34,
m11 * b.m41 + m21 * b.m42 + m31 * b.m43 + m41 * b.m44,
m12 * b.m11 + m22 * b.m12 + m32 * b.m13 + m42 * b.m14,
m12 * b.m21 + m22 * b.m22 + m32 * b.m23 + m42 * b.m24,
m12 * b.m31 + m22 * b.m32 + m32 * b.m33 + m42 * b.m34,
m12 * b.m41 + m22 * b.m42 + m32 * b.m43 + m42 * b.m44,
m13 * b.m11 + m23 * b.m12 + m33 * b.m13 + m43 * b.m14,
m13 * b.m21 + m23 * b.m22 + m33 * b.m23 + m43 * b.m24,
m13 * b.m31 + m23 * b.m32 + m33 * b.m33 + m43 * b.m34,
m13 * b.m41 + m23 * b.m42 + m33 * b.m43 + m43 * b.m44,
m14 * b.m11 + m24 * b.m12 + m34 * b.m13 + m44 * b.m14,
m14 * b.m21 + m24 * b.m22 + m34 * b.m23 + m44 * b.m24,
m14 * b.m31 + m24 * b.m32 + m34 * b.m33 + m44 * b.m34,
m14 * b.m41 + m24 * b.m42 + m34 * b.m43 + m44 * b.m44
};
return result;
}
...
}
Und diese Funktion ermöglicht uns eine klassische Matrixmultiplikation durchzuführen. Wollen wir zum Beispiel einen Vector rotieren und transformieren dann lassen sich diese beiden Operationen zu einer zusammenfassen indem man zuerst die beiden Matrizen miteinander multipliziert.
Backface Culling
An dieser Stelle möchte ich Sie bitten sich mit dem Thema Face Culling zu beschäftigen. Es handelt sich dabei um eine einfache Technik um in komplexeren Szenen die Anzahl der Renderaufrufe zu reduzieren. Konkret nutzen wir diese Funktion um die Rückseiten aller Oberflächen beim Rendern zu überspringen. Wenn wir beispielsweise einen Würfel zeichnen, dann ist es nicht notwendig die Innenseite des Würfels zu zeichnen, da wir diese nicht sehen können (Es sei denn die Kamera stände mitten drin).
Um Backface Culling zu aktivieren, gehen Sie in Ihren Renderer und fügen dort etwa an der gleichen Stelle wo sie Multisampling aktiveren folgende Anweisung hinzu:
renderer.cpp
Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
...
glEnable(GL_MULTISAMPLE);
glfwSwapInterval(1);
glEnable(GL_CULL_FACE);
...
}
Wenn Sie nun Ihre Anwendung starten, sollte Ihr Rechteck nur noch eine Vorderseite haben. Drehen Sie die Kamera hinter das Rechteck, dann sehen Sie nichts. Diese Einstellung benötigen wir für alle nachfolgenden Kapitel.
Zusätzliche Infos
Wir definieren alle unsere Dreiecke und Rechtecke im Gegenuhrzeigersinn bzw. Counter Clockwise (CCW). Falls bei Ihnen die Vorderseite des Rechtecks unsichtbar sein sollte, dann prüfen Sie, ob bei Ihrem Rechteck tatsächlich CCW als Reihenfolge verwendet wurde. Der obige Artikel sollte dies ausreichen erklären.
Mesh rendern
Als Mesh definieren wir eine neue Klasse. Von dieser Klasse erben dann später zwei weitere Klassen Cube und Sphere.
mesh.h
#pragma once
#include "cgmath.h"
#include <vector>
class Mesh
{
public:
virtual void render() const;
void setPosition(const Vector3 &position);
void setRotation(const Vector3 &rotation);
protected:
Matrix4 position = Matrix4::translate(0, 0,0);
Matrix4 rotation = Matrix4::rotateX(0.0);
std::vector<Vertex> vertices = {};
};
mesh.cpp
#define GLFW_INCLUDE_GLEXT
#include "mesh.h"
#include <GLFW/glfw3.h>
void Mesh::render() const
{
Matrix4 worldMatrix = position * rotation;
glPushMatrix();
float worldMatrixF[16];
worldMatrix.toColumnMajor(worldMatrixF);
glMultMatrixf(worldMatrixF);
glBegin(GL_QUADS);
for (auto vertex : vertices)
{
glColor3fv((float *)&vertex.color);
glVertex3fv((float *)&vertex.position);
}
glEnd();
glPopMatrix();
}
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);
}
Die Klasse Mesh ist die Basisklasse für verschiedene 3D Modelle die wir weiter unten definieren. Jedes Mesh hat zwei Matrizen zur Beschreibung seiner Position und Rotation im Raum. Außerdem werden hier alle Vertices (Eckpunkte) des Meshes aufbewahrt.
Die render() Funktion ermöglicht es das Mesh an die Grafikkarte zu senden und zu zeichnen.
Meshes der Szene hinzufügen
Wir legen einen std::vector in der Szene an, welchen wir über die FunktionaddMesh() befüllen können. Wir verwenden hier einen std::shared_ptr<>. Weitere Infos zu Shared Pointern finden Sie hier.
Für den Aufbau eines Grafikprogramms ist der Shared pointer eine sehr praktische Lösung weil unsere Meshes damit später an mehreren Stellen in unserer Anwendung aufbewahrt werden können und dabei eigenständig freigegeben werden sobald sie nicht mehr benötigt werden.
scene.h
#pragma once
#include "mesh.h"
#include <memory>
#include <vector>
class Scene
{
public:
...
void addMesh(const std::shared_ptr<Mesh> &mesh);
void render();
private:
std::vector<std::shared_ptr<Mesh>> meshes;
};
Durch die veränderte Signatur von render() können wir auch die Includes für GLFW und die Kamera entfernen.
scene.cpp
void Scene::addMesh(const std::shared_ptr<Mesh> &mesh)
{
meshes.push_back(mesh);
}
void Scene::render()
{
for (std::shared_ptr<Mesh> &mesh : meshes)
{
mesh->render();
}
}
Der Aufruf der Funktion muss nun auch im Renderer angepasst werden.
renderer.h
...
class Renderer
{
public:
...
private:
...
void renderScene(Scene &scene) const;
};
renderer.cpp
void Renderer::renderScene(Scene &scene) const
{
glClear(GL_COLOR_BUFFER_BIT);
activeCamera.loadViewMatrix();
scene.render();
}
Würfel erstellen
Die Klasse Cube erbt von der Klasse Mesh. Wir müssen nun die pure virtual Funktion mit der würfelspezifischen Funktion überschreiben. Wir zeigen dies durch das Keyword override.
cube.h
#pragma once
#include "mesh.h"
class Cube : public Mesh
{
public:
Cube(const Color &color);
};
cube.cpp
#include "cube.h"
Cube::Cube(const Color &color)
{
Vector3 p1(-1, -1, 1);
Vector3 p2( 1, -1, 1);
Vector3 p3( 1, 1, 1);
Vector3 p4(-1, 1, 1);
vertices.emplace_back(p1, color);
vertices.emplace_back(p2, color);
vertices.emplace_back(p3, color);
vertices.emplace_back(p4, color);
for (int i = 1; i < 6; i++)
{
Matrix4 rotationMatrix;
if (i <= 3) rotationMatrix = Matrix4::rotateY(deg2rad(90.0 * i));
if (i == 4) rotationMatrix = Matrix4::rotateX(deg2rad(90.0));
if (i == 5) rotationMatrix = Matrix4::rotateX(deg2rad(-90.0));
Vector4 result = rotationMatrix * Vector4(p1, 1.0);
vertices.emplace_back(result.xyz(), color);
result = rotationMatrix * Vector4(p2, 1.0);
vertices.emplace_back(result.xyz(), color);
result = rotationMatrix * Vector4(p3, 1.0);
vertices.emplace_back(result.xyz(), color);
result = rotationMatrix * Vector4(p4, 1.0);
vertices.emplace_back(result.xyz(), color);
}
}
Innerhalb des Konstruktors werden die Vertices des Cubes berechnet. Die Funktionen zum Rendern werden bereits von der Basisklasse Mesh bereitgestellt.
Kugel erstellen
Analog zu unserem Würfel erstellen wir nun eine Kugel.
sphere.h
#pragma once
#include "mesh.h"
class Sphere : public Mesh
{
public:
Sphere(const Color &color);
};
sphere.cpp
#include "sphere.h"
Sphere::Sphere(const Color &color)
{
const int segments = 64;
const int rings = segments / 2;
int vcount = segments * rings * 4;
std::vector<std::vector<Vector3>> vectors;
vectors.resize(segments + 1);
std::vector<Vector3> fillVector;
fillVector.resize(rings + 1);
std::fill(fillVector.begin(), fillVector.end(), Vector3(0.0, 0.0, 0.0));
std::fill(vectors.begin(), vectors.end(), fillVector);
for (int y = 0; y <= rings; y++)
{
float deg = 180.0f / rings * (y - rings * 0.5f);
Matrix4 rotationMatrixX = Matrix4::rotateX(deg2rad(deg));
Vector4 startVector = rotationMatrixX * Vector4(0, 0, 1, 1);
vectors[0][y] = startVector.xyz();
vectors[segments][y] = startVector.xyz();
for (int x = 1; x < segments; x++)
{
float deg2 = 360.0f / (float)segments * (float)x;
Matrix4 rotationMatrixY = Matrix4::rotateY(deg2rad(deg2));
Vector4 result = rotationMatrixY * Vector4(vectors[0][y], 1.0);
vectors[x][y] = result.xyz();
}
}
int i = 0;
for (int y = 0; y < rings; y++)
{
for (int x = 0; x < segments; x++)
{
vertices.emplace_back(vectors[x][y + 1], color);
vertices.emplace_back(vectors[x + 1][y + 1], color);
vertices.emplace_back(vectors[x + 1][y], color);
vertices.emplace_back(vectors[x][y], color);
}
}
}
Szene erzeugen und rendern
Innerhalb unserer Renderer Klasse erzeugen wir nun eine Szene mit diesen Meshes.
renderer.cpp
void Renderer::start()
{
auto cube1 = std::make_shared<Cube>(Color(0.5f, 0.73f, 0.14f, 1.0f));
cube1->setPosition(Vector3(3.0, 0.0, 0.0));
auto cube2 = std::make_shared<Cube>(Color(0.96f, 0.67f, 0.0f, 1.0f));
cube2->setPosition(Vector3(-3.0, 0.0, 0.0));
auto sphere = std::make_shared<Sphere>(Color(0.61f, 0.07f, 0.18f, 1.0f));
Scene scene;
scene.addMesh(cube1);
scene.addMesh(cube2);
scene.addMesh(sphere);
glClearColor(0.29f, 0.36f, 0.4f, 1.0f);
while (!glfwWindowShouldClose(window))
{
renderScene(scene);
glfwSwapBuffers(window);
glfwPollEvents();
printFps();
}
}
Viewport an Fenstergröße anpassen
Falls sich die Fenstergröße ändert, muss ein Callback gesetzt werden, damit wir enstprechend darauf reagieren können. Wir fügen dazu zunächst im Renderer ein flag resized hinzu und setzen es auf true, wenn das Callback getriggert wird. Ebenfalls fügen wir eine Funktion setViewportSize ein, die die nötigen Änderungen vornehmen soll.
renderer.h
class Renderer
{
public:
...
private:
bool resized = false;
...
void setViewportSize();
};
renderer.cpp
Renderer::Renderer(const std::string &title, uint32_t width, uint32_t height)
{
...
glfwSetWindowUserPointer(window, this);
...
glfwSetFramebufferSizeCallback(window, [](GLFWwindow *w, int width, int height)
{
Renderer *self = static_cast<Renderer *>(glfwGetWindowUserPointer(w));
self->resized = true;
});
...
}
void Renderer::start()
{
...
setViewportSize();
glClearColor(0.29f, 0.36f, 0.4f, 1.0f);
while (!glfwWindowShouldClose(window))
{
...
if (resized)
{
resized = false;
setViewportSize();
}
}
}
void Renderer::setViewportSize()
{
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
activecamera.loadProjectionMatrix(width / static_cast<double>(height));
}
Hausaufgabe
- Räumen Sie Ihren Code auf, schreiben Sie Kommentare und machen Sie sich Notizen für die Prüfung.
- Bereiten Sie eigenständig bis zur nächsten Veranstaltung Kapitel 6 vor: Implementieren Sie dazu das Tutorial von meiner Website und ziehen Sie weitere Quellen zu Rate um exakt zu verstehen was die Software macht und warum.