Ich habe in den letzten Wochen ziemlich viel Zeit damit verbracht, Bits verschiedener Artikel zusammenzustellen, um herauszufinden, wie man Normal Mapping in der Lair Engine implementieren kann. Das Problem dabei ist, dass die Methoden und die Terminologie, die in Artikeln über Normal Mapping verwendet werden, sehr unterschiedlich sind. Das macht es zu einem sehr verwirrenden Thema für Nicht-Mathe-Liebhaber wie mich. In dem folgenden Artikel werde ich drei gebräuchliche Techniken für Normal Mapping für mathematische Laien wie mich erklären.
In dem folgenden Video wird erklärt, was Normal Mapping eigentlich ist:
Ursprünglich habe ich mit der Implementierung von Normal Maps ohne vorberechnete Tangenten begonnen, was nach dem einfachsten Weg klang, aber wahrscheinlich nicht der beste Ort für den Anfang war. Bei der Umsetzung gab es erhebliche Probleme, das zum Laufen zu bringen. Das größte Problem war das Licht, dass sich auch drehte, wenn das gesamte Modell gedreht wurde.
Nach ein paar Tagen frustvollen Debuggings bin ich für ein paar Wochen zu anderen Dingen übergegangen und habe mir in meiner Freizeit Artikel über Normal Mapping durchgelesen. Als ich dachte, dass ich es verstanden habe, entschloss ich mich diesen Code zu integrieren, um Tangenten-Vektoren für ein beliebiges Mesh zu berechnen, sowie Shader-Code, um Normal Mapping im View Space durchzuführen. Zu meiner Enttäuschung hatte diese Implementierung genau das gleiche Problem wie die Ursprüngliche: Die Beleuchtung drehte sich mit dem Modell. An diesem Punkt war ich ein wenig verblüfft, also stellte ich eine Frage im Forum und bekam dabei zwei hilfreiche Antworten:
Koordinationsräume.
Wenn Sie 3D-Rendering durchführen, gibt es viele verschiedene Koordinatenbereiche, mit denen Sie sich auseinandersetzen müssen. Bei Normal Mapping sieht die Kette der Transformationen so aus:
Tangent Space ist derjenige, der uns heute interessiert. Es ist der Koordinatenraum, in dem sich die Normals in einer Normal Map befinden.
Die TBN Matrix.
Jeder Artikel, den Sie über Normal Mapping lesen, spricht über die TBN-Matrix. Es ist benannt nach den Elementen, aus denen es sich zusammensetzt, den Vektoren Tangent, Bitangent und Normal. Die TBN-Matrix erlaubt es ihnen, Normals von der Normal Map in den Modellraum zu konvertieren. Das ist sozusagen die einfachste Aufgabe, die es macht.
Um eine TBN-Matrix aus einer Fläche und einer Tangente zu berechnen, benötigen Sie GLSL-Code wie folgt:
Das Normal, das Sie schon millionenfach gesehen haben, ist ein Vektor senkrecht zum Face im Modellraum. Tangentenpunkte entlang der positiven U-Textur-Koordinatenachse für die Fläche. Um die Bitangente zu berechnen, nehmen wir das Kreuzprodukt der normalen und tangentialen Vektoren und multiplizieren es dann mit einer Konstante in tangent.w, die die Handedness des tangentialen Raumes ist. Die Bitangentenpunkte entlang der V-Texturkoordinatenachse der Fläche. Im folgenden Beispiel wurde ein Texture Mapped Cube erstellt, um das Normal Mapping zu debuggen.
Diese TBN-Matrix ist für uns jetzt nicht besonders hilfreich, weil es hier um die Beleuchtung geht und es hier lediglich um die Umwandlung von Normals aus dem Tangentenraum in den Modellraum geht. Um einen größeren Nutzen zu erzielen, können wir es folgendermaßen aufbauen:
Durch Multiplikation jedes Vektors mit der Modellmatrix erhalten wir eine TBN, die vom Tangentenraum in den Weltraum konvertiert. Sie werden feststellen, dass wenn wir ein Vec4 aus den Normals, Tangens und Bitangens generieren, 0.0 für den w-Wert generieren und nicht 1.0. Der Grund dafür ist, dass keine Übersetzung in der Modellmatrix vorhanden ist, da es keinen Sinn macht, Richtungsvektoren zu übersetzen. Das bringt uns natürlich zu:
World Space Normal Mapping.
Die Idee mit World Space Normal Mapping ist es, ein Normal aus der Normal Map und dem Richtungsvektor zur Lichtquelle im World Space umzuwandeln, so dass wir ein Punktprodukt aus der Normal Map und der Lichtrichtung nehmen können, um die Größe des diffus reflektierenden Lichtes oder Lambertwertes zu erhalten.
So generiere ich im vollständigen Beispielcode für World Space Normal Mapping unten eine TBN-Matrix, die von Tangent Space in World Space konvertiert sowie einen lightDirection-Vektor im World Space im Fragment-Shader. Im Vertex-Shader verwende ich die TBN-Matrix, um das Normal von der Normal Map von Tangent Space in den World Space zu konvertieren und mit der normalisierten lightDirection auch im Weltraum zu punktieren.
Das ist ihnen vielleicht im Fragment-Shader aufgefallen:
Da die Daten in der normal Map im Bereich [0.0 – 1.0] gespeichert werden sollten, müssen Sie auf den ursprünglichen Bereich [-1.0 -1.0] zurückskalieren. Wenn wir von der Genauigkeit unserer Daten überzeugt sind, könnten wir die Normalisierung hier abschaffen.
Im gesamten Beispielcode generiere ich nur das Lambert Element der kompletten ADS (Ambient/Diffuse/Specular) Lighting Pipeline. Ich rendere diesen Wert dann als Graustufen, so dass Sie sehen können, welchen Beitrag die Normal Map zum endgültigen Bild leisten wird. Ich hoffe, dass Sie den Begriff Lambert gut verstehen werden, um ihn selbst in die komplette ADS-Beleuchtung einzubauen. Dabei sind die viewMatrix, modelMatrix, normalMatrix, modelViewMatrix etc. äquivalent zu der veralteten OpenGL GL_NormalMatrix, GL_ModelViewProjectionMatrix etc.
World Space Normal Mapping Vertex Shader.
World Space Normal Mapping Fragment Shader.
Das Tolle an der Arbeit im World Space ist also, dass man die Tangente, Bitangente und Normalwerte überprüfen kann, indem man sie an den Fragementshader übergibt und sie auf dem Bildschirm rendert und sich dann anschaut, welche Farbe sie haben. Wenn eine Fläche rot endet, dann zeigt der Vektor, den Sie auf dieser Fläche rendert, entlang der positiven X-Achse. Sie können dasselbe auch für die Normal Map Werte tun. Beachten Sie, dass diese Vektoren auch negative Werte haben können. Wenn Sie sich also nicht in den Bereich von [0.0 – 1.0] zurückskalieren, werden Sie einige schwarze Polygone sehen.
Ich baute ein Würfelmodell und verbrachte dann die meiste Zeit eines Nachmittags damit, jeden einzelnen Input in den Shader zu überprüfen, um herauszufinden, warum sich das Licht mit der Kamera drehte. Am Ende habe ich herausgefunden, dass das nicht wirklich das war, was überhaupt passiert ist.
Tangent Space Normal Maps.
Wenn Sie die Normal Map auf Google Images suchen, werden Sie viele pulverblaue Bilder sehen. Dies sind Tangent Space Normal Maps. Der Grund für die bläuliche Färbung ist, dass der Aufwärtsvektor für eine Normal Map auf der positiven Z-Achse liegt, die im blauen Kanal einer Bitmap gespeichert ist. Um eine Bitmap des Tangent Space zu betrachten, ist flat pulverblau. Normals hingegen, die nach oben zeigen, sind cyan und diejenigen, die nach unten zeigen, sind mangenta.
Es gibt keine Art von Standardisierung für Normal Maps, so dass einige nur zwei Datenkanäle haben, einige haben die Normals in die entgegengesetzte Richtung vertikal ausgerichtet, also schauen Sie sich die Normal Maps sehr genau an, um sicherzustellen, dass sie in dem Format sind, das Sie erwarten. Daneben gibt es auch Bump-Maps, die häufig grün sind. Diese sind meistens völlig unterschiedlich und es empfiehlt sich nicht, diese damit zu verwenden.
View Space Normal Mapping.
Sobald ich ein gutes Handling mit dem World Space Normal Mapping hatte, wollte ich auch das View Space Normal Mapping auszuprobieren. Die Idee ist hier die Gleiche wie beim normalen World Space Mapping, nur dass wir diesmal die Vektoren in View Space umwandeln. Der Grund dafür ist, dass Sie mehr Arbeit auf den Vertex-Shader verlagern und einige Berechnungen vereinfachen können.
Berechnen wie also unsere TBN noch einmal:
Das hier ist ein bisschen anders. Zuerst multiplizieren wir mit der normalMatrix, um den Normal-, Tangenten- und Bitangenwert in View Space umzuwandeln. Da die normale Matrix bereits 3×3 ist, brauchen wir den 0.0 Trick nicht zu benutzen, den im im World Space gemacht haben. Als nächstes machen wir eine Matte3 aus t, b und n, aber dieses Mal machen wir eine Transponierung darauf. Die Transponierung kehrt die Wirkung der TBN-Matrix um, so dass sie statt von Tangent Space in View Space nun von View Space in Tangent Space konvertiert wird. Es gibt einen mathematischen Grund, warum das in diesem Fall funktioniert. Dieser Trick funktioniert nicht in allen Matrizen.
Was wir mit dieser rückwärts gerichteten TBN-Matrix machen, ist die Richtung in den Lichtquellenvektor von World Space zu View Space umzuwandeln und dann die TBN-Matrix zu verwenden, um sie wieder in Tangent Space umzuwandeln.
Knifflig! Unser LightDirection-Vektor befindet sich nun im Tangent Space, dem gleichen Raum wie unsere Normal Map-Vektoren.
Jetzt werden Sie feststellen, dass der TBN Konstruktionscode oben im Shader unten auskommentiert ist. Das liegt daran, dass es einen Weg gibt, diese Berechnung etwas einfacher zu machen:
Mit unserem kniffligen LightDirection-Vektor im Tangent Space ist der Fragment-Shader also supereinfach und schnell.
View Space Normal Mapping Vertex Shader.
View Space Normal Mapping Fragment Shader.
Normal Mapping ohne vorberechnete Tangenten.
Nachdem ich das Normal Mapping von View Space einmal in Betrieb genommen hatte, war es kein Aufwand mehr, das vorberechnete, tangential-freie Normal Mapping zum Laufen zu bringen. Dies macht das Normal Mapping im World Space wie im ersten Beispiel, aber es berechnet die Tangente und Bitangente im Fragment-Shader. Ich kann keinen signifikanten visuellen Unterschied zwischen dem Ergebnis der vorberechneten Tangenten feststellen, aber ihre Leistung kann variieren.
Es gibt deutlich mehr GPU-Berechnungen in der vorberechneten, tangential-freien Implementierung, aber Sie ersparen sich die Übertragung eines 12-Byte-Vertex-Attributs auf die GPU, so dass die Wahl der Grafikkarte wirklich von ihrer Plattform und anderer Rendering-Last abhängt. Auf manchen mobilen Plattformen ist die vorberechnete, tangential-freie Implementierung offenbar deutlich langsamer. Ich werde die Tangente weiterhin offline berechnen und als Vertex-Attribut an den Vertex-Shader übergeben, da einige meiner anderen Shader die GPU bereits stark belastet haben. Ich behalte diese Implementierung jedoch für den Fall, dass ich ein Modell mit einem kleinen normal-gemappten Element habe und der Rest nicht normal gemappt ist.
Normal Mapping ohne vorberechneten Tangents Vertex Shader.
Normal Mapping ohne vorberechneten Tangents Fragment Shader.
Geheimnis gelüftet.
Ich habe die Lösung für mein Problem mit dem rotierenden Licht gefunden:
Plötzlich fing ich an, eine flache, schattige Beleuchtung zu bekommen, die wie erwartet aussah. Das Licht drehte sich nicht mehr.
Gamma und Normal Maps.
Die Normal Maps, die ich verwendete, waren PNG-Dateien. Das war die erste, die ich mit Gimp zum Debuggen erstellt habe und es war eine völlig ebene Oberfläche, die mir eine flache Schattierung geben sollte, wenn sie korrekt funktioniert. Die Zweite habe ich aus dem Internet heruntergeladen, um zu testen, ob diese vernünftig funktioniert. Es stellte sich heraus, dass beide Bilder das gleiche Problem hatten. Beide Bilder hatten einen Gamma-Wert von 2,2, der in ihren PNG-Dateien gespeichert war, aber die Daten in den Dateien waren eigentlich Gamma 1.0. Wenn OpenGL die Normal Maps auf die Grafikkarte übertrug, wandelte es sie automatisch vom SRGB-Raum in den linearen Raum um und verwandelte so alle darin enthaltenen Normals. Dies ist nicht das erste Mal, dass ich dieses Problem mit PNG-Dateien habe, also war es an der Zeit ein Tool zu erstellen. Ich habe ein kleines Dienstprogramm geschrieben, um ein PNG zu laden, den Gamma-Wert zu ändern, ohne die Daten zu verändern und dann ein neues PNG zu schreiben.