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