Möchten Sie lernen, wie Sie Ihrem 3D-Spiel Texturen, Beleuchtung, Schatten, normale Karten, leuchtende Objekte, Umgebungsokklusion und andere Effekte hinzufügen können? Großartig! Dieser Artikel enthält eine Reihe von Schattierungstechniken, mit denen Sie die Grafik Ihres Spiels auf ein neues Niveau heben können. Ich erkläre jede Technik so, dass Sie diese Informationen auf jeden Werkzeugstapel anwenden / portieren können, sei es Godot, Unity oder etwas anderes.
Als „Klebstoff“ zwischen den Shadern habe ich mich für die großartige Spiel-Engine Panda3D und OpenGL Shading Language (GLSL) entschieden. Wenn Sie denselben Stapel verwenden, erhalten Sie einen zusätzlichen Vorteil: Sie lernen, wie Sie Schattierungstechniken speziell in Panda3D und OpenGL verwenden.
Vorbereitung
Unten ist das System, mit dem ich den Beispielcode entwickelt und getestet habe.
Mittwoch
Der Beispielcode wurde in der folgenden Umgebung entwickelt und getestet:
- Linux Manjaro 4.9.135-1-MANJARO
- OpenGL-Renderer-Zeichenfolge: GeForce GTX 970 / PCIe / SSE2
- OpenGL-Versionszeichenfolge: 4.6.0 NVIDIA 410.73
- g ++ (GCC) 8.2.1 20180831
- Panda3D 1.10.1-1
Material
Jedes der zum Erstellen von
mill-scene.egg
verwendeten
Blender- Materialien
mill-scene.egg
über zwei Texturen.
Die erste Textur ist eine normale Karte, die zweite ist eine diffuse Karte. Wenn ein Objekt die Normalen seiner Scheitelpunkte verwendet, wird eine "normale blaue" Normalkarte verwendet. Aufgrund der Tatsache, dass alle Modelle dieselben Karten an denselben Positionen haben, können Shader verallgemeinert und auf den Wurzelknoten des Szenendiagramms angewendet werden.
Beachten Sie, dass das Szenendiagramm eine
Funktion der Panda3D-Engine-
Implementierung ist .
Hier ist eine einfarbige normale Karte, die nur die Farbe enthält
[red = 128, green = 128, blue = 255]
.
Diese Farbe zeigt die Einheitennormale an, die in der positiven Richtung der z-Achse
[0, 0, 1]
anzeigt.
[0, 0, 1] = [ round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ] = [128, 128, 255] = [ round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ] = [0, 0, 1]
Hier sehen wir die Einheitsnormalen
[0, 0, 1]
die in eine einfache blaue Farbe
[128, 128, 255]
, und das durchgezogene Blau, das in eine Einheitsnormalen umgewandelt wurde.
Dies wird ausführlicher im Abschnitt über normale Kartenüberlagerungstechniken beschrieben.
Panda3d
In diesem Codebeispiel wird
Panda3D als „Klebstoff“ zwischen den Shadern verwendet. Dies hat keinen Einfluss auf die unten beschriebenen Techniken. Das heißt, Sie können die hier untersuchten Informationen in jedem ausgewählten Stack oder jeder ausgewählten Spiel-Engine verwenden. Panda3D bietet bestimmte Annehmlichkeiten. In dem Artikel habe ich über sie gesprochen, sodass Sie entweder ihr Gegenstück in Ihrem Stapel finden oder sie selbst neu erstellen können, wenn sie nicht auf dem Stapel sind.
Es ist zu berücksichtigen, dass
gl-coordinate-system default
,
textures-power-2 down
und
textures-auto-power-2 1
hinzugefügt
config.prc
. Sie sind nicht in der Standardkonfiguration von
Panda3D enthalten .
Standardmäßig verwendet Panda3D ein rechtshändiges Koordinatensystem mit einer z-Achse nach oben, während OpenGL ein rechtshändiges Koordinatensystem mit einer y-Achse nach oben verwendet.
gl-coordinate-system default
können Sie Transformationen zwischen zwei Koordinatensystemen in Shadern entfernen.
textures-auto-power-2 1
ermöglicht die Verwendung von Texturgrößen, die keine Zweierpotenzen sind, wenn das System sie unterstützt.
Dies ist praktisch, wenn Sie SSAO ausführen oder andere Techniken innerhalb eines Bildschirms / Fensters implementieren, da die Bildschirm- / Fenstergröße normalerweise keine Zweierpotenz ist.
textures-power-2 down
reduziert die Größe von Texturen auf eine Zweierpotenz, wenn das System nur Texturen mit Größen unterstützt, die Zweierpotenzen entsprechen.
Beispielcode erstellen
Wenn Sie den Beispielcode ausführen möchten, müssen Sie ihn zuerst erstellen.
Panda3D läuft unter Linux, Mac und Windows.
Linux
Installieren Sie zunächst das Panda3D SDK für Ihre Distribution.
Finden Sie heraus, wo sich Panda3D-Header und -Bibliotheken befinden. Höchstwahrscheinlich befinden sie sich in
/usr/include/panda3d/
bzw. in
/usr/lib/panda3d/
.
Klonen Sie dann dieses Repository und navigieren Sie zu seinem Verzeichnis.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Kompilieren Sie nun den Quellcode in eine Ausgabedatei.
g++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-O2 \
-I/usr/include/python2.7/ \
-I/usr/include/panda3d/
Erstellen Sie nach dem Erstellen der Ausgabedatei eine ausführbare Datei, indem Sie die Ausgabedatei ihren Abhängigkeiten zuordnen.
g++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/usr/lib/panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
Weitere Informationen finden Sie im
Panda3D-Handbuch .
Mac
Beginnen Sie mit der Installation des
Panda3D SDK für Mac.
Finden Sie heraus, wo sich die Header und Bibliotheken von Panda3D befinden.
Klonen Sie dann das Repository und navigieren Sie zu seinem Verzeichnis.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Kompilieren Sie nun den Quellcode in eine Ausgabedatei. Sie müssen herausfinden, wo sich die Include-Verzeichnisse in Python 2.7 und Panda3D befinden.
clang++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-g \
-O2 \
-I/usr/include/python2.7/ \
-I/Developer/Panda3D/include/
Erstellen Sie nach dem Erstellen der Ausgabedatei eine ausführbare Datei, indem Sie die Ausgabedatei ihren Abhängigkeiten zuordnen.
Sie müssen herausfinden, wo sich die Panda3D-Bibliotheken befinden.
clang++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/Developer/Panda3D/lib \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
Weitere Informationen finden Sie im
Panda3D-Handbuch .
Windows
Beginnen Sie mit der
Installation des Panda3D SDK für Windows.
Finden Sie heraus, wo sich Panda3D-Header und -Bibliotheken befinden.
Klonen Sie dieses Repository und navigieren Sie zu seinem Verzeichnis.
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
Weitere Informationen finden Sie im
Panda3D-Handbuch .
Demo starten
Nach dem Erstellen des Beispielcodes können Sie die ausführbare Datei oder Demo ausführen. So laufen sie unter Linux oder Mac.
./3d-game-shaders-for-beginners
Und so laufen sie unter Windows:
3d-game-shaders-for-beginners.exe
Tastatursteuerung
Die Demo verfügt über eine Tastatursteuerung, mit der Sie die Kamera bewegen und den Status verschiedener Effekte ändern können.
Bewegung
w
- tief in die Szene eintauchen.a
- Drehen Sie die Szene im Uhrzeigersinn.s
- entferne dich von der Szene.d
- Drehen Sie die Szene gegen den Uhrzeigersinn.
Umschaltbare Effekte
y
- SSAO aktivieren.Shift
+ y
- SSAO deaktivieren.u
- Einbeziehung von Schaltkreisen.Shift
+ u
- Konturen deaktivieren.i
- Blüte aktivieren.Shift
+ i
- Blüte deaktivieren.o
- Aktivieren Sie normale Karten.Shift
+ o
- normale Karten deaktivieren.p
- Einschluss von Nebel.Shift
+ p
- Nebel ausschalten.h
- Einbeziehung der Schärfentiefe.Shift
+ h
- Schärfentiefe deaktivieren.j
- Posterisierung aktivieren.Shift
+ j
- Posterisierung deaktivierenk
- Pixelierung aktivieren.Shift
+ k
- Pixelisierung deaktivieren.l
- Schärfen.Shift
+ l
- Schärfe deaktivieren.n
Einschluss von Filmkorn.Shift
+ n
- Filmkorn deaktivieren.
Referenzsystem
Bevor Sie mit dem Schreiben von Shadern beginnen, müssen Sie sich mit den folgenden Referenzsystemen oder Koordinatensystemen vertraut machen. Alle von ihnen hängen davon ab, woher die aktuellen Koordinaten des Ursprungs der Referenz stammen
(0, 0, 0)
. Sobald wir es herausfinden, können wir sie mithilfe einer Matrix oder eines anderen Vektorraums transformieren. Wenn die Ausgabe eines Shaders nicht richtig aussieht, liegt die Ursache normalerweise in verwirrten Koordinatensystemen.
Modell
Das Koordinatensystem des Modells oder Objekts ist relativ zum Ursprung des Modells. In dreidimensionalen Modellierungsprogrammen, beispielsweise in Blender, wird es normalerweise in der Mitte des Modells platziert.
Die Welt
Der Weltraum ist relativ zum Ursprung der Szene / Ebene / des Universums, die Sie erstellt haben.
Rückblick
Der Koordinatenraum der Ansicht ist relativ zur Position der aktiven Kamera.
Ausschnitt
Beschneidungsraum relativ zur Mitte des Kamerarahmens. Alle darin enthaltenen Koordinaten sind homogen und liegen im Intervall
(-1, 1)
. X und y sind parallel zum Kamerafilm und die z-Koordinate ist die Tiefe.
Alle Scheitelpunkte, die nicht innerhalb der Grenzen der Sichtbarkeitspyramide oder des Sichtbarkeitsvolumens der Kamera liegen, werden abgeschnitten oder verworfen. Wir sehen, wie dies mit einem Würfel geschieht, der von der hinteren Ebene der Kamera abgeschnitten wird, und mit einem Würfel, der sich an der Seite befindet.
Bildschirm
Der Bildschirmbereich ist (normalerweise) relativ zur unteren linken Ecke des Bildschirms. X ändert sich von Null auf die Bildschirmbreite. Y wechselt von Null auf Bildschirmhöhe.
GLSL
Anstatt mit einer Pipeline fester Funktionen zu arbeiten, verwenden wir eine programmierbare GPU-Rendering-Pipeline. Da es programmierbar ist, müssen wir ihm den Programmcode in Form von Shadern übergeben. Ein Shader ist ein (normalerweise kleines) Programm, dessen Syntax der C-Sprache ähnelt. Eine programmierbare GPU-Rendering-Pipeline besteht aus verschiedenen Schritten, die mit Shadern programmiert werden können. Verschiedene Arten von Shadern umfassen Vertex-Shader, Tessellation-Shader, geometrische Shader, Fragment-Shader und Computer-Shader. Um die im Artikel beschriebenen Techniken zu verwenden, reicht es aus, Scheitelpunkt und Fragment zu verwenden
Stufen.
#version 140 void main() {}
Hier ist der minimale GLSL-Shader, bestehend aus der GLSL-Versionsnummer und der Hauptfunktion.
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
Hier ist der abgeschnittene Scheitelpunkt-Shader GLSL, der den Eingabescheitelpunkt in einen Beschneidungsraum umwandelt und diese neue Position als einheitliche Scheitelpunktposition anzeigt.
Die
main
gibt nichts zurück, da sie
void
ist und die Variable
gl_Position
die Inline-Ausgabe ist.
Zwei erwähnenswerte Schlüsselwörter sind:
uniform
und
in
.
Das
uniform
Schlüsselwort bedeutet, dass diese globale Variable für alle Scheitelpunkte gleich ist. Panda3D selbst setzt
p3d_ModelViewProjectionMatrix
und für jeden Scheitelpunkt ist es dieselbe Matrix.
Das Schlüsselwort in bedeutet, dass diese globale Variable an den Shader übergeben wird. Ein Vertex-Shader ruft jeden Vertex ab, aus dem die Geometrie besteht, an den ein Vertex-Shader angehängt ist.
#version 140 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); }
Hier ist der zugeschnittene GLSL-Fragment-Shader, der undurchsichtiges Grün als Farbe des Fragments anzeigt.
Vergessen Sie nicht, dass ein Fragment nur ein Bildschirmpixel betrifft, aber mehrere Fragmente ein Pixel betreffen können.
Achten Sie auf das Schlüsselwort out.
Das Schlüsselwort
out
bedeutet, dass diese globale Variable vom Shader festgelegt wird.
Der Name
fragColor
optional, sodass Sie einen anderen auswählen können.
Hier ist die Ausgabe der beiden oben gezeigten Shader.
Textur-Rendering
Anstatt direkt auf dem Bildschirm zu rendern / zu zeichnen, verwendet der Beispielcode eine Technik für
der Name "In Textur rendern" (In Textur rendern). Um eine Textur zu rendern, müssen Sie den Frame-Puffer konfigurieren und die Textur daran binden. Sie können mehrere Texturen an einen einzelnen Frame-Puffer binden.
Die an den Bildspeicher gebundenen Texturen speichern die vom Fragment-Shader zurückgegebenen Vektoren. Normalerweise sind diese Vektoren Farbvektoren
(r, g, b, a)
, aber sie können entweder Positionen oder normale Vektoren
(x, y, z, w)
. Für jede gebundene Textur kann ein Fragment-Shader einen separaten Vektor ausgeben. Zum Beispiel können wir in einem Durchgang die Position und Normalen des Scheitelpunkts ableiten.
Der Großteil des Beispielcodes, der mit Panda3D funktioniert, bezieht sich auf das Festlegen
der Bildspeichertexturen . Zur Vereinfachung hat jeder Fragment-Shader im Beispielcode nur eine Ausgabe. Um jedoch eine hohe Bildrate (FPS) zu gewährleisten, müssen wir in jedem Rendering-Durchgang so viele Informationen wie möglich ausgeben.
Hier sind zwei Texturstrukturen für den Bildpuffer aus dem Beispielcode.
Die erste Struktur rendert eine Wassermühlenszene unter Verwendung einer Vielzahl von Scheitelpunkt- und Fragment-Shadern in eine Bildpuffertextur. Diese Struktur verläuft mit der Mühle und entlang der entsprechenden Fragmente durch jeden der Eckpunkte der Bühne.
In dieser Struktur funktioniert der Beispielcode wie folgt.
- Speichert Geometriedaten (z. B. Position oder Scheitelpunktnormal) für die zukünftige Verwendung.
- Speichert Materialdaten (z. B. diffuse Farbe) für die zukünftige Verwendung.
- Erzeugt eine UV-Bindung verschiedener Texturen (diffuse, normale Karten, Schattenkarten usw.).
- Berechnet Umgebungslicht, diffuses, reflektiertes und emittiertes Licht.
- Macht Nebel.
Die zweite Struktur ist eine orthogonale Kamera, die auf ein Rechteck in Form eines Bildschirms gerichtet ist.
Diese Struktur durchläuft nur vier Peaks und ihre entsprechenden Fragmente.
In der zweiten Struktur führt der Beispielcode die folgenden Aktionen aus:
- Verarbeitet die Ausgabe einer anderen Bildpuffertextur.
- Kombiniert verschiedene Frame Buffer Texturen zu einer.
Im Codebeispiel sehen wir die Ausgabe einer Frame-Buffer-Textur, wobei der entsprechende Frame auf true und für alle anderen auf false gesetzt wird.
Texturierung
Texturierung ist die Bindung einer Farbe oder eines anderen Vektors an ein Fragment unter Verwendung von UV-Koordinaten. Die Werte von U und V variieren von Null bis Eins. Jeder Scheitelpunkt erhält eine UV-Koordinate und wird im Scheitelpunkt-Shader angezeigt.
Der Fragment-Shader erhält die interpolierte UV-Koordinate. Interpolation bedeutet, dass die UV-Koordinate für das Fragment irgendwo zwischen den UV-Koordinaten der Eckpunkte liegt, aus denen die Fläche des Dreiecks besteht.
Vertex-Shader
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
Hier sehen wir, dass der Vertex-Shader die Koordinate der Textur an den Fragment-Shader ausgibt. Beachten Sie, dass dies ein zweidimensionaler Vektor ist: ein Wert für U und einer für V.
Fragment Shader
#version 140 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; }
Hier sehen wir, dass der Fragment-Shader nach der Farbe in seiner UV-Koordinate sucht und diese als Farbe des Fragments anzeigt.
Bildschirmfülltextur
#version 140 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; }
Beim Rendern einer Textur ist das Netz ein flaches Rechteck mit demselben Seitenverhältnis wie der Bildschirm. Daher können wir die UV-Koordinaten nur wissend berechnen
A) die Breite und Höhe der Textur mit der Größe des Bildschirms, der dem Rechteck unter Verwendung von UV-Koordinaten überlagert ist, und
B) die x- und y-Koordinaten des Fragments.
Um x an U zu binden, müssen Sie x durch die Breite der eingehenden Textur teilen. Um y an V zu binden, müssen Sie y durch die Höhe der eingehenden Textur teilen. Sie werden sehen, dass diese Technik im Beispielcode verwendet wird.
Beleuchtung
Um die Beleuchtung zu bestimmen, müssen Aspekte der Umgebungsbeleuchtung, der diffusen, reflektierten und emittierten Beleuchtung berechnet und kombiniert werden. Der Beispielcode verwendet Phong-Beleuchtung.
Vertex-Shader
Für jede Lichtquelle mit Ausnahme des Umgebungslichts bietet Panda3D eine praktische Struktur, die sowohl für Vertex- als auch für Fragment-Shader verfügbar ist. Am bequemsten ist eine Schattenkarte und eine Matrix zum Anzeigen von Schatten, um Scheitelpunkte in einen Schatten- oder Beleuchtungsraum umzuwandeln.
Beginnend mit dem Vertex-Shader müssen wir den Vertex aus dem Betrachtungsraum in den Schatten- oder Beleuchtungsraum für jede Lichtquelle in der Szene transformieren und entfernen. Dies wird sich in Zukunft als nützlich erweisen, damit der Fragment-Shader Schatten rendern kann. Ein Schatten- oder Lichtraum ist ein Raum, in dem jede Koordinate relativ zur Position der Lichtquelle ist (der Ursprung ist die Lichtquelle).
Fragment Shader
Der Fragment-Shader übernimmt den Großteil der Beleuchtungsberechnung.
Material
Panda3D stellt uns Material (in Form einer Struktur) für das Netz oder Modell zur Verfügung, das wir gerade rendern.
Mehrere Lichtquellen
Bevor wir uns mit den Beleuchtungsquellen der Szene befassen, erstellen wir ein Laufwerk, das sowohl diffuse als auch reflektierte Farben enthält.
Jetzt können wir die Lichtquellen in einem Zyklus umgehen und die diffusen und reflektierten Farben für jede berechnen.
Beleuchtungsbezogene Vektoren
Hier sind vier Grundvektoren erforderlich, um die diffusen und reflektierten Farben zu berechnen, die von jeder Lichtquelle eingeführt werden. Der Beleuchtungsrichtungsvektor ist ein blauer Pfeil, der auf die Lichtquelle zeigt. Der normale Vektor ist ein grüner Pfeil, der vertikal nach oben zeigt. Der Reflexionsvektor ist ein blauer Pfeil, der den Richtungsvektor des Lichts widerspiegelt. Der Augen- oder Ansichtsvektor ist der orangefarbene Pfeil, der zur Kamera zeigt.
Die Beleuchtungsrichtung ist der Vektor von der Scheitelpunktposition zur Position der Lichtquelle.
Wenn dies eine gerichtete Beleuchtung ist, setzt Panda3D
p3d_LightSource[i].position.w
Null. Richtungsbeleuchtung hat keine Position, nur Richtung. Wenn dies eine gerichtete Beleuchtung ist, ist die Beleuchtungsrichtung daher eine negative oder entgegengesetzte Richtung zur Quelle, da Panda3D für eine gerichtete Beleuchtung
p3d_LightSource[i].position.xyz
auf
p3d_LightSource[i].position.xyz
.
Die Normale zum Scheitelpunkt muss ein Einheitsvektor sein. Die Einheitsvektoren haben einen Wert gleich eins.
Als nächstes brauchen wir drei weitere Vektoren.
Wir brauchen ein skalares Produkt unter Beteiligung der Beleuchtungsrichtung, daher ist es besser, es zu normalisieren. Dies gibt uns einen Abstand oder eine Größe gleich Eins (Einheitsvektor).
Die Blickrichtung ist entgegengesetzt zur Position des Scheitelpunkts / Fragments, da die Position des Scheitelpunkts / Fragments relativ zur Position der Kamera ist. Vergessen Sie nicht, dass sich die Position des Scheitelpunkts / Fragments im Betrachtungsraum befindet. Anstatt uns von der Kamera (Auge) zum Scheitelpunkt / Fragment zu bewegen, bewegen wir uns daher vom Scheitelpunkt / Fragment zur Kamera (Auge).
Der Reflexionsvektor ist eine Reflexion der Beleuchtungsrichtung senkrecht zur Oberfläche. Wenn der "Lichtstrahl" die Oberfläche berührt, wird er in demselben Winkel reflektiert, in dem er gefallen ist. Der Winkel zwischen dem Richtungsvektor der Beleuchtung und der Normalen wird als "Einfallswinkel" bezeichnet. Der Winkel zwischen dem Reflexionsvektor und der Normalen wird als "Reflexionswinkel" bezeichnet.
Sie müssen das Vorzeichen des reflektierten Lichtvektors ändern, da dieser in die gleiche Richtung wie der Vektor des Auges zeigen sollte. Vergessen Sie nicht, dass die Richtung des Auges von der Oberseite / dem Fragment zur Position der Kamera reicht. Wir werden den Reflexionsvektor verwenden, um die Helligkeit des reflektierten Lichts zu berechnen.
Diffuse Beleuchtung
Die Helligkeit der diffusen Beleuchtung ist das Skalarprodukt der Normalen zur Oberfläche und der Beleuchtungsrichtung eines einzelnen Vektors. Das Skalarprodukt kann von minus eins bis eins reichen. Wenn beide Vektoren in die gleiche Richtung zeigen, ist die Helligkeit Eins. In allen anderen Fällen wird es weniger als die Einheit sein.
Wenn sich der Beleuchtungsvektor der gleichen Richtung wie normal nähert, tendiert die Helligkeit der diffusen Beleuchtung zur Einheit.
Wenn die Helligkeit der diffusen Beleuchtung kleiner oder gleich Null ist, müssen Sie zur nächsten Lichtquelle gehen.
Jetzt können wir die diffuse Farbe berechnen, die von dieser Quelle eingeführt wird. Wenn die Helligkeit der diffusen Beleuchtung eins ist, ist die diffuse Farbe eine Mischung aus der Farbe der diffusen Textur und der Farbe der Beleuchtung.
Bei jeder anderen Helligkeit ist die diffuse Farbe dunkler.Beachten Sie, dass ich die diffuse Farbe so beschränke, dass sie nicht heller als die Farbe der diffusen Textur ist. Dies verhindert eine Überbelichtung der Szene.Reflektiertes Licht
Nach diffuser Beleuchtung wird die Reflexion berechnet.
Die Helligkeit des reflektierten Lichts ist das Skalarprodukt zwischen dem Augenvektor und dem Reflexionsvektor. Wie im Fall der Helligkeit von diffusem Licht ist die Helligkeit des reflektierten Lichts gleich Eins, wenn zwei Vektoren in die gleiche Richtung zeigen. Jede andere Helligkeit verringert die Menge der von dieser Lichtquelle eingebrachten reflektierten Farbe.Der Glanz des Materials bestimmt, wie stark die Beleuchtung des reflektierten Lichts gestreut wird. Normalerweise wird es in einem Simulationsprogramm eingestellt, zum Beispiel in Blender. In Blender wird es Spiegelhärte genannt.Scheinwerfer
Dieser Code erlaubt nicht, dass die Beleuchtung Fragmente außerhalb des Scheinwerferkegels oder der Scheinwerferpyramide beeinflusst. Glücklicherweise kann Panda3D definieren spotDirection
und spotCosCutoff
zur Arbeit mit Richtungs- und Spot - Lichtern. Scheinwerfer haben sowohl eine Position als auch eine Richtung. Richtungsbeleuchtung hat jedoch nur Richtung und Punktquellen haben nur Position. Dieser Code funktioniert jedoch für alle drei Beleuchtungsarten, ohne dass verwirrende if-Anweisungen erforderlich sind. spotCosCutoff = cosine(0.5 * spotlightLensFovAngle);
Wenn bei Projektionsbeleuchtung das Skalarprodukt des Vektors "Fragment-Beleuchtungsquelle" und der Richtungsvektor des Projektors kleiner als der Kosinus des halben Winkels des Sichtfelds desProjektors ist, berücksichtigt der Shader den Einfluss dieser Quelle nicht.Beachten Sie, dass Sie das Vorzeichen ändern müssen unitLightDirection
. unitLightDirection
geht vom Fragment zum Suchscheinwerfer, und wir müssen vom Suchscheinwerfer zum Fragment wechseln, da es spotDirection
in einem bestimmten Abstand von der Position des Suchscheinwerfers direkt zur Mitte der Pyramide des Suchscheinwerfers geht.Bei Richtungs- und Spotbeleuchtung setzt Panda3D den spotCosCutoff
Wert auf -1. Denken Sie daran, dass das Skalarprodukt im Bereich von -1 bis 1 variiert. Daher spielt es keine Rolle, was es sein wird unitLightDirectionDelta
, da es immer größer oder gleich -1 ist.
Wie der Code unitLightDirectionDelta
funktioniert auch dieser Code für alle drei Arten von Lichtquellen. Bei Scheinwerfern werden die Fragmente heller, wenn sie sich der Mitte der Scheinwerferpyramide nähern. Für Richtungs- und Punktlichtquellen spotExponent
ist Null. Denken Sie daran, dass jeder Wert mit der Potenz Null gleich Eins ist, sodass die diffuse Farbe gleich sich selbst ist, multipliziert mit Eins, das heißt, sie ändert sich nicht.Schatten
Panda3D vereinfacht die Verwendung von Schatten, da für jede Lichtquelle in der Szene eine Schattenkarte und eine Schattentransformationsmatrix erstellt werden. Um selbst eine Transformationsmatrix zu erstellen, müssen Sie eine Matrix sammeln, die die Koordinaten des Betrachtungsraums in den Beleuchtungsraum umwandelt (die Koordinaten sind relativ zur Position der Lichtquelle). Um selbst eine Schattenkarte zu erstellen, müssen Sie die Szene aus Sicht der Lichtquelle in die Bildpuffertextur rendern. Die Bildpuffertextur sollte den Abstand von der Lichtquelle zu den Fragmenten enthalten. Dies wird als "Tiefenkarte" bezeichnet. Schließlich müssen Sie Ihre hausgemachte Tiefenkarte als uniform sampler2DShadow
und die Schattentransformationsmatrix als manuell in den Shader übertragen uniform mat4
. Also werden wir neu erstellen, was Panda3D automatisch für uns tut.Es wird das gezeigte Code-Snippet verwendet textureProj
, das sich von der oben gezeigten Funktion unterscheidet texture
. textureProj
zuerst teilt vertexInShadowSpaces[i].xyz
durch vertexInShadowSpaces[i].w
. Sie verwendet es dann vertexInShadowSpaces[i].xy
, um die in der Schattenkarte gespeicherte Tiefe zu finden. Dann vertexInShadowSpaces[i].z
vergleicht sie die Tiefe der Oberseite mit der Tiefe der Schattenkarte in vertexInShadowSpaces[i].xy
. Wenn der Vergleich erfolgreich ist, wird textureProj
einer zurückgegeben. Andernfalls wird Null zurückgegeben. Null bedeutet, dass sich dieser Scheitelpunkt / Fragment im Schatten befindet, und eins bedeutet, dass sich der Scheitelpunkt / Fragment nicht im Schatten befindet.Beachten textureProj
Sie, dass abhängig von der Konfiguration der Schattenkarte auch ein Wert von null bis eins zurückgegeben werden kann. In diesem BeispieltextureProj
Führt mehrere Tiefentests basierend auf benachbarten Tiefen durch und gibt einen gewichteten Durchschnitt zurück. Dieser gewichtete Durchschnitt kann Schatten glatt machen.Dämpfung
Der Abstand zur Lichtquelle ist einfach die Größe oder Länge des Beleuchtungsrichtungsvektors. Beachten Sie, dass wir nicht die normalisierte Beleuchtungsrichtung verwenden, da ein solcher Abstand gleich Eins wäre.Der Abstand zur Lichtquelle ist erforderlich, um die Dämpfung zu berechnen. Dämpfung bedeutet, dass die Wirkung von Licht von der Quelle weg abnimmt.Parameter constantAttenuation
, linearAttenuation
und quadraticAttenuation
Sie können beliebige Werte festlegen. Es sollte mit beginnen constantAttenuation = 1
, linearAttenuation = 0
und quadraticAttenuation = 1
. Mit diesen Parametern ist sie in der Position der Lichtquelle gleich Eins und tendiert gegen Null, wenn sie sich von ihr entfernt.Endgültige Farbbeleuchtung
Um die endgültige Farbe der Beleuchtung zu berechnen, müssen Sie die diffuse und reflektierte Farbe hinzufügen. Dies muss dem Laufwerk in einem Zyklus hinzugefügt werden, in dem die Lichtquellen in der Szene umgangen werden.Umgebung
Die Umgebungslichtkomponente im Beleuchtungsmodell basiert auf der Umgebungsfarbe des Materials, der Farbe der Umgebungsbeleuchtung und der Farbe der diffusen Textur.Es sollte niemals mehr als eine Umgebungslichtquelle geben, daher sollte diese Berechnung im Gegensatz zu den Berechnungen der für jede Lichtquelle akkumulierten diffusen und reflektierten Farben nur einmal durchgeführt werden.Bitte beachten Sie, dass die Farbe des Umgebungslichts bei der Durchführung von SSAO nützlich ist.Alles zusammenfügen
Die endgültige Farbe ist die Summe aus Umgebungsfarbe, diffuser Farbe, reflektierter Farbe und emittierter Farbe.Quellcode
Normale Karten
Mit normalen Karten können Sie der Oberfläche ohne zusätzliche Geometrie neue Teile hinzufügen. Wenn Sie in einem 3D-Modellierungsprogramm arbeiten, werden normalerweise High- und Low-Poly-Versionen des Netzes erstellt. Dann werden die Normalen der Eckpunkte aus dem Hochpolynetz genommen und in die Textur eingebrannt. Diese Textur ist eine normale Karte. Dann ersetzen wir im Fragment-Shader die Normalen der Eckpunkte des Low-Poly-Netzes durch die Normalen des High-Poly-Netzes, die in die Normalkarte eingebrannt sind. Aus diesem Grund scheint es beim Beleuchten eines Netzes mehr Polygone zu geben, als es tatsächlich ist. Auf diese Weise können Sie hohe FPS beibehalten und gleichzeitig die meisten Details aus der High-Poly-Version übertragen.Hier sehen wir den Übergang von einem High-Poly-Modell zu einem Low-Poly-Modell und dann zu einem Low-Poly-Modell mit einer überlagerten normalen Karte.Vergessen Sie jedoch nicht, dass das Überlagern einer normalen Karte nur eine Illusion ist. Ab einem bestimmten Winkel sieht die Oberfläche wieder flach aus.Vertex-Shader
Beginnend mit dem Vertex-Shader müssen wir den Normalenvektor, den Binormalvektor und den Tangentenvektor an den Fragment-Shader ausgeben. Diese Vektoren werden im Fragment-Shader verwendet, um die Normalen der normalen Karte vom Tangentenraum in den Betrachtungsraum zu transformieren.p3d_NormalMatrix
konvertiert die Normalenvektoren des Scheitelpunkt-, Binormal- und Tangentenvektors in den Betrachtungsraum. Vergessen Sie nicht, dass im Betrachtungsraum alle Koordinaten relativ zur Position der Kamera sind.[p3d_NormalMatrix] sind die besten 3x3-Reverse-Transponierungselemente von ModelViewMatrix. Diese Struktur wird verwendet, um den Normalenvektor in die Koordinaten des Betrachtungsraums umzuwandeln.
Quelle
Wir müssen auch die UV-Koordinaten der normalen Karte an den Fragment-Shader ausgeben.Fragment Shader
Denken Sie daran, dass die Scheitelpunktnormalen zur Berechnung der Beleuchtung verwendet wurden. Um die Beleuchtung zu berechnen, gibt uns die normale Karte jedoch andere Normalen. Im Fragment-Shader müssen wir die Normalen der Eckpunkte durch die Normalen in der Normalkarte ersetzen.
Unter Verwendung der Koordinaten der vom Vertex-Shader übertragenen Normalkarte extrahieren wir die entsprechende Normalen aus der Karte.
Oben habe ich gezeigt, wie Normalen in Farben konvertiert werden, um normale Karten zu erstellen. Jetzt müssen wir diesen Prozess umkehren, damit wir die ursprünglichen Normalen auf die Karte bringen können. [ r, g, b] = [ r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z]
So sieht das Entpacken von Normalen von der normalen Karte aus.
Die aus der Normalkarte erhaltenen Normalen befinden sich normalerweise im Tangentenraum. Sie können sich jedoch in einem anderen Raum befinden. Mit Blender können Sie beispielsweise Normalen im Tangentenraum, Objektraum, Weltraum und Kameraraum backen.Um die Normalen der Normalkarte vom Tangentenraum in den Betrachtungsraum zu übertragen, erstellen Sie eine 3x3-Matrix, die auf dem Tangentenvektor, den binormalen Vektoren und der Scheitelpunktnormalen basiert. Multiplizieren Sie die Normalen mit dieser Matrix und normalisieren Sie sie. Hier sind wir zu den Normalen gekommen. Alle anderen Beleuchtungsberechnungen werden weiterhin durchgeführt.Quellcode