In diesem Kapitel kümmern wir uns um die Beleuchtung unserer Szene.
Das Phong-Beleuchtungsmodell
Die Aufgabe eines Beleuchtungsmodells ist es, die Helligkeit der einzelnen Pixel auf der Oberfläche eines Objektes zu bestimmen. Dabei gilt die Annahme, dass einfallendes Licht von einer Oberfläche entweder absorbiert wird (matt schwarze Oberfläche) oder reflektiert wird (spiegelnde Oberfläche). Die meisten Materialien absorbieren einen Teil des Lichtes (bestimmt durch die Farbe des Objektes) und reflektieren einen bestimmten Anteil des Lichtes (bestimmt durch die Glattheit oder Rauheit des Objektes). Für die Berechnung der Beleuchtung unserer Szene verwenden wir das Phong-Beleuchtungsmodell. Dieses Modell ist leicht zu implementieren und veranschaulicht auf einfache Weise dieses Grundprinzip der Beleuchtung. Das Phong Modell teilt die Lichtberechnung in 3 Komponenten auf, die wir nun nacheinander implementieren wollen:
- Ambient Light (Hintergrundlicht)
- Diffuse Light (ideal diffuser Anteil)
- Specular Light (ideal spiegelnder Anteil)
Ambiente Komponente
Die ambiente Komponente beschreibt ein Hintergrundlicht welches immer, unabhängig von der Position der Lichtquellen eine Szene beleuchtet. Es simuliert das Verhalten von Licht an einem bedeckten Tag ohne direktes Sonnenlicht und dient dazu, dass die Rückseite von beleuchteten Objekten nicht komplett schwarz erscheinen sondern immer noch leicht sichtbar sind. In unserem Shadercode nutzen wir den Variablennamen ambient
Ideal diffuse Komponente
Das gerichtete Licht (beispielsweise von der Sonne oder einer Lampe) wird im Phong Beleuchtungsmodell in zwei unabhängige Komponenten aufgeteilt. Die diffuse Komponente beschreibt den Anteil des Lichtes, der beim Auftreffen auf ein Objekt absorbiert wird. Diese Komponente beeinflusst die Farbe und Helligkeit des Objektes und ist unabhängig vom Standpunkt des Betrachters. In unserem Shadercode nutzen wir den Variablennamen diffuse
Ideal spiegelnde Komponente
Die spiegelnde Komponente beschreibt den Anteil des Lichtes, der beim Auftreffen auf ein Objekt reflektiert wird. Diese Komponente beschreibt die Glanzlichter auf der Oberfläche des Objektes und ist vom Standpunkt des Betrachters abhängig. In unserem Shadercode nutzen wir den Variablennamen specular
Weiterführende Infos
Die drei Komponenten werden anschaulich mit Bildern und Formeln unter anderem in diesem Wikipedia Artikel erklärt.
Neue Shader
Wir beginnen damit zwei neue Shader zu erstellen. In diesen Shadern implementieren wir das oben beschriebene Bleuchtungsmodell.
shaders/vertex_shader_lit.glsl
#version 330 core
layout (location = 0) in vec3 VertPosIn;
layout (location = 1) in vec2 TexCoordIn;
layout (location = 2) in vec3 NormVecIn;
out vec2 TexCoord;
out vec3 NormVec;
out vec3 SunDirectionObjSpc;
out vec3 CameraPosObjSpc;
out vec3 VertPos;
uniform mat4 WorldMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectionMatrix;
uniform vec3 SunDirection;
uniform vec3 CameraPos;
void main()
{
mat4 WvpMatrix = ProjectionMatrix * ViewMatrix * WorldMatrix;
gl_Position = WvpMatrix * vec4(VertPosIn, 1.0);
TexCoord = TexCoordIn;
NormVec = NormVecIn;
SunDirectionObjSpc = normalize(vec3(inverse(WorldMatrix) * vec4(normalize(SunDirection), 0.0)));
CameraPosObjSpc = vec3(inverse(WorldMatrix) * vec4(CameraPos, 1.0));
VertPos = VertPosIn;
}
shaders/fragment_shader_lit.glsl
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
in vec3 NormVec;
in vec3 VertPos;
in vec3 SunDirectionObjSpc;
in vec3 CameraPosObjSpc;
uniform sampler2D Diffuse;
void main()
{
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 textureColor = vec3(texture(Diffuse, TexCoord));
// ambient
vec3 ambient = 0.1 * lightColor * textureColor;
// diffuse
float lightIntensity = max(dot(NormVec, SunDirectionObjSpc), 0.0);
vec3 diffuse = 0.9 * lightIntensity * lightColor * textureColor;
// specular
vec3 viewDir = normalize(VertPos - CameraPosObjSpc);
vec3 reflectDir = reflect(SunDirectionObjSpc, NormVec);
vec3 specular = 0.5 * pow(max(dot(viewDir, reflectDir), 0.0), 16) * lightColor;
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
Kameraposition
Damit die Berechnung der Spiegelungen auf der Oberfläche von Objekten funktioniert, benötigen wir die Position der Kamera als Koordinate im Shader. Dazu müssen wir an verschiedenen Stellen im Code Anpassungen vornehmen. Wir holen die Kameraposition aus Simulation und übergeben diese jedes Frame an den Renderer. Innerhalb des Renderers wird diese Position an jedes Model durch model.render() übergeben.
simulation.h
class Simulation
{
public:
...
const Vector3 &getCameraPosition();
private:
...
};
simulation.cpp
const Vector3 &Simulation::getCameraPosition()
{
return camera.getPosition();
}
renderer.h
...
class Renderer
{
public:
...
void setCameraPosition(const Vector3 &cameraPosition);
private:
...
Vector3 cameraPosition = Vector3(0.0, 0.0, 0.0);
};
renderer.cpp
...
void Renderer::loop()
{
...
for (auto &[key, model] : models)
{
model.render(projectionMatrix, viewMatrix, sunDirection, cameraPosition);
}
}
...
void Renderer::setCameraPosition(const Vector3 &cameraPosition)
{
this->cameraPosition = cameraPosition;
}
main.cpp
while (window.loop(deltaTime))
{
...
renderer.setCameraPosition(simulation.getCameraPosition());
renderer.loop();
}
model.h
class Model
{
public:
...
void render(const Matrix4 &projectionMatrix, const Matrix4 &viewMatrix, const Vector3 &sunDirection, const Vector3 &cameraPosition);
private:
...
};
model.cpp
void Model::render(const Matrix4 &projectionMatrix, const Matrix4 &viewMatrix, const Vector3 &sunDirection, const Vector3 &cameraPosition)
{
...
shader->setVector3("CameraPos", cameraPosition);
...
mesh->draw();
}
Normalenvektoren
Außerdem benötigt jedes Dreieck auf der Oberfläche von Objekten einen sog. Normalenvektor. Dieser Vektor steht per Definition senkrecht auf der jeweiligen Oberfläche bzw. auf dem jeweiligen Vertex und kann dann im Shader benutzt werden um den Einfallswinkel des Lichtes zu bestimmen. Wir ergänzen den Normalenvektor für unsere Meshes damit diese Daten im Shader ankommen.
vertex.h
struct Vertex
{
Vertex(const Vector3 &position, const Vector2 &texCoord, const Vector3 &normal);
...
float normal[3] = {};
};
vertex.cpp
Vertex::Vertex(const Vector3 &position, const Vector2 &texCoord, const Vector3 &normal)
: ...,
normal{static_cast<float>(normal.x), static_cast<float>(normal.y), static_cast<float>(normal.z)}
{
}
mesh.cpp
Mesh::Mesh(const std::string &filename)
{
...
vertices.reserve(v.size());
for (int i = 0; i < f.size(); i++)
{
...
vertices.emplace_back(v[values[0]], vt[values[1]], vn[values[2]]);
}
init();
}
void Mesh::init()
{
...
glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void *)offsetof(Vertex, normal));
glEnableVertexAttribArray(2);
}
Neue Shader aktivieren
Um die neuen Shader in Betrieb zu nehmen, müssen wir lediglich die beiden Modell Dateien anpassen.
models/thm.model
m meshes/thm.obj
s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit.glsl
t Diffuse textures/thm_colors.jpg
models/earth.model
m meshes/sphere.obj
s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit.glsl
t Diffuse textures/earth_diffuse.jpg
Nun sollte sich die Engine starten lassen und die beiden Objekte sollten mit Beleuchtung erscheinen.
Diffuse-Roughness-Normal Workflow
Um realistischere Ergebnisse zu erzielen als die einfache Beleuchtung, benötigen wir zusätzliche Texturen um weitere Daten an unseren Shader zu übergeben. Ein verbreiteter Workflow bei der Erstellung solcher Texturen ist der Diffuse-Roughness-Normal Workflow. Das heißt statt nur einer Textur für die Farbe eines Objektes, werden drei Texturen geladen. Die Diffuse Texture enthält Informationen über die Farbe, eine zusätzliche Roughness Textur enthält Informationen darüber, wie glatt bzw. rau die Oberfläche ist und die Normal Textur enthält die Normalenvektoren der Oberfläche. Die Diffuse Textur haben Sie bereits. Laden Sie sich die beiden zusätzlichen Grafiken hier herunter.
Laden Sie sich die Roughness Textur herunter. Speichern Sie die Textur anschließend unter Computergrafik/cgm_08/textures/earth_roughness.jpg
Laden Sie sich die Normal Textur herunter. Speichern Sie die Textur anschließend unter Computergrafik/cgm_08/textures/earth_normal.jpg
Neuer DRN Shader
shaders/fragment_shader_lit_drn.glsl
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
in vec3 NormVec;
in vec3 VertPos;
in vec3 SunDirectionObjSpc;
in vec3 CameraPosObjSpc;
uniform sampler2D Diffuse;
uniform sampler2D Roughness;
uniform sampler2D NormalMap;
void main()
{
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 textureColor = vec3(texture(Diffuse, TexCoord));
float roughness = vec3(texture(Roughness, TexCoord)).r;
vec3 normal = normalize(texture(NormalMap, TexCoord).rgb * 2.0 - 1.0);
// ambient
vec3 ambient = 0.1 * lightColor * textureColor;
// diffuse
float lightIntensity = max(dot(normal, SunDirectionObjSpc), 0.0);
vec3 diffuse = 0.9 * lightIntensity * lightColor * textureColor;
// specular
vec3 viewDir = normalize(VertPos - CameraPosObjSpc);
vec3 reflectDir = reflect(SunDirectionObjSpc, normal);
vec3 specular = 0.5 * pow(max(dot(viewDir, reflectDir), 0.0), 16) * lightColor * (1.0 - roughness);
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
Modelldatei der Erde anpassen
Damit der neue Shader und die neuen Texturen genutzt werden, passen wir die .model Datei entsprechend an.
models/earth.model
m meshes/sphere.obj
s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit_drn.glsl
t Diffuse textures/earth_diffuse.jpg
t Roughness textures/earth_roughness.jpg
t NormalMap textures/earth_normal.jpg
Nun sollte sich die Engine starten lassen und die Erde sollte detailreicher erscheinen als zuvor.