In diesem Kapitel wollen wir die Beleuchtung unserer Szene implementieren.

Aktivieren des Tiefenpuffers

Das Ergebnis des letzten Kapitels zeigt sehr anschaulich ein Problem in der 3D Grafik: Objekte oder Vertices werden in genau der Reihenfolge gezeichnet, wie sie an die GPU gesendet werden. Das führt bei frei beweglichen Kameras zu Problemen, da Objekte nicht unbedingt in einer logischen Reihenfolge gezeichnet werden. Hierbei hilft der Tiefenpuffer. Eine Erklärung dazu finden Sie auch im OpenGL Wiki. Die GPU bringt die dafür benötigte Funktionalität mit. Wir müssen das Feature lediglich aktivieren und vor jedem Renderaufruf sicherstellen, dass der Tiefenpuffer geleert wird.

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); ... } void Renderer::renderScene(Scene &scene) const { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ... }

Wenn Sie nun die Kamera drehen, werden Sie feststellen dass die Objekte in der richtigen Reihenfolge erscheinen.

3D Modelle für Beleuchtung vorbereiten

Erweiterung der Vertex Struktur

Unser Vertex besteht aktuell nur aus Position und Farbe. Um die Helligkeit von Oberflächen zu berechnen benötigt die GPU zusätzlich Informationen darüber, in welche Richtung die Oberfläche ausgerichtet ist. Hierfür erweitern wir die Vertex Struktur damit wir pro Vertex einen zusätzlichen Normalenvektor übergeben können.

cgmath.h

struct Vertex { Vertex(const Vector3 &position, const Vector3 &norm, const Color &color) : 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)), color(static_cast<float>(color.r), static_cast<float>(color.g), static_cast<float>(color.b)) { } float position[3]; float normal[3]; float color[3]; };

Anschließend müssen wir unsere 3D Modelle so anpassen, dass diese für jedes Vertex die passende Normale berechnen.

Würfel mit Normalen

Den folgenden Code können Sie so übernehmen. Wichtig ist, dass Sie verstehen und erklären können in welche Richtung die Normalenvektoren des Würfels zeigen.

cube.cpp

Cube::Cube(const Color &color) { ... Vector3 normal(0, 0, 1); vertices.emplace_back(p1, normal, color); vertices.emplace_back(p2, normal, color); vertices.emplace_back(p3, normal, color); vertices.emplace_back(p4, normal, 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)); Vector3 normalRotated = (rotationMatrix * Vector4(normal, 1)).xyz(); Vector4 result = rotationMatrix * Vector4(p1, 1.0); vertices.emplace_back(result.xyz(), normalRotated, color); result = rotationMatrix * Vector4(p2, 1.0); vertices.emplace_back(result.xyz(), normalRotated, color); result = rotationMatrix * Vector4(p3, 1.0); vertices.emplace_back(result.xyz(), normalRotated, color); result = rotationMatrix * Vector4(p4, 1.0); vertices.emplace_back(result.xyz(), normalRotated, color); } }

Kugel mit Normalen

Den folgenden Code können Sie so übernehmen. Wichtig ist, dass Sie verstehen und erklären können in welche Richtung die Normalenvektoren der Kugel zeigen.

sphere.cpp

Sphere::Sphere(const Color &color) { ... for (int y = 0; y < rings; y++) { for (int x = 0; x < segments; x++) { vertices.emplace_back(vectors[x][y + 1], vectors[x][y + 1], color); vertices.emplace_back(vectors[x + 1][y + 1], vectors[x + 1][y + 1], color); vertices.emplace_back(vectors[x + 1][y], vectors[x + 1][y], color); vertices.emplace_back(vectors[x][y], vectors[x][y], color); } } }

Beleuchtung aktivieren

Unsere Szene sieht aktuell noch nicht wirklich dreidimensional aus. Das liegt daran, dass wir bisher keinerlei Beleuchtung implementiert haben. Alle Seiten der Objekte sind gleich hell. Das menschliche Auge benötigt aber Helligkeitsunterschiede um die Form eines Objektes erkennen zu können.

Licht einschalten

Damit unsere GPU die nötigen Funktionen zur Beleuchtung nutzen kann, müssen wir zuerst das Feature einschalten.

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_LIGHTING); glEnable(GL_NORMALIZE); float noLight[4] = {0.0, 0.0, 0.0, 1.0}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, noLight); ... }

Lichtfarben und -positionen festlegen

Anschließend definieren wir die Lichtfarben. Hierfür nehmen wir an, dass die Lichtquelle weißes Licht emittiert. Zusätzlich setzen wir einen Wert für das Umgebungslicht mit einer Helligkeit von 10% der Lichtquelle.

renderer.cpp

namespace Colors { Color white = Color(1.0, 1.0, 1.0, 1.0); Color sunLight = Color(0.9, 0.9, 0.9, 1.0); Color ambientLight = Color(0.05, 0.05, 0.05, 1.0); }

OpenGL unterstützt mindestens 8 Lichtquellen. (GL_LIGHT0 bis GL_LIGHT7) Jede dieser Lichtquellen können wir zur Laufzeit mit Parametern versehen sowie ein- und ausschalten. Wir entscheiden uns dafür GL_LIGHT1 zu verwenden und befüllen es nun mit den eben festgelegten Werten.

scene.h

class Scene { public: ... void render(const Camera &camera) const; void setLight(const Vector4 &position, const Color &diffuse, const Color &ambient, const Color &specular); private: ... float lightPosition[4] = {0.0f, 0.0f, 0.0f, 0.0f}; float lightAmbient[3] = {0.0f, 0.0f, 0.0f}; float lightDiffuse[3] = {0.0f, 0.0f, 0.0f}; float lightSpecular[3] = {0.0f, 0.0f, 0.0f}; };

scene.cpp

void Scene::render(const Camera &camera) const { camera.loadViewMatrix(); glEnable(GL_LIGHT1); glLightfv(GL_LIGHT1, GL_POSITION, lightPosition); glLightfv(GL_LIGHT1, GL_AMBIENT, lightAmbient); glLightfv(GL_LIGHT1, GL_DIFFUSE, lightDiffuse); glLightfv(GL_LIGHT1, GL_SPECULAR, lightSpecular); for (const std::shared_ptr<Mesh> &mesh : meshes) { mesh->render(); } glDisable(GL_LIGHT1); } void Scene::setLight(const Vector4 &position, const Color &diffuse, const Color &ambient, const Color &specular) { lightPosition[0] = static_cast<float>(position.x); lightPosition[1] = static_cast<float>(position.y); lightPosition[2] = static_cast<float>(position.z); lightPosition[3] = static_cast<float>(position.w); lightAmbient[0] = static_cast<float>(ambient.r); lightAmbient[1] = static_cast<float>(ambient.g); lightAmbient[2] = static_cast<float>(ambient.b); lightDiffuse[0] = static_cast<float>(diffuse.r); lightDiffuse[1] = static_cast<float>(diffuse.g); lightDiffuse[2] = static_cast<float>(diffuse.b); lightSpecular[0] = static_cast<float>(specular.r); lightSpecular[1] = static_cast<float>(specular.g); lightSpecular[2] = static_cast<float>(specular.b); }

Rendern mit Beleuchtung

Beim Rendern können wir nun für jeden Frame das Licht anpassen, falls wir ein bewegliches Licht haben. Und für jedes Mesh übergeben wir nun zuerst den Normalenvektor und dann die Farbe. Beachten Sie, dass wir statt glColor3fv() nun glMaterialfv() verwenden. Mit dieser Funktion können wir genauer bestimmen welche Eigenschaften eine Oberfläche hat. Hier legen wir fest, dass unser Material sowohl im direkten Licht als auch im indirekten Umgebungslicht die gleiche Farbe hat.

Die Scene heißt seit diesem Kapitel foreground weil wir sie in den nachfolgenden Kapiteln um eine weitere background Scene ergänzen werden.

renderer.cpp

void Renderer::start() { ... Vector4 lightPosition(50000, 20000, 50000, 0); foreground.setLight(lightPosition, Colors::sunLight, Colors::ambientLight, Colors::white); while (!glfwWindowShouldClose(window)) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); foreground.render(activeCamera); glfwSwapBuffers(window); ... } }

mesh.cpp

void Mesh::render() const { ... for (auto vertex : vertices) { glNormal3fv((float *)&vertex.normal); glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, (float *)&vertex.color); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (float *)&vertex.color); glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 30.0f); glVertex3fv((float *)&vertex.position); } glEnd(); glPopMatrix(); }

Praktikum

Was ist das?

Diese Aufgabe sollten Sie eigenverantwortlich am 16.05.2025 erledigen. Das Ergebnis bringen Sie gemeinsam mit Ihrem vorbereiteten Kapitel 7 mit in die Veranstaltung am 23.05.2025. Dort werden wir die Ergebnisse gemeinsam besprechen.

Schreiben Sie einen neuen Konstruktor für die Sphere Klasse mit dem sich ein Sphere Segment erzeugen lässt. Ein solches Segment ist ein Achtel der kompletten Kugel wie im folgenden Bild:

Ein Achtel der Kugel

Sobald Sie in der Lage sind ein solches Segment zu erzeugen, versuchen Sie im Renderer eine komplette Kugel zu erzeugen indem Sie 8 Einzelteile laden und entsprechend transformieren.

void Renderer::start() { auto sphereSegment1 = std::make_shared<Sphere>(...); auto sphereSegment2 = std::make_shared<Sphere>(...); auto sphereSegment3 = std::make_shared<Sphere>(...); ... }

Das Ergebnis sollte genauso aussehen wie vorher, nur dass man nun die einzelnen Kugelsegmente unterschiedlich einfärben kann.

Kugel bestehend aus 8 Einzelteilen

Gibt's noch was zu beachten?

In Kapitel 7 werden noch ein letztes Mal Änderungen an der Kugel vorgenommen. Das bedeutet auch ihre Kugelsegmente müssen wir später nochmal anpassen.

Sollen wir das dann auch bis zum nächsten Termin machen?

Nein, das wird Teil der nächsten Aufgabe im Praktikum am 23.05.

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 (am 23.05.) die Praktikumsaufgabe und Kapitel 7 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.