Grafik-Praktikum Game Programming - Exam
Aufgabe 1)
Du arbeitest an einem 2D-Spiel, bei dem ein Spielcharakter ein Labyrinth durchquert. Es sind verschiedene Transformationen erforderlich, um die Bewegungen und Sichtweise des Charakters darzustellen. Verwende das kartesische Koordinatensystem und die Konzepte der 2D Vektoren und Matrizen, um folgende Aufgaben zu lösen.
a)
Angenommen, der Spielcharakter befindet sich initial bei den Koordinaten (3, 4). Der Charakter bewegt sich dann um die Vektoren (2, -1), (-1, 2) und schließlich um (-3, -3). Berechne die finale Position des Charakters nach allen Bewegungen.
Lösung:
- Der Spielcharakter startet bei den Koordinaten (3, 4).
- Die erste Bewegung ist durch den Vektor (2, -1) gegeben.
- Die zweite Bewegung ist durch den Vektor (-1, 2) gegeben.
- Die dritte Bewegung ist durch den Vektor (-3, -3) gegeben.
Lass uns Schritt für Schritt die finalen Koordinaten berechnen:
- Initiale Position: (3, 4)
- Nach der ersten Bewegung:
- Addiere die Vektoren: (3, 4) + (2, -1)
- Dies ergibt: (3 + 2, 4 - 1) = (5, 3)
- Nach der zweiten Bewegung:
- Addiere die Vektoren: (5, 3) + (-1, 2)
- Dies ergibt: (5 - 1, 3 + 2) = (4, 5)
- Nach der dritten Bewegung:
- Addiere die Vektoren: (4, 5) + (-3, -3)
- Dies ergibt: (4 - 3, 5 - 3) = (1, 2)
Daher ist die finale Position des Charakters nach allen Bewegungen (1, 2).
b)
Eine Rotation ist notwendig, um den Charakter in die Richtung des Labyrinthausgangs zu drehen, der an der Position (8, 10) liegt. Bestimme die Rotationsmatrix, um den Spielcharakter um den Winkel \theta zu rotieren, wobei \theta der Winkel zwischen der positiven x-Achse und der Linie von der aktuellen Position des Charakters zum Labyrinthausgang ist.
Lösung:
Um die Rotationsmatrix zu bestimmen, die den Spielcharakter in die Richtung des Labyrinthausgangs an der Position (8, 10) dreht, müssen wir zuerst den Winkel \(\theta\) berechnen. Dieser Winkel ist der Winkel zwischen der positiven x-Achse und der Linie von der aktuellen Position des Charakters zum Labyrinthausgang.
Wir gehen Schritt für Schritt vor:
- Bestimme den Vektor von der aktuellen Position des Charakters zum Labyrinthausgang.
- Berechne den Winkel \(\theta\) mit Hilfe des Tangens.
- Ermittle die Rotationsmatrix für den Winkel \(\theta\).
1. Bestimme den Vektor
- Angenommen, die aktuelle Position des Charakters ist (1, 2) (basierend auf der vorherigen Aufgabe).
- Der Vektor vom Charakter zum Labyrinthausgang ist:
(x2 - x1, y2 - y1) = (8 - 1, 10 - 2)= (7, 8)
2. Berechne den Winkel \(\theta\)
- Der Winkel \(\theta\) kann durch den Tangens des Verhältnisses der y- und x-Komponenten des Vektors bestimmt werden:
- \( \theta = \tan^{-1} \left( \frac{y}{x}\right) \)
- Hier ist \( y = 8 \) und \( x = 7 \), also:
- \( \theta = \tan^{-1} \left( \frac{8}{7} \right) \)
3. Bestimme die Rotationsmatrix
- Die allgemeine Rotationsmatrix für einen Winkel \(\theta\) ist:
- \[ R(\theta) = \begin{pmatrix} \cos(\theta) & -\sin(\theta) \ \sin(\theta) & \cos(\theta) \end{pmatrix} \]
- Setze \(\theta = \tan^{-1} \left( \frac{8}{7}\right)\) in die Rotationsmatrix ein:
- \[ R(\theta) = \begin{pmatrix} \cos\left( \tan^{-1} \left( \frac{8}{7}\right) \right) & -\sin\left( \tan^{-1} \left( \frac{8}{7}\right) \right) \ \sin\left( \tan^{-1} \left( \frac{8}{7}\right) \right) & \cos\left( \tan^{-1} \left( \frac{8}{7}\right) \right) \end{pmatrix} \]
Damit haben wir die Rotationsmatrix, die den Spielcharakter in die Richtung des Labyrinthausgangs an der Position (8, 10) dreht.
Aufgabe 2)
Rasterung und Rendering-TechnikenUmwandlung von Vektorgrafik in Rastergrafik (Bildpunkte). Rendering erstellt das finale Bild basierend auf Szene und Beleuchtung.
- Rasterung: Diskretisierung geometrischer Daten zu Bildpunkten.
- Rendering: Erzeugt Bilder aus Szenenbeschreibungen.
- Wichtige Techniken: Raytracing, Rasterization, Radiosity.
- Raytracing: Verfolgt Lichtstrahlen, um realistische Bilder zu erzeugen.
- Rasterization: Wandelt Dreiecke in Pixel um.
- Shader: Programme, die Beleuchtung und Farbgebung bestimmen.
- Pipeline: Vertex- und Fragment-Shader-Stufen.
a)
Erkläre den Unterschied zwischen Raytracing und Rasterization. Welche Vor- und Nachteile haben diese beiden Techniken im Hinblick auf die Bildqualität und die Berechnungszeit?
Lösung:
Unterschied zwischen Raytracing und Rasterization:
- Raytracing:
- Verfolgt den Weg von Lichtstrahlen von der Kamera durch jedes Pixel auf dem Bildschirm. Diese Strahlen werden dann in der Szene verfolgt, um zu sehen, mit welchen Objekten und Oberflächen sie kollidieren. Dabei wird berechnet, wie das Licht reflektiert, gebrochen oder absorbiert wird, um realistische Schatten, Reflexionen und Transparenzen zu erzeugen.
- Vorteile:
- Erzeugt sehr realistische Bilder mit genauen Schatten, Reflexionen und Transparenzen.
- Gut geeignet für Szenen mit komplexen Lichtverhältnissen und Spiegelungen.
- Nachteile:
- Sehr rechenintensiv und zeitaufwendig aufgrund der detaillierten Lichtberechnungen.
- Benötigt mehr Rechenleistung und ist daher oft langsamer als Rasterization, besonders bei Echtzeit-Anwendungen.
- Rasterization:
- Wandelt Dreiecke und andere Primitiven in Pixel um. Dabei werden die Vertex-Positionen, Farben und Texturen direkt auf die Bildschirmfläche projiziert. Im Gegensatz zu Raytracing verfolgt Rasterization keine Lichtstrahlen, sondern verarbeitet die Geometrie der Szene direkt.
- Vorteile:
- Schneller und weniger rechenintensiv als Raytracing, da keine Lichtstrahlen verfolgt werden.
- Besser geeignet für Echtzeitanwendungen wie Videospiele, da es schnelleres Rendering ermöglicht.
- Nachteile:
- Erzeugt weniger realistische Bilder im Vergleich zu Raytracing, da es schwierig ist, genaue Schatten, Reflexionen und Transparenzen darzustellen.
- Benötigt zusätzliche Techniken (z. B. Shader, Post-Processing-Effekte), um die Bildqualität zu verbessern und näher an Raytracing heranzukommen.
b)
Du hast eine Szene mit drei Lichtquellen und einem reflektierenden Objekt. Berechne die Beleuchtung des Objekts unter der Annahme, dass du Raytracing verwendest. Berücksichtige dabei die Reflexionen und setze folgende Werte für die Reflexionskoeffizienten: Lichtquelle 1 (\textit{r}), Lichtquelle 2 (\textit{g}), Lichtquelle 3 (\textit{b}). Gib die mathematischen Schritte und Formeln an, die zur Bestimmung der finalen Farbwerte des Objekts führen.
Lösung:
Beleuchtung der Szene mit Raytracing:
Um die Beleuchtung des reflektierenden Objekts zu berechnen und dabei die Reflexionen zu berücksichtigen, müssen wir mehrere Lichtstrahlen (Rays) und ihre Interaktionen mit dem Objekt und den Lichtquellen verfolgen.
Gegeben:
- Lichtquelle 1 (\textit{r})
- Lichtquelle 2 (\textit{g})
- Lichtquelle 3 (\textit{b})
- Reflexionskoeffizienten r, g, b (für Rot, Grün und Blau)
Mathematische Schritte:
- Incidente Lichtstrahlen berechnen: Jeder Lichtstrahl trifft das Objekt unter einem bestimmten Winkel. Dabei wird ein Teil des Lichts reflektiert und ein Teil absorbiert. Wir berechnen die Intensität des Lichts, das auf das Objekt trifft, für jede Lichtquelle.
- Reflektiertes Licht berechnen: Das reflektierte Licht wird in Richtung der Kamera zurückgeworfen, wobei der Reflexionskoeffizient (r, g, b) des Objekts eine Rolle spielt. Die Intensität des reflektierten Lichts ist gegeben durch:
- \textit{R} = Intensität Lichtquelle 1 * Reflexionskoeffizient r
- \textit{G} = Intensität Lichtquelle 2 * Reflexionskoeffizient g
- \textit{B} = Intensität Lichtquelle 3 * Reflexionskoeffizient b
- Summe der Lichtintensitäten berechnen: Die gesamte Lichtintensität, die das Objekt erreicht, ist die Summe der Lichtintensitäten aller reflektierten Strahlen.
- Finale Farbwerte berechnen: Die finalen Farbwerte des Objekts sind die summierten Intensitäten der reflektierten Lichtstrahlen. Diese Werte können durch die Kamera erfasst werden und ergeben zusammen die Farbdarstellung des Objekts.
Zusammenfassung der Formeln:
Für jede Lichtquelle und ihre entsprechende Reflexionsfarbe berechnen wir:
Für Lichtquelle 1 (\textit{r}): \textit{R} = Intensität Lichtquelle 1 * Reflexionskoeffizient r
Für Lichtquelle 2 (\textit{g}): \textit{G} = Intensität Lichtquelle 2 * Reflexionskoeffizient g
Für Lichtquelle 3 (\textit{b}): \textit{B} = Intensität Lichtquelle 3 * Reflexionskoeffizient b
Endgültige Farbwerte:
Farbwert_{final}(R) = \textit{R} Farbwert_{final}(G) = \textit{G} Farbwert_{final}(B) = \textit{B}
Aufgabe 3)
Shader-Sprachen: GLSL und HLSLShader-Sprachen werden verwendet, um Grafikprozessoren (GPUs) anzusteuern, um spezialisierte Grafik- und Bildverarbeitungsaufgaben zu erfüllen.
- GLSL (OpenGL Shading Language): Hauptsächlich in OpenGL verwendet. Basiert auf der C-Programmiersprache. Dateiendungen: .vert (Vertex-Shader), .frag (Fragment-Shader).
- HLSL (High-Level Shading Language): Hauptsächlich in DirectX verwendet. Basiert auf der C-Programmiersprache. Dateiendungen: .hlsl oder .fx.
- GLSL wird in einem Quelltext direkt an die GPU gesendet, um kompiliert zu werden.
- HLSL wird vorab kompiliert und die Binärdateien werden an die GPU gesendet.
- Beide Sprachen bieten Ähnlichkeiten bei der Syntax und Funktionalität, unterscheiden sich jedoch in API-Spezifika.
- Shader-Typen in beiden Sprachen: Vertex-Shader, Fragment/Pixel-Shader, Geometry-Shader, Tessellation-Shader, Compute-Shader.
a)
Erkläre den Unterschied zwischen Vertex-Shader und Fragment-Shader in Bezug auf ihre jeweilige Funktion und wo sie in der Grafikpipeline verwendet werden.
Lösung:
- Vertex-Shader:Ein Vertex-Shader ist ein Programm, das auf jedes einzelne Vertex eines 3D-Objekts angewendet wird. Die Hauptaufgabe eines Vertex-Shaders besteht darin, die 3D-Koordinaten der Vertices in 2D-Koordinaten zu transformieren, die auf dem Bildschirm dargestellt werden können. Er ist verantwortlich für die Verarbeitung von Vertex-Attributen wie Position, Farbe und Texturkoordinaten. Der Vertex-Shader wird in der frühen Phase der Grafikpipeline ausgeführt, genauer gesagt nach dem Vertex Pulling und vor dem Primitive Assembly.Funktionen des Vertex-Shaders:
- Transformation von 3D-Koordinaten in 2D-Koordinaten
- Durchführen von Beleuchtung und Shading-Berechnungen auf Vertices
- Manipulation von Vertex-Attributen (z.B. Position, Normale, Texturkoordinaten)
- Fragment-Shader:Ein Fragment-Shader, auch bekannt als Pixel-Shader, ist ein Programm, das auf jedes Fragment (potenziellen Pixel) angewendet wird, das durch Rasterisierung erzeugt wird. Die Hauptaufgabe eines Fragment-Shaders besteht darin, die endgültige Farbe eines Pixels zu berechnen, die dann auf dem Bildschirm dargestellt wird. Er verarbeitet Fragment-Attribute wie interpolierte Werte von Vertices und Texturinformationen. Der Fragment-Shader wird in der späten Phase der Grafikpipeline ausgeführt, genauer gesagt nach dem Rasterization Step und vor dem Framebuffer Operation.Funktionen des Fragment-Shaders:
- Berechnung der Pixel-Farbe und -Tiefe
- Anwendung von Texturen
- Durchführen von Beleuchtungs- und Shading-Effekten auf Pixel-Ebene
- Manipulation der Alpha-Werte für Transparenz
Zusammenfassung:- Der Vertex-Shader verarbeitet Vertices und ist für die Transformation und Beleuchtung verantwortlich. Er wird früh in der Grafikpipeline ausgeführt.
- Der Fragment-Shader verarbeitet Fragmente (potenzielle Pixel) und berechnet die endgültigen Pixelwerte. Er wird spät in der Grafikpipeline ausgeführt.
b)
Schreibe ein einfaches GLSL-Programm für einen Vertex-Shader, der die eingehenden Vertex-Positionen unverändert durchlässt. Verwende die typischen GLSL-Syntax und -Datentypen.
Lösung:
- Hier ist ein einfaches GLSL-Programm für einen Vertex-Shader, der die eingehenden Vertex-Positionen unverändert durchlässt. Dies wird oft als 'Pass-Through' Vertex-Shader bezeichnet:
#version 330 corelayout(location = 0) in vec3 aPos; // eingehende Vertex-Positionvoid main(){ // Setzt die Ausgabe-Punkteposition gleich der Eingangs-Punkteposition gl_Position = vec4(aPos, 1.0);}
c)
Diskutiere die Vor- und Nachteile der direkten Quelltext-Kompilierung von GLSL gegenüber der Vorab-Kompilierung von HLSL in Bezug auf die Entwicklung und Ausführung von Anwendungen.
Lösung:
- Direkte Quelltext-Kompilierung von GLSL
- Vorteile:
- Flexibilität: Entwickler können Shader-Code zur Laufzeit ändern oder anpassen, was eine hohe Flexibilität bei der Entwicklung und beim Debugging bietet.
- Schnelle Iteration: Änderungen am Shader-Code werden sofort übernommen und können direkt getestet werden, was die Entwicklungszyklen verkürzt.
- Einfache Integration: Da der Quellcode direkt an die GPU gesendet wird, ist die Integration in OpenGL-basierte Anwendungen relativ unkompliziert.
- Nachteile:
- Leistungseinbußen: Die Kompilierung von Shader-Code zur Laufzeit kann zusätzliche Zeit in Anspruch nehmen, was die Startzeit der Anwendung verlängern kann.
- Kompatibilitätsprobleme: Unterschiedliche GPUs und Treiber können sich unterschiedlich verhalten, was zu unerwarteten Problemen bei der Shader-Kompilierung führen kann.
- Schwer zu debuggen: Debugging-Informationen sind möglicherweise begrenzt, was es schwieriger machen kann, Fehler im Shader-Code zu finden.
- Vorab-Kompilierung von HLSL
- Vorteile:
- Optimierte Leistung: Da Shader-Code vorab kompiliert wird, können Compiler-Optimierungen angewendet werden, was zu einer verbesserten Laufzeitleistung führt.
- Konsistenz: Vorab kompilierte Shader werden als Binärdateien ausgeliefert, was konsistente Ergebnisse auf unterschiedlichen Hardware-Plattformen gewährleistet.
- Fehlertoleranz: Fehler im Shader-Code können bereits während der Entwicklungsphase identifiziert und behoben werden, bevor die Anwendung ausgeliefert wird.
- Nachteile:
- Weniger Flexibilität: Eine Anpassung des Shader-Codes zur Laufzeit ist nicht oder nur sehr eingeschränkt möglich.
- Längere Entwicklungszyklen: Änderungen am Shader-Code erfordern eine erneute Kompilierung, was den Entwicklungszyklus verlängern kann.
- Komplexität der Toolchain: Die Notwendigkeit einer Vorab-Kompilierung erfordert eine komplexere Toolchain und Build-Prozess.
- Zusammenfassung:
- Die direkte Quelltext-Kompilierung von GLSL bietet Flexibilität und schnelle Iterationen, kann jedoch zu Leistungseinbußen und Kompatibilitätsproblemen führen.
- Die Vorab-Kompilierung von HLSL optimiert die Leistung und gewährleistet Konsistenz, geht aber zu Lasten der Flexibilität und erfordert eine komplexere Entwicklungsumgebung.
d)
Ein Compute-Shader ist ein mächtiges Werkzeug für allgemeine Berechnungen auf der GPU. Wenn man eine Compute-Shader-Anwendung schreiben möchte, um eine Matrixmultiplikation durchzuführen, welche Schritte und Überlegungen sind dabei zu beachten? Schreibe den Pseudo-Code und erkläre die Funktionsweise.
Lösung:
- Wenn man eine Compute-Shader-Anwendung zur Matrixmultiplikation schreiben möchte, sind folgende Schritte und Überlegungen zu beachten:
- Schritte und Überlegungen:
- Problemdefinition: Die Matrixmultiplikation ist eine grundlegende Operation, die zwei Matrizen A (m x n) und B (n x p) nimmt und das Ergebnis in eine Matrix C (m x p) schreibt.
- Ressourcenplanung: Bestimme die Größe der Matrizen und die notwendige Speicherzuweisung auf der GPU.
- Compute-Shader erstellen: Schreibe den Compute-Shader-Code, der die Matrixmultiplikation durchführt.
- Shader-Kommunikation: Richte die notwendigen Uniforms und Shader Storage Buffer Objects (SSBOs) ein, um die Matrizen A, B und C zu speichern und darauf zuzugreifen.
- Shader-Ausführung: Bestimme die Anzahl der Workgroups und Dispatch-Parameter, um sicherzustellen, dass alle Elemente der Ergebnis-Matrix C berechnet werden.
- Ergebnisverarbeitung: Lese die Ergebnisdaten zurück auf die CPU, falls erforderlich.
- Pseudo-Code für Compute-Shader zur Matrixmultiplikation:
Shader Code (GLSL-Syntax):#version 430// Größe der Matrizen als Uniforms layout(location = 0) uniform int m; // Anzahl der Zeilen von A und C layout(location = 1) uniform int n; // Anzahl der Spalten von A und Zeilen von B layout(location = 2) uniform int p; // Anzahl der Spalten von B und C // Eingabematrizen als SSBOs layout(std430, binding = 0) buffer MatrixA { float A[]; // m x n }; layout(std430, binding = 1) buffer MatrixB { float B[]; // n x p }; // Ausgabematrix als SSBO layout(std430, binding = 2) buffer MatrixC { float C[]; // m x p }; // Compute-Shader main() Funktion void main() { // Berechnen des aktuellen Workgroup Threads' Zeile und Spalte uint row = gl_GlobalInvocationID.x; uint col = gl_GlobalInvocationID.y; float value = 0.0; // Matrix-Multiplikation durchführen for (uint k = 0; k < n; k++) { value += A[row * n + k] * B[k * p + col]; } // Ergebnis in Matrix C speichern C[row * p + col] = value; }
- Erklärung der Funktionsweise:
- Initialisierung: Während der Initialisierungsphase werden die Uniforms und SSBOs mit den Matrizen A, B und C sowie deren Dimensionen gefüllt.
- Workgroup-Aufteilung: Jede Workgroup-Thread (Invoke) ist für die Berechnung eines Elements in der Ergebnis-Matrix C verantwortlich. Die Anzahl der Workgroups und ihre Größe werden so gewählt, dass sie alle Elemente der Matrix C abdecken.
- Schleifenoperation: Jede Schleifeniteration multipliziert und akkumuliert die entsprechenden Elemente von A und B, um ein Element von C zu berechnen.
- Ergebnis: Sobald alle Workgroups abgeschlossen sind, enthält die Ergebnis-Matrix C die vollständige Matrix-Multiplikation von A und B.
Aufgabe 4)
In einem 3D-Spiel, das Du entwickelst, willst Du realistische Beleuchtung und Schatten für eine Szene umsetzen, die verschiedene Arten von Lichtquellen und Oberflächen enthält. Die Szene enthält ein Punktlicht, ein gerichtetes Licht und ein Umgebungslicht. Die Oberflächen in der Szene haben unterschiedliche Materialien, die sowohl diffuse als auch spekulare Reflexionen zeigen. Zudem soll für eine Konsistenz im Spiel sowohl Self-Shadowing als auch der Schattenwurf von Objekten berücksichtigt werden.
a)
Implementiere mit Hilfe des Phong-Beleuchtungsmodells die Berechnung der Beleuchtung für ein gegebenes Fragment in der Szene. Stelle sicher, dass Du die diffuse, spekulare und Umgebungslichtkomponenten für Punktlicht, gerichtetes Licht und Umgebungslicht in deinen Berechnungen berücksichtigst. Nutze folgende Parameter:
- Normalenvektor des Fragments: \mathbf{N}
- Vektoren zu den Lichtquellen: \mathbf{L_p} (Punktlicht), \mathbf{L_d} (gerichtetes Licht)
- Blickrichtungsvektor: \mathbf{V}
- Reflexionskoeffizienten: \(k_d, k_s, k_a\) (diffus, spekular, Umgebung)
- Spekularitätskonstante: \(n\)
- Intensitäten der Lichtquellen: \(I_{p}, I_{d}, I_{a}\) (Punktlicht, gerichtetes Licht, Umgebung)
Berechne die Intensität des Lichts am Fragment mit den angegebenen Parametern und gebe Deine Berechnungen in einer strukturierten Weise aus.
// Implementierung hier
Lösung:
Um die Beleuchtung eines Fragments in der Szene unter Verwendung des Phong-Beleuchtungsmodells zu berechnen, müssen die diffusen, spekulären und Umgebungslichtkomponenten für die verschiedenen Lichtquellen (Punktlicht, gerichtetes Licht und Umgebungslicht) berücksichtigt werden. Hier sind die Details:
- Normalenvektor des Fragments: \( \mathbf{N} \)
- Vektoren zu den Lichtquellen: \( \mathbf{L_p} \) (Punktlicht) und \( \mathbf{L_d} \) (gerichtetes Licht)
- Blickrichtungsvektor: \( \mathbf{V} \)
- Reflexionskoeffizienten: \( k_d, k_s, k_a \) (diffuse, spekulare und Umgebungslichtkoeffizienten)
- Spekularitätskonstante: \( n \)
- Intensitäten der Lichtquellen: \( I_p, I_d, I_a \) (Punktlicht, gerichtetes Licht, Umgebung)
Die allgemeine Formel für die Lichtintensität \( I \) an einem Fragment lautet:
I = k_a * I_a + (k_d * max(0, \mathbf{N} \cdot \mathbf{L}) * I + k_s * max(0, \mathbf{R} \cdot \mathbf{V})^n * I)
wobei \( \mathbf{R} \) der Reflexionsvektor ist, der berechnet wird als:
\mathbf{R} = 2 * (\mathbf{N} \cdot \mathbf{L}) * \mathbf{N} - \mathbf{L}
Um diese Gleichungen umzusetzen, können wir eine Funktion in Python schreiben:
import numpy as npdef phong_illumination(N, Lp, Ld, V, kd, ks, ka, n, Ip, Id, Ia): # Normalisieren der Eingangsvektoren N = N / np.linalg.norm(N) Lp = Lp / np.linalg.norm(Lp) Ld = Ld / np.linalg.norm(Ld) V = V / np.linalg.norm(V) # Berechnung der Reflexionsvektoren Rp = 2 * np.dot(N, Lp) * N - Lp Rd = 2 * np.dot(N, Ld) * N - Ld # Diffuse Komponenten diffuse_p = kd * max(0, np.dot(N, Lp)) * Ip diffuse_d = kd * max(0, np.dot(N, Ld)) * Id # Spekulare Komponenten specular_p = ks * max(0, np.dot(Rp, V)) ** n * Ip specular_d = ks * max(0, np.dot(Rd, V)) ** n * Id # Umgebungslichtkomponente ambient = ka * Ia # Gesamthelligkeit berechnen I = ambient + diffuse_p + diffuse_d + specular_p + specular_d return I# Beispiel für die Verwendung der FunktionN = np.array([0, 0, 1])Lp = np.array([1, 1, 1]) / np.linalg.norm(np.array([1, 1, 1]))Ld = np.array([0, 0, -1])V = np.array([0, 0, -1])kd = 0.8ks = 0.5ka = 0.1n = 32Ip = 1.0Id = 1.0Ia = 0.2intensity = phong_illumination(N, Lp, Ld, V, kd, ks, ka, n, Ip, Id, Ia)print(f'Berechnete Lichtintensität: {intensity:.3f}')
In dieser Funktion wird die Phong-Beleuchtung für das gegebene Fragment unter Berücksichtigung der angegebenen Parameter berechnet. Die Intensität des beleuchteten Fragments wird ausgegeben.
b)
Erläutere die Methode des Shadow Mappings zur Berechnung von Schatten und diskutiere ihre Vor- und Nachteile. Implementiere ein einfaches Shadow Mapping für eine Szene mit einem Punktlicht. In die Implementierung soll eine Möglichkeit eingebaut werden, Self-Shadowing korrekt zu behandeln.
// Implementierung hier
Lösung:
Shadow Mapping ist eine weit verbreitete Technik zur Berechnung von Schatten in 3D-Szenen. Dabei wird aus der Sicht der Lichtquelle eine Tiefenkarte (Shadow Map) erstellt, die den Abstand der nächstgelegenen Objekte zur Lichtquelle speichert. Diese Tiefenkarte wird dann verwendet, um zu bestimmen, ob ein Punkt in der Szene im Schatten liegt oder nicht.
Methode des Shadow Mappings:
- Rendern der Szene aus der Sicht der Lichtquelle und Speichern der Tiefenwerte aller sichtbaren Pixel in einer Tiefenkarte (Shadow Map).
- Im normalen Rendering-Pass wird für jeden Pixel die Entfernung zur Lichtquelle berechnet und mit dem entsprechenden Wert in der Shadow Map verglichen.
- Liegt der Wert des Pixels hinter dem tiefsten Punkt aus der Shadow Map (d.h., weiter entfernt von der Lichtquelle), so ist der Pixel im Schatten, ansonsten ist er beleuchtet.
Vorteile:
- Einfache Implementierung und weit verbreitete Nutzung.
- Kann Schatten für jede Art von Geometrie erzeugen.
Nachteile:
- Aliasing-Probleme (Blockartefakte) und die Notwendigkeit von Filtertechniken, um sie zu mindern.
- Probleme mit Self-Shadowing (Acne-Problem).
- Benötigt zusätzlichen Speicher für die Shadow Map.
Implementierung eines einfachen Shadow Mappings für eine Szene mit einem Punktlicht und Behandlung von Self-Shadowing:
import numpy as np# Beispielhaftes Pseudo-Code zur Veranschaulichung# Erstellen einer Tiefenkarte aus der Sicht des Punktlichtsdef create_shadow_map(scene, light_pos): shadow_map = np.zeros((shadow_map_width, shadow_map_height)) light_view_matrix = get_light_view_matrix(light_pos) light_projection_matrix = get_light_projection_matrix() for obj in scene: for vertex in obj.vertices: light_space_pos = light_projection_matrix @ light_view_matrix @ vertex depth = light_space_pos.z shadow_map[light_space_pos.x, light_space_pos.y] = depth return shadow_map# Rendern der Szene mit Shadow Mappingdef render_scene_with_shadows(scene, shadow_map, light_pos): light_view_matrix = get_light_view_matrix(light_pos) light_projection_matrix = get_light_projection_matrix() for obj in scene: for vertex in obj.vertices: world_pos = vertex view_pos = view_matrix @ world_pos light_space_pos = light_projection_matrix @ light_view_matrix @ world_pos current_depth = light_space_pos.z shadow_map_depth = shadow_map[light_space_pos.x, light_space_pos.y] bias = 0.005 # Bias zur Behandlung von Self-Shadowing if current_depth > shadow_map_depth + bias: # Pixel ist im Schatten pixel_color = obj.shadow_color else: # Pixel ist beleuchtet pixel_color = obj.color set_pixel(view_pos.x, view_pos.y, pixel_color)# Anwendung der Methoden example_scene = load_scene()light_position = np.array([10, 10, 10])shadow_map = create_shadow_map(example_scene, light_position)render_scene_with_shadows(example_scene, shadow_map, light_position)
In dieser Pseudo-Code-Implementierung wird zunächst eine Tiefenkarte aus der Sicht des Punktlichts erstellt. Anschließend wird die Szene gerendert, wobei überprüft wird, ob ein Pixel im Schatten liegt, indem der aktuelle Tiefenwert mit dem Wert in der Shadow Map verglichen wird. Ein Bias wird hinzugefügt, um das Self-Shadowing-Problem zu minimieren.