Lazarus - OpenGL 3.3 Tutorial - Beleuchtung - Einfache Beleuchtung
Inhaltsverzeichnis
Beleuchtung - Einfache Beleuchtung
Einleitung
Ohne Beleuchtung sehen die Object statisch aus, wen man noch Beleuchtung ins Spiel bringt, wirkt eine OpenGL-Scene viel realistischer.
Dabei gibt es verschiedene Arten von Beleuchtung. Die meist verwendete ist das Directional Light, dies entspricht dem Sonnenlicht.
Dieses Beispiel zeigt eine ganz einfache Variante von diesem Licht. Je steiler das Licht auf ein Polygon einstrahlt, je heller wird das Polygon.
Das man dies berechnen kann, braucht es für jede Ecke des Polygons einen Normale-Vektor.
Eine Normale zeigt meistens senkrecht auf ein Polygon.
Es gibt Ausnahmen, zB. bei runden Flächen. Dazu in einem späteren Beispiel.
Die Normalen werden mit der ModelMatrix multipliziert, da diese unbeinflusst von Perspektive/Frustum ist.
Die Position der Polygone wird mit Matrix modifiziert, da ist eine perspektifische Darstellung erwünscht.
Wen man dies nicht macht, hat man eine falsche Abdunklung auf den schrägen Dreiecken. Die macht sich besonders start bemerkbar bei Punkt und Spot-Licht.
Hier wird die einfachste Variante einer Beleuchtung gezeigt.
Dazu wird das Skalarprodukt zwischen der Normalen und der Lichtposition berechnet.
Dabei werden die Polygone dunkler, je grösser der Winkel. 0°=weiss; 180°=schwarz.
Diese Beleuchtung ist eigentlich nicht üblich, aber immerhin sieht man die Mehses viel besser.
Aber es zeigt wenigsten, wie das Grundgerüst einer Beleuchtung aussieht.
Die gebräuchlichsten Lichvarianten von OpenGL:
- Ambient-Light - Einfache Raumausleuchtung, alles ist gleich Hell.
- Directional-Light - Das Licht kommt alles aus gleicher Richtung, so wie das Sonnenlicht auf Erde.
- Point-Light - Das Licht wird von einem Punkt ausgestrahlt, so wie bei einer Glühbirne.
- Spot-Light - Das Lich hat einen Kegel, so wie wen es aus einer Taschenlampe kommt.
Was zu beachten das die Beleuchtungs-Effekte keine Schatten berücksichtigen.
Schatten muss man auf eine ganz andere weise berechnen.
Es ist auch möglich mehrere Lichtquellen zu berechnen, dazu werden alle Lichtquellen addiert.
Das sieht man gut bei den mehrfarbigen Beleuchtungbeispielen. Da sieht man auch, das das Licht farbig sein kann.
Dazu später.
Die Konstanten der Würfel-Vektoren.
const
CubeVertex: TCube =
(((-0.5, 0.5, 0.5), (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5)), ((-0.5, 0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5)),
((0.5, 0.5, 0.5), (0.5, -0.5, 0.5), (0.5, -0.5, -0.5)), ((0.5, 0.5, 0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5)),
((0.5, 0.5, -0.5), (0.5, -0.5, -0.5), (-0.5, -0.5, -0.5)), ((0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (-0.5, 0.5, -0.5)),
((-0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (-0.5, -0.5, 0.5)), ((-0.5, 0.5, -0.5), (-0.5, -0.5, 0.5), (-0.5, 0.5, 0.5)),
// oben
((0.5, 0.5, 0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5)), ((0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (-0.5, 0.5, 0.5)),
// unten
((-0.5, -0.5, 0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)), ((-0.5, -0.5, 0.5), (0.5, -0.5, -0.5), (0.5, -0.5, 0.5)));
Für die Normale wird nur eine Variable für Vektoren deklariert, da diese aus den Vektoren des Würfels berechnet werden.
Diese zeigt dann senkrecht auf das Dreieck.
var
CubeNormal: TCube;
Für die Normale braucht es noch eine VBO.
type
TVB = record
VAO,
VBOvert, // VBO für Vektor.
VBONormal: GLuint; // VBO für Normale.
end;
In der Unit Matrix hat es eine fertige Funktion, welche die Normale aus den Vertex-Koordinaten berechnet.
Diese Funktion sollte man nur verwenden, wen die Normale senkrecht auf dem Dreieck steht.
Bei runden Objekten ist dies nicht der Fall.
procedure TForm1.CreateScene;
begin
FaceToNormale(CubeVertex, CubeNormal);
Die Normale wird auf gleiche weise in den VRAM geladen, wie die Vertex-Koordinaten.
procedure TForm1.InitScene;
begin
glClearColor(0.6, 0.6, 0.4, 1.0); // Hintergrundfarbe
// --- Daten für Würfel
glBindVertexArray(VBCube.VAO);
// Vektor
glBindBuffer(GL_ARRAY_BUFFER, VBCube.VBOvert);
glBufferData(GL_ARRAY_BUFFER, sizeof(CubeVertex), @CubeVertex, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, False, 0, nil);
// Normale
glBindBuffer(GL_ARRAY_BUFFER, VBCube.VBONormal);
glBufferData(GL_ARRAY_BUFFER, sizeof(CubeNormal), @CubeNormal, GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, False, 0, nil);
end;
Hier sieht man gut, das 2 Matrizen dem Shader übergeben werden.
Bevor die Matrix mit Frustum und Worldposition beinflusst wird, wird sie das erste mal dem Shader übergeben.
Diese Matrix beinhaltet nur die lokalen Transformationen der Meshes.
Für die Position der Vektoren, wird eine komplett berechnete Matrix dem Shader übergeben.
Die Multiplikationen hätte man auch im Shader ausführen können, aber dies verbraucht nur unnötige GPU-Leistung.
Wen man es mit der CPU macht, wird die Berechnung nur einmal pro Meshes gemacht und nicht bei jedem Vektor.
Das wird in allen Beleuchtungsbeispielen so gemacht, egal ob Punkt, Directinal, etc. Beleuchtung.
Ausser bei Ambient, da es dort keine Normalen gibt.
In diesem Beispiel wird die pro Würfel gemacht. Da der Würfel mehrmals verwendet wird, gibt es pro Würfel eine Berechnung.
for x := -s to s do begin
for y := -s to s do begin
for z := -s to s do begin
Matrix.Identity;
Matrix.Translate(x * d, y * d, z * d); // Lokale Translationen.
Matrix := ModelMatrix * Matrix;
Matrix.Uniform(ModelMatrix_ID); // Erste Übergabe an den Shader.
Matrix := FrustumMatrix * WorldMatrix * Matrix; // Matrizen multiplizieren.
Matrix.Uniform(Matrix_ID); // Die komplettt berechnete Matrix übergeben.
glDrawArrays(GL_TRIANGLES, 0, Length(CubeVertex) * 3); // Zeichnet einen einzelnen Würfel.
end;
end;
end;
Einfachere Beleuchtungen macht man im Vertex-Shader.
Will man aber komplexer Beleuchtungen, nimmt man dazu den Fragment-Shader, das dieser Pixelgenau ist.
Dafür wird aber mehr Berechnugszeit benötigt.
Vertex-Shader:
Die Berechnug für das Licht des einfachen Beispieles ist hier im Vetex-Shader.
Hier sieht man, das verschiedene Matrizen für Normale und Vertex verwendet werden.
#version 330
// Die Lichtquelle befindet sich Links.
#define LightPos vec3(1.0, 0.0, 0.0)
#define PI 3.1415
layout (location = 0) in vec3 inPos; // Vertex-Koordinaten
layout (location = 1) in vec3 inNormal; // Normale
out vec4 Color; // Farbe, an Fragment-Shader übergeben.
uniform mat4 ModelMatrix; // Matrix des Modeles, ohne Einfluss von Frustum.
uniform mat4 Matrix; // Matrix für die Drehbewegung und Frustum.
float light(in vec3 p, in vec3 n) {
vec3 v1 = normalize(p); // Vektoren normalisieren, so das die Länge des Vektors immer 1.0 ist.
vec3 v2 = normalize(n); // In diesem Beispiel sind diese schon 1.0, aber in der Praxis können auch andere Werte ankommen.
float d = dot(v1, v2); // Skalarprodukt ( Winkel ) aus beiden Vektoren berechnen.
// Der Winkel ist bei 180° = Pi.
d = acos(d); // Davon noch den Arkuskosinus berechnen. Somit hat man den Winkel zwischen den beiden Vektoren.
d /= PI; // Anschliessend diesen noch durch Pi teilen, da 0° Weiss und 180° Schwarz sein soll.
return d;
}
void main(void) {
gl_Position = Matrix * vec4(inPos, 1.0); // Die komplette Berechnete Matrix.
vec3 Normal = mat3(ModelMatrix) * inNormal; // Matrix mit lokalen Tranformationen.
float col = light(LightPos, Normal); // Licht berechnen.
Color = vec4(col, col, col, 1.0);
}
Fragment-Shader
#version 330
in vec4 Color; // interpolierte Farbe vom Vertexshader
out vec4 outColor; // ausgegebene Farbe
void main(void) {
outColor = Color; // Die Ausgabe der Farbe
}
Autor: Mathias
Siehe auch
- Übersichtseite Lazarus - OpenGL 3.3 Tutorial