Shader
Per Frame
Pipeline Draw-Call
Eingabestreams definieren
Draw Call
Streamdefinitionen wiederverwenden
Daten an die GPU senden

Double Buffering ist ein Konzept aus dem Rendering, bei dem mit 2 Framebuffern gearbeitet wird.

Dabei kommt das aktuelle Bild, das der Monitor zeigt immer aus dem Front Buffer. Währenddessen kann das nächste Bild im Back Buffer gezeichnet werden. Um das fertig gezeichnete Bild schließlich anzuzeigen, wird ein Buffer Swap ausgeführt, der die beiden Puffer tauscht.

Das verhindert Flackern und halbfertige Bilder.

Shader unbinden

  • [[glUseProgram]]

Buffer Swap

Context::swap_buffers();

Die Baryzentrische Inteprolation ist eine Interpolation auf einem Dreieck, also zwischen 3 Koordinaten.

Gegeben sind die Werte , , an den Dreieckspunkten, zwischen denen interpoliert werden soll und die baryzentrischen Koordinaten , an denen interpoliert wird.

glGetShaderiv - Gibt Informationen über den Shader zurück

void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
  • GLuint shader - Der Shader, über den Informationen abgefragt werden sollen
  • GLenum pname - Das Parameter, das abgefragt werden soll, z.B.:
    • GL_COMPILE_STATUS - Frägt ab, ob der Shader erfolgreich kompiliert wurde
  • GLint *params - Adresse, an die die Informationen geschrieben werden sollen

Beispiel

Um zu checken, ob ein Shader korrekt kompiliert wurde:

GLint result = GL_FALSE;
glGetShaderiv(vert_shader, GL_COMPILE_STATUS, &result);
 
// ist result nun false, hat der shader nicht kompiliert.

In OpenGL kann man Vertex Shader und Fragment Shader definieren. Diese werden im Setup zu einem Shader Programm zusammengefasst, welches beim Draw-Call in der Render-Pipeline automatisch ausgeführt wird. Dafür müssen einige Sachen vorbereitet werden:

  1. Shader Program erstellen mit glCreateProgram
  2. Für jeden Shader:
    1. Shader erstellen mit glCreateShader
    2. Shader-Quellcode in den Shader laden mit glShaderSource
    3. Shader kompilieren mit glCompileShader
    4. Shader in das Shader Program einbinden mit glAttachShader
  3. Shader Program linken mit glLinkProgram

Erst dann ist das Shader Program bereit genutzt zu werden:

Zum Löschen:

Löschen

  • Schlägt das Kompilieren fehl sollten die Shader und Programme gelöscht werden.
  • Shader können auch direkt nach dem erfolgreichem Linken gelöscht werden. Davor sollten sie noch mit glDetachShader vom Programm gelöst werden.
  • Das Shader Programm sollte dann natürlich erst gelöscht werden, wenn es nicht mehr zum Rendern benötigt wird.

Beispiel

// 1. Shader Program erstellen
GLuint shader_prog = glCreateProgram();
 
// 2.1. Shader erstellen
GLuint vert_shader = glCreateShader(GL_VERTEX_SHADER);
 
// 2.2. Shader-Quellcode in den Shader laden
const char *vert_source_cstr; // Sei hier mal Definiert und Nullterminiert
glShaderSource(vert_shader, 1, &vert_source_cstr, 0);
 
// 2.3. Shader kompilieren
glCompileShader(vert_shader);
 
// Check ob es Kompilierfehler gab
GLint result = GL_FALSE;
glGetShaderiv(vert_shader, GL_COMPILE_STATUS, &result);
 
if (result != GL_TRUE) {
	cerr << "Failed to compile shader" << endl;
	// Aufräumen
	glDeleteShader(vert_shader);
	glDeleteProgram(shader_prog);
	shader_prog = -1;
	exit(-1);
}
 
// 2.4. Shader an das Program anheften
glAttachShader(shader_prog, vert_shader);
 
// (2.1. bis 2.4. analog nochmal für den Fragment Shader)
 
// 3. Shader Program linken
 
glLinkProgram(shader_prog);
 
// Check auf Linkerfehler
result = GL_FALSE;
glGetProgramiv(shader_prog, GL_LINK_STATUS, &result);
if (result != GL_TRUE) {
	cerr << "Failed to link shaders" << endl;
	// Aufräumen
	glDeleteShader(vert_shader);
	glDeleteShader(frag_shader);
	glDeleteProgram(shader_prog);
	shader_prog = -1;
	exit(-1);
}
 
// Alles lief durch! Wir können den Shader jetzt verwenden:
glUseProgram(shader_prog);

Referenz

glCreateShader - Erstellt einen Shader (also einen Teil eines Shader Programms).

GLuint glCreateShader(GLenum shader_type);
  • GLenum shader_type - Der Typ des Shaders, z.B.:

Rückgabewert: GLenum - Eine Adresse über die der Shader referenziert werden kann.

glShaderSource - Weist einem Shader Quellcode zu.

void glShaderSource(
	GLuint shader, 
	GLsizei count, 
	const GLchar **string,
	const GLint *length
);
  • GLuint shader - Das Shader Objekt, generiert mit glCreateShader
  • GLsizei count - Wie viele Strings (“dateien”) übergeben werden sollen
  • const GLchar **string - Array an GLSL Shader strings
  • const GLint *length - Länge derShader strings. Kann man auf 0 setzen, um anzugeben dass die Strings Nullterminiert sind.

glUseProgram - Bindet ein Shader Programm

void glUseProgram(GLuint program);
  • GLuint program -

glAttachShader - Verbindet einen Shader mit einem Shader Programm

void glAttachShader(GLuint program, GLuint shader);

glCompileShader - Kompiliert einen Shader.

void glCompileShader(GLuint shader);
  • GLuint shader - Shader, der kompiliert werden soll

Draw Call

  • [[glDrawArrays]]
  • [[glDrawElements]]

Rasterisierung

Der Fragment Shader ist Teil der OpenGL Draw-Pipeline und wird für jedes Fragment aufgerufen. Er berechnet typischerweise:

  • Die Farbe des aktuellen Fragments und damit eines Pixels

Arbeitsweise

  • Vom Input gl_FragCoord kommt die Pixelposition (x, y) + Tiefe (z) des Fragments
  • In gl_FragDepth kann ein neuer Z-Buffer-Wert geschrieben werden
  • Der erste andere Output ist die Farbe, die auf dem Bildschirm landet

Beispiel

Dieser Fragment Shader bekommt die interpolierte Farbe vom Rasterizer und gibt sie anschließend aus:

in vec3 frag_col;
out vec4 out_color;
 
void main() {
	out_color = vec4(frag_col, 1);
}
 

Per-Fragment-Ops

Start

Shader Laden

  • [[glCreateProgram]]
  • [[glCreateShader]]
    • [[glShaderSource]]
    • [[glCompileShader]]
  • [[glAttachShader]]
  • [[glBindAttribLocation]]
  • [[glLinkProgram]]
  • [[glUseProgram]]

glBufferData - Sendet Daten an einen Buffer Binding Point.

void glBufferData (
	GLenum target, 
	GLsizei size, 
	const void *data, 
	GLenum usage
);
  • GLenum target - Der Zielport.
  • GLsizei size - Die Größe der zu sendenden Daten in Bytes.
  • const void *data - Zeiger auf die zu sendenden Daten.
  • GLenum usage - Hint an OpenGL, wie die Daten voraussichtlich verarbeitet werden, z.B.:
    • GL_STATIC_DRAW - Daten werden einmal auf die GPU hochgeladen und dann in jedem Frame gezeichnet DRAW
    • … (andere wenig relevant für CG)

Beispiel

glBufferData (
	GL_ARRAY_BUFFER,
	vertices.size() * sizeof(vec3),
	&vertices[0],
	GL_STATIC_DRAW
);

Daten an die GPU senden

  • [[glGenBuffers]]
  • [[glBindBuffer]]
  • [[glBufferData]]

glBindBuffer - Bindet einen Buffer Object an einen Buffer Binding Point.

void glBindBuffer(GLenum target, GLuint buffer)
  • GLenum target - Der Zielport. Beispiel:
    • GL_ARRAY_BUFFER - Zum Speichern von Vertex Arrays
    • GL_ELEMENT_ARRAY_BUFFER - Zum Speichern von Indizes (von z.B. Indexed Face Sets)
  • GLuint buffer - Die Adresse des Buffers.

Beispiel

// Buffer an den Port binden
glBindBuffer(GL_ARRAY_BUFFER , vbo);
 
// Jetzt können Daten an den Port geschickt werden, die dann im Buffer landen
 
// Port wieder entbinden
glBindBuffer(GL_ARRAY_BUFFER , 0);

Viewport

Vertex-Fetch

Primitive Assembly

Hier erst kommt der Index Buffer rein!

  • [[Indexed Face Set]]

Clipping

Fixed Function Vertex Processing

  • [[Perspektivisch korrekte Interpolation]]

Der Vertex Shader ist Teil der OpenGL Draw-Pipeline und wird für jeden Vertex aufgerufen. Üblicherweise wird hier berechnet:

  • Transformationen der Model-Verticies durch die verschiedenen Spaces

Ein Vertex Shader muss mindestens die Clip Space-Position des aktuellen Vertex in gl_Position zurück geben.

Arbeitsweise

  • Vom Input gl_VertexID kommt der Index des aktuellen Vertex
  • In den Output gl_Position kommt die Clip-Space Position des aktuellen Vertex
  • Andere Inputs mit können mit Input-Streams verbunden werden mit glBindAttribLocation

Interpolationsart einstellen

Alle Outputs werden an den Rasterisierer gegeben, der diese dann Interpoliert und an den Fragment Shader weitergibt.

Man kann die Art und Weise, wie interpoliert wird steuern, indem man vor out ein Keyword schreibt:

  • <Kein Keyword> - Interpoliert durch Baryzentrische Interpolation
  • flat - Kein Interpolieren, Wert kommt vom “provoking vertex” (meist erster Vertex des Dreiecks)
  • noperspective Interpolation durch Lineare Interpolation, ohne Perspektivkorrektur

Beispiel:

flat out vec3 frag_col; 

Beispiel

Dieser Vertex Shader transformiert einen Vertex vom Eyespace in den Clip Space und gibt die Farbe weiter an den Fragment Shader:

in vec3 local_vertex;
in vec3 color;
 
out vec3 frag_col;
 
uniform mat4 view;
uniform mat4 proj;
 
void main() {
	gl_Position = proj * view * vec4(local_vertex, 1.0);
	frag_col = color;
}
 

Framebuffer leeren

  • [[glClear]]
  • [[glClearColor]]

Shader binden

  • [[glUseProgram]]

glClear - Leert den aktuell gebundenen Framebuffer.

void glClear(GLbitfield mask);
  • GLbitfield mask - Welche Buffer geleert werden sollen. Optionen:
    • GL_COLOR_BUFFER_BIT - Der aktuelle Farbbuffer
    • GL_DEPTH_BUFFER_BIT - Der Depth Buffer
    • (GL_STENCIL_BUFFER_BIT - Der Stencil Buffer)

In OpenGL können wir Uniforms verwenden, um Daten für Shader bereit zu stellen, die über den gesamten Draw Call gleich sind. Kandidaten hiefür sind:

Uniforms laden

  1. Zuerst definieren wir Uniforms im Shader mit dem Schlüsselwort uniform:
uniform mat4 myUniformMatrix;
  1. Anschließend den Shader Laden
  2. Pro Frame dann das Shader Programm aktivieren
  3. Anschließend können wir mit glGetUniformLocation den Index der Variable auslesen
  4. An diesen Index senden wir dann die Uniform-Daten, z.B mit glUniform

Referenz

In OpenGL besteht die Möglichkeit, mehrere Attribute pro Vertex zu verarbeiten. Um ein Attribut von einem Buffer in den Shader zu streamen, muss zuerst wieder das Buffer Object an einen Binding Point gebunden werden. Anschließend kann ein Input-Stream definiert werden, der dann implizit von dem gebundenen Buffer liest. Danach kann der gebundene Buffer wieder gelöst werden. In der Praxis geht das so:

Falls wir mit Indexed Face Sets arbeiten, müssen wir dieses noch an den Binding Point GL_ELEMENT_ARRAY_BUFFER hängen. Dadurch ist in OpenGL automatisch der Index-Stream definiert und die Primitive werden später richtig konstruiert.

Beispiel

// vbo1 als ersten Shader-Input definieren
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
 
// vbo2 als zweiten Shader-Input definieren
glBindBuffer(GL_ARRAY_BUFFER, vbo2);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);

Referenz

Streamdefinitionen wiederverwenden

  • [[glGenVertexArrays]]
  • [[glBindVertexArray]]

glVertexAttribPointer - Aktiviert einen Input-Stream für den Vertex Shader.

void glEnableVertexAttribArray(GLuint index);
  • GLuint index - Der Index des Streams

glVertexAttribPointer - Definiert einen Input Stream für den Vertex Shader. Als Stream-Quelle wird dabei der Buffer verwendet, der zur Zeit des Aufrufs an GL_ARRAY_BUFFER hängt.

void glVertexAttribPointer (
	GLuint index,
	GLint size,
	GLenum type,
	GLboolean normalized,
	GLsizei stride,
	const void * data
);
  • GLuint index - Der Index des Streams
  • GLint size - Komponenten per Vertex
  • (GLenum type - Gibt den gespeicherten Datentyp an, meistens `GL_FLOAT)
  • (GLboolean normalized - Soll Normalisierung angewendet werden, meistens GL_FALSE)
  • (GLsizei stride - Lücke zwischen zwei Elementen in Bytes)
  • const void * data - Start-Offset in Bytes

Beispiel

// 
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

Wir haben gesehen, wie man Vertex- und Index-Daten in GPU-Buffer schreibt und anschließend Streams an den Vertex-Shader definiert. Jetzt können wir die Rendering-Pipeline in Gang setzen. Das geht so:

Den Großteil (Clipping, Rasterisieren, …) erledigt OpenGL von selbst, allerdings können wir uns mit Shadern in bestimmten Abschnitten in bestimmte Phasen einklinken:

Beispiel

// vbo1 als ersten Shader-Input definieren
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
 
// vbo2 als zweiten Shader-Input definieren
glBindBuffer(GL_ARRAY_BUFFER, vbo2);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
 
// ibo als Indizies nochmal binden, sofern nicht schon gemacht
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
 
// Draw Call!
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
 
// Bindungen lösen
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);

Referenz

glDrawArrays - Rendert Primitive aus Verticies, die direkt aus dem Vertex Array kommen.

void glDrawArrays(GLenum mode, GLint first, GLsizei count);
  • GLenum mode - Primitiver Typ zum Rendern
  • GLint first - Erster Vertex der genutzt werden soll
  • GLsizei count - Anzahl an Verticies die genutzt werden sollen

glDrawElements - Rendert Primitive aus Verticies, definiert durch ihre Indizies.

void glDrawElements (
	GLenum mode,
	GLsizei count,
	GLenum type,
	const void *indices
);
  • GLenum mode - Primitiv zu Rendern, z.B.:
    • GL_TRIANGLES - Dreiecke
    • GL_TRIANGLE_STRIP - Dreiecks-Strip. Dabei werden neue Punkte mit den zwei letzten Punkten aus dem vorherigem Dreieck verbunden. Eignet sich gut für zusammenhängende Flächen.
    • GL_QUADS- Quadrate
  • GLsizei count - Anzahl der Verticies die genutzt werden sollen
  • GLenum type - Typ des Index Buffers
  • const void *indices - Offset des Index-Pointers

Uniforms hochladen

  • [[glGetUniformLocation]]
  • [[glUniform]]

Eingabestreams definieren

  • [[glEnableVertexAttribArray]]
  • [[glVertexAttribPointer]]

glGetProgramiv - Gibt die Informationen eines Shader Programm zurück

void glGetProgramiv(GLuint program, GLenum param_name, GLint *values );

Damit OpenGL auf der GPU rechnen kann, müssen wir alle nötigen Daten (z.B. die Positionen der einzelnen Vertices oder das Indexed Face Set) bereitstellen. Die Daten speichern wir in einem Buffer Object. Das ist einfach Speicher auf der GPU. Auf den haben wir allerdings keinen direkten Zugriff, sondern nur über einen Buffer Binding Point. Diesen Binding Point können wir mit dem Buffer verbinden, um dann Daten über den Zielport an den Buffer zu speichern. Konkret geht das so:

  1. Buffer auf der GPU erstellen mit glGenBuffers
  2. Den Buffer an ein Ziel binden mit glBindBuffer
  3. Die Daten über Ziel an gebundenen Buffer senden mit glBufferData

Beispiel

// Die seien hier mal schon definiert
vector<vec3> vertices;
vector<unsigned> indices;
 
 
// 1. Buffer anlegen
GLuint vbo;
GLuint ibo;
glGenBuffers(1, &vbo);
glGenBuffers(1, &ibo);
 
// 2. Buffer an ein Ziel binden
glBind(GL_ARRAY_BUFFER, vbo); 
 
// 3. Daten über Ziel an Buffer senden
glBufferData(
	GL_ARRAY_BUFFER, 
	vertices.size() * sizeof(vec3), 
	&vertices[0], 
	GL_STATIC_DRAW
); 
 
// Bindung lösen
glBind(GL_ARRAY_BUFFER, 0); 
 
// Analog für den Index Buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(
	GL_ELEMENT_ARRAY_BUFFER, 
	indices.size() * sizeof(unsigned), 
	&indices[0], 
	GL_STATIC_DRAW
);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // Bindung lösen

Referenz

glGenBuffers - Erstellt ein Buffer Object auf der GPU.

void glGenBuffers(GLsizei n, GLuint *buffers);
  • GLsizei n - Die Anzahl der Buffer
  • GLuint *buffers - Eine Variable in die die GPU-Speicheradresse des Buffers geschrieben wird

Beispiel

GLuint vbo ;
glGenBuffers(1 , &vbo);
 
// Jetzt zeigt *vbo auf den Buffer

Problemstellung

Wir haben gesehen, wie man in OpenGL rasterisiert. Dieses Vorgehen funktioniert gut wenn man nur wenige Meshes hat. Das Problem: Tatsächlich arbeitet man ja mit einigen hunderten Meshes. Und da jeder dieser Aufrufe ein Call der CPU auf die GPU, ist, dauert das. Oft ist dann nicht mehr die GPU das Bottleneck sondern die CPU, die extrem viele Calls an die GPU senden muss.

Lösung

In OpenGL löst man dieses Problem mit sogenannten Vertex Array Objects. Ein VAO kann man sich als “Recorder” vorstellen, das solange es gebunden ist aufzeichnet, welche Vertex-Input-Streams definiert werden. Dadurch müssen beim Draw-Call die einzelnen Vertex-Inputs nicht jedes mal neu definiert werden, sondern lediglich der VAO gebunden. Der lädt die Konfiguration dann automatisch. In der Praxis sieht das dann so aus:

Initial einmal:

  1. VAO generien mit glGenVertexArrays
  2. VAO binden mit glBindVertexArray
  3. Definieren der Streams wie wir es bereits gesehen haben
  4. VAO unbinden
  5. Buffer unbinden

Für den Draw-Call dann nur noch:

  1. VAO binden
  2. Den Draw-Call ausführen wie wir es bereits gesehen haben
  3. VAO unbinden

Beispiel

Initial einmal:

GLUint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);        // Ab hier wird aufgezeichnet
 
	glBindBuffer(GL_ARRAY_BUFFER, vbo1);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
	
	glBindBuffer(GL_ARRAY_BUFFER, vbo2);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
	
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
	
glBindVertexArray(0);          // Aufzeichnung beendet
 
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);

Draw-Call:

glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);

Referenz

OpenGL Piping Data.svg

glGenVertexArrays - Generiert ein Vertex Array Object

void glGenVertexArrays(GLsizei n, GLuint *vaos);
  • GLsizei n - Anzahl der zu generierenden VAOs
  • GLuint *vaos - Array in das die generierten VAO Adressen gespeichert werden

glBindVertexArray - Bindet ein Vertex Array Object

void glBindVertexArray(GLuint array)
  • GLuint array - Adresse des zu bindenden VAO

glDeleteShader - Löscht einen Shader

void glDeleteShader(GLuint shader);

Ist ein Shader noch an ein Shader Programm gebunden, muss er erst noch mit glDetachShader gelöst werden.

glBindAttribLocation - Verknüpft einen Stream-Index mit einer Shader-Variable.

void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);
  • GLuint program - Der Index des Shader Programms
  • GLuint index - Der Index des Streams
  • const GLchar *name - Name der Shader Variable

Beispiel

glAttachShader(shader_prog, vert_shader);
glAttachShader(shader_prog, frag_shader);
 
// Stream an Index 0 wird mit "local_vertex" verknüpft
glBindAttribLocation(shader_prog, 0, "local_vertex");
 
glLinkProgram(shader_prog);

Im Vertex Shader:

in vec3 local_vertex;

glDetachShader - Löst einen Shader von einem Shader Programm

void glDetachShader(GLuint program, GLuint shader);

Ein Shader Programm fasst mehrere Shader zusammen. Es kann mit glCreateProgram erstellt werden.

Ein Shader ist ein Programm, das auf der GPU läuft. In Kontext von OpenGL sind Shader der programmierbare Part einer Rendering Pipeline.

glGetUniformLocation - Gibt den Index einer Uniform-Variable zurück.

GLint glGetUniformLocation(GLuint program, const GLchar *name);
  • GLuint program - Index des Shader Programms
  • const GLchar *name - Name der Uniform-Variabl

glUniform - Gibt den Wert einer Uniform-Variable an.

void glUniform{{type}}(GLint location, {{values}})
  • GLint location - Der Index der Uniform Variable.

Mit glUniform können verschiedenste Datentypen versendet werden. Beispiel:

glUniformMatrix4fv

glUniformMatrix4fv - GIbt den Wert eine Uniform-Variable vom Typ 4x4-Matrix an.

void glUniformMatrix4fv(
	GLint location, 
	GLsizei count, 
	GLboolean transpose,
  	const GLfloat *value
  );
  • GLint location - Der Index der Uniform Variable.
  • GLsizei count - Anzahl der zu übergebenden Matrizen
  • GLboolean transpose - Ob die Matrix transponiert werden soll
  • const GLfloat *value - Zeiger auf die Matrix

Beispiel

glUniformMatrix4fv(
	uniform_location("model"), 
	1, 
	GL_FALSE, 
	glm::value_ptr(mat4(1))
);

glClearColor - Füllt den Farb-Framebuffer mit einer Farbe auf.

void glClearColor(
	GLfloat red,
  	GLfloat green,
  	GLfloat blue,
  	GLfloat alpha
);
  • red, green, blue, alpha - Farbwerte. Wertebereich ist .