Parallax Mapping (Offset Mapping, Virtual Displacement Mapping) + Ambient Occlusion

Bereits in OpenGL-Tutorial 12 haben Sie eine Technik kennengelernt, mit deren Hilfe sich im Vergleich zum Normal Mapping mit nur wenig mehr Berechnungsaufwand deutlich realistischere Oberflächenstrukturen simulieren lassen – das sogenannte Parallax Mapping (Offset Mapping oder Virtual Displacement Mapping). Berücksichtigt man darüber hinaus die in der Normal Map gespeicherten Oberflächen-Informationen zur Modifizierung des ambienten Beleuchtungsanteils (Stichwort Ambient Occlusion), so wird die räumliche Tiefenwirkung der Oberflächen noch einmal deutlich verbessert.








































Obgleich durch das Normal Mapping die Beleuchtung in Computerspielen sehr viel realistischer wirkt, hat es doch einen entscheidenen Nachteil – stimmen die in der Textur gespeicherten Normalen nicht mit der Flächennormale des zu texturierenden Dreiecks (bzw. mit den Normalen der Dreiecks-Vertices) überein, dann sind die zur Texturierung verwendeten Texturkoordinaten nicht ganz korrekt. Gleiches gilt natürlich auch für das Surface Mapping, da hierfür die gleichen Texturkoordinaten wie beim Normal Mapping verwendet werden.
Im Zuge einer Überarbeitung besagten Programmbeispiels werden wir uns im Rahmen des heutigen Artikels mit einer speziellen Technik befassen, bei welcher die Neuberechnung der Texturkoordinaten direkt im World Space erfolgt.


Texturen werden dazu verwendet, um einem 3D-Modell zusätzliche Oberflächendetails hinzuzufügen. Blickt man jedoch schräg auf eine Oberfläche, dann fällt natürlich auf, dass diese zusätzlichen Details keinen räumlichen Charakter haben – sie sind lediglich zweidimensional. Nur bei ebenen Oberflächen (bsp. eine Metall- oder Holzplatte) ist die Illusion perfekt, da alle Texel (Textur Pixel) nahezu gleiche Höhenwerte haben. Verwendet man zusätzlich zur Surface Map eine Normal Map, so sind alle in der Textur gespeicherten Normalen parallel zur Flächennormale des zu texturierenden Dreiecks orientiert. In diesem Fall erzielt man mit einfachem Normal Mapping das gleiche optische Resultat wie beim Parallax Mapping. Die zu verwendenden Texturkoordinaten müssen daher nicht korrigiert werden (DeltaTex == 0.0).
Unebene Flächen zeichnen sich durch unterschiedliche Oberflächen-Höhenwerte aus, die wir zusätzlich zu den Texturnormalen im Alpha-Kanal unserer Normal Maps speichern. Die sich ändernden Höhenwerte haben zur Folge, dass die Texturnormalen nun nicht mehr parallel zur Flächennormale des zu texturierenden Dreiecks orientiert sind. Als Maß für die Abweichung verwenden wir das Punktprodukt aus Texturnormale und Flächennormale:

// Für ebene Oberflächen ist Factor1 == 0.0
// Da sich Factor1 proportional zu DeltaTex verhält, ist für ebene
// Oberflächen DeltaTex == 0. Normal Mapping und Parallax Mapping
// liefern in diesem Fall das gleiche optische Resultat.
float Factor1 =  dot(NormalPerTexel, VertexNormal);
Factor1 = 1.0 - Factor1*Factor1;


Der zweite Faktor, der sich auf DeltaTex ausswirkt, ist der Blickwinkel der Kamera. Das Problem – von der Kamera abgewandte, bzw. aus Sicht der Kamera verdecke Flächen (großer Winkel zwischen der Blickrichtung und der Texturnormale) müssen beim Parallax Mapping möglichst verschwinden. Auch hier hilft uns wieder das Punktptodukt weiter:

float Factor2 = max(dot(NormalPerTexel, -ViewDirection), 0.0);


Kombiniert man nun beide Faktoren miteinander, dann lässt sich der Texturkoordinaten-Korrektur-Term (DeltaTex) wie folgt berechnen:

float DeltaTex = Factor2*Factor1*OffsetMappingScale*NormalColor.a;


Der Parameter OffsetMappingScale dient zur Gewichtung der beiden Faktoren. Des Weiteren legen wir fest (!), dass sich DeltaTex zusätzlich proportional zu den in der Normal Map gespeicherten Höhenwerten verhalten soll.

Mithilfe des Korrektur-Terms DeltaTex berechnen sich schließlich die korrigierten Texturkoordinaten wie folgt:

vec2 TexCorrected = gs_TexCoord[0].st - vec2(DeltaTex, DeltaTex);


Um den aus der Texturkoordinaten-Änderung resultierenden räumlichen Eindruck zu verstärken, berechnen wir unter Verwendung von DeltaTex zusätzlich einen Korrekturterm für die ambiente Beleuchtung:

float AmbientOcclusionValue = (1.0-max(AmbientOcclusionScale*DeltaTex, 0.0));

Hinweis:
Risse und Spalten in einer Oberfläche erscheinen bei Abwesenheit von direkter Beleuchtung dunkler als die übrigen Oberflächenbereiche, da „weniger Licht“ in die Vertiefungen eindringt. Die ambiente Lichtintensität entspricht der Summe des eingestrahlten Lichts aus allen unverdeckten Richtungen. Die Grenzflächen der Vertiefungen blockieren nun den Lichteinfall aus einigen Richtungen (Beschattung), wodurch sich die ambiente Lichtintensität verringert (Ambient Occlusion).
Bei flachen Oberflächen ist die ambiente Lichtintensität am größten, da der Lichteinfall an keiner Stelle blockiert wird. In Ecken und Kanten verringert sich dagegen die ambiente Lichtintensität, da sich auch hier die beteiligten Oberflächen gegenseitig beschatten:













Kommen wir nun zur Implementierung im Fragment Shader:

#version 330

#define texture2D texture
#define textureCube texture

precision highp float;

in  vec4 gs_TexCoord[8];
out vec4 gs_FragColor;
// Hinweis: Die Scale-Faktoren werden im Shader als Konstanten definiert,
// damit man sie im Testbetrieb unkompliziert modifizieren kann!
// Selbstverständlich können die Werte auch aus dem Hauptprogramm heraus
// übergeben werden!
const float OffsetMappingScale = 0.04;
const float AmbientOcclusionScale = 40.0;


uniform vec3 ViewDirection;

uniform sampler2D SurfaceTexture;
uniform sampler2D NormalTexture;

uniform vec3 negLightDir;
uniform vec4 LightColor;
uniform float AmbientValue;

void main()
{
    mat3 matTexturespaceToWorldspace;

    // Perpendicular2:
    matTexturespaceToWorldspace[0] = gs_TexCoord[2].xyz;

    // Perpendicular1:
    matTexturespaceToWorldspace[1] = gs_TexCoord[3].xyz;

    // Normal:
    matTexturespaceToWorldspace[2] = gs_TexCoord[1].xyz;

    vec4 NormalColor = texture2D(NormalTexture, gs_TexCoord[0].st);

    vec3 NormalPerTexel = matTexturespaceToWorldspace*
                          (2.0*NormalColor.rgb - 1.0);

    float Factor2 = max(dot(NormalPerTexel, -ViewDirection), 0.0);
    float Factor1 = dot(NormalPerTexel, gs_TexCoord[1].xyz);
    Factor1 = 1.0 - Factor1*Factor1;

    float DeltaTex = Factor2*Factor1*OffsetMappingScale*NormalColor.a;

    float AmbientOcclusionValue = (1.0-max(AmbientOcclusionScale*DeltaTex, 0.0));

    vec2 TexCorrected = gs_TexCoord[0].st - vec2(DeltaTex, DeltaTex);

    vec4 SurfaceColor = texture2D(SurfaceTexture, TexCorrected);

   
// durchsichtige Bereiche nicht rendern:
    if(SurfaceColor.a < 0.01)
        discard;

    NormalPerTexel = matTexturespaceToWorldspace*
                    (2.0*texture2D(NormalTexture, TexCorrected).rgb - 1.0);

    gs_FragColor = 0.75*min((AmbientOcclusionValue*AmbientValue+
                   0.5*dot(NormalPerTexel, negLightDir)+0.5), 2.0)*
                   LightColor*SurfaceColor;
}