Beleuchtung und Tiefenpuffer
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, in der 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
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:

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.
renderer.cpp
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.

Lernziele
Nach diesem Kapitel sollten Sie die folgenden Fragen beantworten können:
- Was ist ein Tiefenpuffer (Z-Buffer) und wie entscheidet die GPU mit seiner Hilfe, welches Fragment sichtbar ist?
- Was ist ein Normalenvektor und warum wird er pro Vertex und nicht pro Dreieck gespeichert?
- Worin unterscheiden sich ambiente, diffuse und spekulare Beleuchtung und wie tragen sie zum Gesamteindruck einer Oberfläche bei?
- Was beschreibt das Phong-Beleuchtungsmodell und aus welchen Komponenten setzt es die Helligkeit eines Punktes zusammen?
- Warum muss die Lichtposition nach der View-Matrix gesetzt werden und welchen Effekt hat die vierte Komponente (w) des Positionsvektors auf die Art der Lichtquelle?
- Was ist der Unterschied zwischen
glColorundglMaterialund warum reicht eine einfache Farbe für beleuchtete Szenen nicht aus? - Was bewirkt
GL_NORMALIZEund warum können Normalenvektoren nach einer Transformation nicht mehr normalisiert sein?
Bearbeitungshinweise
Arbeiten Sie dieses Kapitel eigenständig vor dem zugehörigen Veranstaltungstermin durch. Wie die Vorbereitung abläuft und worauf es dabei ankommt, erfahren Sie im Konzept der Veranstaltung.