In diesem Kapitel kümmern wir uns um die Beleuchtung unserer Szene.

Inhalt


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 das die Rückseite von beleuchteten Objekten nicht komplett schwarz erscheinen sondern immer noch leicht sichtbar sind.

In unserem Shader Code nutzen wir den Variablennamen ambient

Ideal diffuse Komponente

Die diffuse Komponente beschreibt ein gerichtetes Licht welches aus Richtung der Lichtquelle auf die Oberfläche scheint, aber unabhängig vom Standpunkt des Betrachters in alle Richtungen reflektiert wird.

In unserem Shader Code nutzen wir den Variablennamen diffuse

Ideal spiegelnde Komponente

Die spiegelnde Komponente ist ebenfalls eine gerichtetes Licht welches aus Richtung der Kamera auf die Oberfläche scheint, es ist allerdings vom Standpunkt des Betrachters abhängig da es lediglich die Reflexion des Lichtes zur Kamera wiedergibt.

In unserem Shader Code 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

Aus Gründen der Anschaulichkeit sind hier in blau die Unterschiede zum einfachen vertex_shader.glsl hervorgehoben.

#version 330 core
layout (location = 0) in vec3 VertPosIn;
layout (location = 1) in vec2 TexCoordIn;
layout (location = 2) in vec3 NormVectIn;

out vec2 TexCoord;
out vec3 NormVect;
out vec3 SunLightObjSpc;
out vec3 CameraPosObjSpc;
out vec3 VertPos;

uniform mat4 WorldMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectionMatrix;
uniform vec3 SunLight;
uniform vec3 CameraPos;

void main()
{
    mat4 WvpMatrix = ProjectionMatrix * ViewMatrix * WorldMatrix;
    gl_Position = WvpMatrix * vec4(VertPosIn, 1.0);
    TexCoord = TexCoordIn;
    NormVect = NormVectIn;
    SunLightObjSpc = vec3(inverse(WorldMatrix) * vec4(SunLight, 0.0));
    CameraPosObjSpc = vec3(inverse(WorldMatrix) * vec4(CameraPos, 1.0));
    VertPos = VertPosIn;
}

shaders/fragment_shader_lit.glsl

Aus Gründen der Anschaulichkeit sind hier in blau die Unterschiede zum einfachen fragment_shader.glsl hervorgehoben.

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;
in vec3 NormVect;
in vec3 VertPos;
in vec3 SunLightObjSpc;
in vec3 CameraPosObjSpc;

uniform sampler2D Diffuse;

void main()
{
    vec3 lightColor = vec3(1.0, 1.0, 1.0);
    vec3 albedo = vec3(texture(Diffuse, TexCoord));

    float lightIntensity = max(dot(NormVect, -SunLightObjSpc), 0.0);

    vec3 ambient = 0.1 * albedo;
    vec3 diffuse = lightIntensity * lightColor * albedo;

    vec3 viewDir = normalize(VertPos - CameraPosObjSpc);
    vec3 reflectDir = reflect(-SunLightObjSpc, NormVect);
    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.

game.cpp

void graphicsUpdateCamera(Matrix, Vector3);
void gameLoop(double time)
{
    //A=65; S=83; D=68; W=87
    Vector3 movement = {};
    if (gameKeyState[87]) movement.z -= walkSpeed * time;
    if (gameKeyState[83]) movement.z += walkSpeed * time;
    if (gameKeyState[65]) movement.x -= walkSpeed * time;
    if (gameKeyState[68]) movement.x += walkSpeed * time;

    movement = matrixVector3Multiply(matrixRotateY(-cameraYaw), movement);
    cameraPosition = vector3Sum(cameraPosition, movement);

    Matrix tMatrix = matrixTranslate(-cameraPosition.x, -cameraPosition.y, -cameraPosition.z);
    Matrix yMatrix = matrixRotateY(cameraYaw);
    Matrix xMatrix = matrixRotateX(cameraPitch);

    graphicsUpdateCamera(matrixMultiply(xMatrix, matrixMultiply(yMatrix, tMatrix)), cameraPosition);
}

graphics.cpp

static Settings settings;
static int viewportWidth = 0;
static int viewportHeight = 0;
static int resizeViewport = false;
static Matrix viewMatrix;
static Matrix projectionMatrix;
static Vector3 cameraPosition;
static Vector3 sunLight = (Vector3){0.577f,-0.577f,-0.577f};
static std::map<int, Model*> models;
static int modelId = 0;
void graphicsUpdateCamera(Matrix m, Vector3 cp)
{
    viewMatrix = m;
    cameraPosition = cp;
}
void graphicsLoop()
{
    setViewport();
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    for (std::map<int, Model*>::iterator itr = models.begin(), itr_end = models.end(); itr != itr_end; ++itr)
    {
        itr->second->render(projectionMatrix, viewMatrix, sunLight, cameraPosition);
    }
}

model.h

void render(Matrix projectionMatrix, Matrix viewMatrix, Vector3 sunLight, Vector3 cameraPosition);

model.cpp

void Model::render(Matrix projectionMatrix, Matrix viewMatrix, Vector3 sunLight, Vector3 cameraPosition)
{
    shader->activate();
    shader->setMatrix("ProjectionMatrix", projectionMatrix);
    shader->setMatrix("ViewMatrix", viewMatrix);
    shader->setMatrix("WorldMatrix", matrixMultiply(matrixTranslate(position.x, position.y, position.z), matrixMultiply(matrixRotateX(rotation.x), matrixMultiply(matrixRotateY(rotation.y), matrixRotateZ(rotation.z)))));
    shader->setVector3("SunLight", sunLight);
    shader->setVector3("CameraPos", cameraPosition);
    for (std::map<std::string,Texture*>::iterator itr = textures.begin(), itr_end = textures.end(); itr != itr_end; ++itr)
    {
        shader->setTexture(itr->first.c_str(), itr->second);
    }
    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

#pragma once
#include "cgmath.h"

union Vertex {
    struct {
        Vector3 pos;
        Vector2 texcoord;
        Vector3 normal;
    };
    float values[8];
};

mesh.cpp

Mesh::Mesh(std::string filename)
{
    ...

    vertices.push_back((Vertex){ v[values[0]], vt[values[1]], vn[values[2]] });
}
void Mesh::init(Vertex* vertices, int vc)
{
    vertexCount = vc;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);

    glBufferData(GL_ARRAY_BUFFER, vertexCount * sizeof(Vertex), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)sizeof(Vector3));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(Vector3) + sizeof(Vector2)));
    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/earth.obj
s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit.glsl
t Diffuse textures/earth8k.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

Aus Gründen der Anschaulichkeit sind hier in blau die Unterschiede zum oben definierten fragment_shader_lit.glsl hervorgehoben.

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;
in vec3 NormVect;
in vec3 VertPos;
in vec3 SunLightObjSpc;
in vec3 CameraPosObjSpc;

uniform sampler2D Diffuse;
uniform sampler2D Roughness;
uniform sampler2D NormalMap;

void main()
{
    vec3 lightColor = vec3(1.0, 1.0, 1.0);
    vec3 albedo = vec3(texture(Diffuse, TexCoord));
    float roughness = vec3(texture(Roughness, TexCoord)).r;
    vec3 normal = normalize(texture(NormalMap, TexCoord).rgb * 2.0 - 1.0);

    float lightIntensity = max(dot(normal, -SunLightObjSpc), 0.0);

    vec3 ambient = 0.1 * albedo;
    vec3 diffuse = lightIntensity * lightColor * albedo;

    vec3 viewDir = normalize(VertPos - CameraPosObjSpc);
    vec3 reflectDir = reflect(-SunLightObjSpc, 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.

model/earth.model

m meshes/earth2.obj
s shaders/vertex_shader_lit.glsl shaders/fragment_shader_lit_drn.glsl
t Diffuse textures/earth8k.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.


Prüfungsvorbereitung

  • Können Sie erklären welche Rolle der Normalenvektor einer Fläche bei der Berechnung von Beleuchtung spielt?
  • Können Sie das Phong Beleuchtungsmodell in einfachen Worten beschreiben?
  • Das Phong Beleuchtungsmodell berechnet die 3 Komponenten der Beleuchtung unabhängig voneinander. Fällt Ihnen ein Grund ein, warum das Problematisch sein könnte?
  • Wir definieren im VertexShader eine Variable CameraPosObjSpc. Können Sie beschreiben was genau darin gespeichert wird?