Benutzer:Mori/OGL3 Quickstart
Inhaltsverzeichnis
OpenGL 3.x Quickstart
Diese Seite ist noch in Bearbeitung/Planung |
Vorwort
warum ogl3 / nachteile /vorteile OpenGL gibt es nun schon seit einigen Jahren und entwickelt sich auch seitdem kontinuierlich weiter. Einer der größten Änderungen erhielt Einzug mit OpenGL 3. Veraltete Befehle werden (unter bestimmten Umständen) nicht mehr supportet und es ist ein neuer Context erforderlich um OpenGL 3 nutzen zu können. Zu den weiteren Änderungen zählen auch das Wegfallen der statischen Renderpipeline und vorgegebene Variablen. Dies ermöglicht uns als Programmierern größere Freiheiten in der Verwendung von OpenGL erzwingt aber gleichzeitig, dass wir uns selber um bestimmte Dinge, wie Modelview- und Projectionmatrizen, kümmern. Dieses Tutorial soll einen kurzen Einstieg in die Möglichkeiten von OpenGL 3 geben und zeigen, wie man die erste Anwendung einfach selber compilieren kann. Da da dieses Tutorial nur einen kurzen Einstieg in OpenGl geben soll sind an den entsprechenden Stellen Verlinkungen zu weiterführenden Artikeln angegeben.
Context
Der erste wesentliche Unterschied zu OpenGL Versionen kleiner als 3.0 besteht in einem weiteren Rendercontext, welcher zusätzlich erstellt weren muss. Wie auch die 1.x/2.x Rendercontexte (im Folgenden RC), erzeugt man hiermit eine neue OpenGL "Instanz", welche zum Rendern auf einen DeviceContext(im Folgenden DC) genutzt werden kann. Der wesentliche Unterschied ist, dass wir einen gültigen 1.x/2.x RenderContext benötigen, um unseren 3.x'er Context anfordern zu können. Wesentliche Änderungen sind:
- Wegfallen des Direct-mode (glBegin,glEnd)
- Wegfallen des der festen Render-Pipeline
- Wegfallen von Displaylisten
Dies führt zu einer Reihe von Neuerungen im Code, hilft aber auch eine gewisse Struktur in die API zu bringen und das OpenGL Interface zu vereinheitlichen.
Um den Context zu bekommen, könnte unsere Funktion in etwa so aussehen:
constructor TGLContext.Create(DeviceContext:HDC);
begin
DC:=DeviceContext; RC:=0;
if InitOpenGL()=false then exit;
RC:=CreateRenderingContextVersion(DC,[opDoubleBuffered],3,1,false,32,24,0,0,0,0);
ActivateRenderingContext(DC,RC);
SetViewPort(1,1);
glClearColor(0.0,0.4,0.8,0.0);
glEnable(GL_DEPTH_TEST);
//glEnable(GL_CULL_FACE);
end;
Unsere Funktion nimmt hierbei einen DeviceContext entgegen (dieser gibt die Zeichenfläche an und ist kann mit der GetDC Funktion abgefragt werden. ZB. getDC(Form1.Handle) ). InitOpenGL ist eine Funktion unseres Headers, welche die OpenGL Library in unser Programm lädt. Hiernach können wir unseren OpenGL 3.x Context anfordern. Die Header-Funktion CreateRenderingContextVersion nimmt uns den Aufwand des erstellen beider Contexte ab und gibt uns (falls OpenGL 3.0 Unterstützt wird) unseren gewünschten RenderContext zurück. Sollte eine andere OpenGL Version als 3.1 gewünscht werden kann diese in dieser Funktion mit den Parametern MajorVersion und MinorVersion angefordert werden. Weiterhin können auch Color-, Z-, Stencil, AccumBits und die Anzahl der AuxBuffers hier eingestellt werden. Zuletzt können wir mit dem Parameter ForwardCompatible wählen, ob der Kontext die veralteten 2.x Funktionen unterstützen soll. Wenn keine Notwendigkeit besteht, sollte dieser Parameter auf true gestellt werden um einen puren 3.x Kontext zu erzeugen. Um OpenGL im Folgenden verwenden zu können müssen wir unseren neuen Kontext aktivieren und ein paar Grundeinstellungen treffen.
OpenGL Objekte
Mit der Umstellung auf OpenGL 3 wurde die API in einigen Bereichen grundlegend verändert. Hierzu zählt auch eine stärkere Kapselung der Daten in Objekten, welche über Handles angesprochen werden können. Verschiedene Befehle manipulieren, somit immer das aktuell gebundene Objekt eines Bereichs. Das nachfolgende Schaubild zeigt grafisch die Verknüpfung von Handles zu aktiven Objekten und den Zugriff durch bestimmte Befehle.
Wie man erkennen kann müssen nicht immer Objekte gebunden sein, sollte in diesem Zustand jedoch ein Befehl versuchen das Objekt zu verändern gibt OpenGL einen Fehler aus. Das gleiche gilt, wenn man versucht Handles verschiedener Gruppen, zum Beispiel ein VBO, als aktives Objekt für Texturen zu binden.
VBOs
Ein elementarer Bestandteil auf dem Weg zu einer sichtbaren Ausgabe sind Vertex Buffer Arrays (im Folgenden: VBO's). Diese beinhalten die 3D-Daten welche auf der Grafikkarte verarbeitet und gezeichnet werden. Damit OpenGL die Daten verarbeiten kann, müssen wir vor dem Zeichnen bekanntgeben in welchem Format die Daten vorliegen. Folgende Informationen werden benötigt:
TVBOElementOffset=record
dtype:Cardinal; //Datentyp: GL_FLOAT, GL_BYTE, ...
normalized:GLboolean; //Sind die Werte normalisiert? (GL_TRUE/GL_FALSE)
count:Cardinal; //Wie viele Elemente sind enthalten? (bei einer Farbe: r,g,b,a = 4)
offset:Cardinal; //Was ist der Offset zu den Werten in einem Vertex
stride:Cardinal; //Schrittweite, bis zum nächsten Vertex
end;
Hierbei möchte ich nochmals auf die Werte Offset und Stride eingehen, welche unter Umständen falsch verstanden werden können. Ein beispielhaftes Vertex im VBO könnte aus 3 Float Positionswerten (X,Y,Z) und 3 Float Farbwerten (R,G,B) bestehen. Im VBO stehen nun für ein Vertex die Werte X,Y,Z,R,G,B hintereinander im Speicher. Hierbei wird die Größe des gesamten Vertex (in Byte) wird im Wert Stride angegeben, dieser beschreibt die Entfernung der Verticies im Speicher voneinander. Der Wert Offset beinhaltet hierbei die Unterteilung des Vertex selbst. Da die Position am Anfang des Vertices gespeichert wird ist der Offset hier 0. Für die Farben erhalten wir hierfür allerdings einen Offset von 12 Bytes, welcher die Verschiebung relativ zum Vertex beschreibt. Da vor den Farben noch die 3 Positionswerte als Float gespeichert werden (SizeOf(Float)=4) müssen wir den Offset auf 3*4=12 setzen. Das folgende Bild veranschaulicht nochmals, wie man Stride und Offset für Daten berechnen kann.
TGLVBO=class
private
hid:GLUInt;
public
vertices:TVBOElementOffset;
textures:TVBOElementOffset;
colors:TVBOElementOffset;
normals:TVBOElementOffset;
constructor Create();
destructor Destroy(); override;
procedure bind();
function startEdit(size:Integer=-1; mode:Cardinal=GL_STATIC_DRAW):Pointer;
procedure endEdit();
end;
procedure TGLVBO.bind;
begin
glBindBuffer(GL_ARRAY_BUFFER,hid);
end;
constructor TGLVBO.Create;
begin
glGenBuffers(1,@hid);
bind();
end;
destructor TGLVBO.Destroy;
begin
glDeleteBuffers(1,@hid);
inherited;
end;
function TGLVBO.startEdit(size:Integer=-1; mode:Cardinal=GL_STATIC_DRAW):Pointer;
begin
bind();
if size>0 then
glBufferData(GL_ARRAY_BUFFER,size,nil,mode);
Result:=glMapBuffer(GL_ARRAY_BUFFER_ARB,GL_READ_WRITE);
end;
procedure TGLVBO.endEdit;
begin
glUnmapBuffer(GL_ARRAY_BUFFER_ARB);
end;
erstellen (laden?) binden
EABs
Ein Element Array Buffer (im folgenden EAB), "erweitert" die Möglichkeiten Meshes zu Zeichnen. Der Einsatz von EABs bietet sowohl die Möglichkeit Meshes dynamischer zu zeichnen, als auch den Speicherverbrauch (je nach Mesh) drastisch zu reduzieren. Wie der Name schon andeutet, handelt es sich um einen Buffer zum Speichern von Element Array's. Diese Elemente sind in diesem Fall die Indices der einzelnen Mesh-Vertices. In diesem Buffer geben wir daher die Daten nichtmehr direkt an, sondern nur noch einen Index und können erstens Vertices zwischen verschiedenen Faces sharen (zB. bei Ecken in Objekten, welche von mehreren Dreiecken verwendet werden) und zweitens, verschiedene Rendermodes (zB. GL_TRIANGLES, GL_TRIANGLE_STRIP, ...) auf ein und das selbe VBO anwenden, ohne die Reihenfolge der Vertices auf der Grafikkarte verändern zu müssen. Zuerst holen wir uns deshalb, wie schon beim VBO, ein Handle (hier hid:GLUInt) von OpenGL und binden dieses als GL_ELEMENT_ARRAY_BUFFER. Dieses Handle wird am Ende wieder mit glDeleteBuffers freigegeben:
constructor TGLEAB.Create;
begin
glGenBuffers(1,@hid);
bind();
end;
destructor TGLEAB.Destroy;
begin
glDeleteBuffers(1,@hid);
inherited;
end;
procedure TGLEAB.bind;
begin
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,hid);
end;
Nachdem wir jetzt unser EAB Handle erzeugt haben müssen wir es nur noch mit Daten befüllen. Dazu brauchen wir einen Vertex-count und die Größe (in Byte) eines einzelnen Vertex um den entsprechenden Speicher reservieren zu können.
function TGLEAB.startEdit(valType:Cardinal; count:Cardinal; size: Integer=-1; mode:Cardinal=GL_STATIC_DRAW): Pointer;
begin
self.valType:=valType;
elementsCount:=count;
bind();
if size>0 then
glBufferData(GL_ELEMENT_ARRAY_BUFFER,size,nil,mode);
Result:=glMapBuffer(GL_ELEMENT_ARRAY_BUFFER,GL_READ_WRITE);
end;
procedure TGLEAB.endEdit;
begin
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
end;
Hierbei erzeugen wir mit glBufferData einen neuen EAB mit der entsprechenden Größe (bei size=-1 lassen wir ein schon bestehendes EAB in der Größe unverändert). Wie schon beim VBO lassen wir uns danach mit glMapBuffer einen Pointer auf den reservierten Speicherbereich zurückgeben, welchen wir beschreiben können. Wie schon beim erzeugen müssen wir beim Binden wieder GL_ELEMENT_ARRAY_BUFFER angeben, damit OpenGL unser gebundenes EAB auswählt und nicht ein VBO.
glDrawElements(GL_TRIANGLES,indicies.elementsCount*3,indicies.valType,nil);
Shader
Nun kommen wir zum eigendlich wichtigsten Punkt beim gesammten Zeichnen: Den Shadern. Mit der Einführung von OpenGL 3 sind wir verpflichtet uns selber um das Berechnen unserer Objekte auf der Grafikkarte zu kümmern. Ein Programm auf der Grafikkarte wird Shader genannt. Auf der Grafikkarte laufen beim Rendern des Bildes viele Shader gleichzeitig. Hierbei unterscheidet man verschiedene Shader Arten, welche verschiedene Aufgaben, wie zum Beispiel das berechnen der Vertexpositionen (Vertex Shader) oder das bestimmen der Pixelfarben (Fragment Shader), erfüllen. Dieses Tutorial zeigt nur die pure Anwendung der beiden notwendigen Vertex und Fragment Shader. Ein ausführliches Tutorial über die Shadersprache glsl ist im Tutorial_glsl zu finden.
Programme
Shader werden in OpenGL zu sogenannten Programmen zusammengefasst, welche die zur Bildberechnung notwendigen Shader enthalten. Nachdem ein solches Programm die entsprechenden Shader erhalten hat, wird es zusammengelinkt. Hierbei werden die einzelnen Shader vom Treiber auf ihre Lauffähigkeit überprüft und zur Anwendung auf der Grafikkarte "freigegeben". Der folgende Code enthält eine Beispielhafte implementation.
TGLProgram=class
private
hid:GLhandle;
pShaders:TList<TGLShader>;
function pGetShader(Index:Integer):TGLShader;
function pGetCount():Integer;
public
constructor Create();
destructor Destroy(); override;
property Handle:GLHandle read hid;
procedure addShader(Shader:TGLShader);
procedure removeShader(Shader:TGLShader);
property Shaders[Index:Integer]:TGLShader read pGetShader;
property Count:Integer read pGetCount;
procedure link();
procedure bind();
function IsValidProgram():Boolean;
end;
implementation
procedure TGLProgram.addShader(Shader: TGLShader);
begin
pShaders.Add(Shader);
glAttachShader(Handle, Shader.Handle);
end;
constructor TGLProgram.Create;
begin
pShaders:=TList<TGLShader>.Create();
hid:=glCreateProgram();
end;
destructor TGLProgram.Destroy;
begin
if IsValidProgram() then
begin
glDeleteProgram(Handle);
hid:=0;
end;
pShaders.Free();
inherited;
end;
function TGLProgram.IsValidProgram: Boolean;
var
res:GLInt;
begin
if Handle=0 then begin result:=false; exit; end;
glGetObjectParameterivARB(Handle,GL_OBJECT_LINK_STATUS_ARB,@res);
result:=(res=GL_TRUE);
end;
procedure TGLProgram.link;
begin
glLinkProgram(Handle);
end;
function TGLProgram.pGetCount: Integer;
begin
Result:=pShaders.Count;
end;
function TGLProgram.pGetShader(Index: Integer): TGLShader;
begin
Result:=pShaders[Index];
end;
procedure TGLProgram.removeShader(Shader: TGLShader);
begin
pShaders.Remove(Shader);
glDetachShader(Handle,Shader.Handle);
end;
procedure TGLProgram.bind();
begin
glUseProgram(Handle);
end;
Shader
Nach den Programmen kommen wir jetzt zu den eigendlichen Shadern. Um diese zu Erstellen müssen wir OpenGL den Shader Code übergeben, sowie angeben, welchen Shader Typ wir erstellen wollen. Auch hier können wir, wie in der folgenden Implementation von IsValidShader, abfragen, ob unser Shader Code korrekt war und uns, wie in der Methode getLastError gezeigt, eine Fehlermeldung ausgeben lassen, falls der Shader nicht compiliert wurde.
TGLShader=class
private
hid:GluInt;
pShaderType:Cardinal;
public
constructor Create(Code:PAnsiChar; ShaderType:Cardinal); //GL_vertex_shader/GL_fragment_shader
destructor Destroy(); override;
property Handle:GLUInt read hid;
property ShaderType:GLUInt read pShaderType;
function IsValidShader():Boolean;
function getLastError():String;
end;
implementation
{ TGLShader }
constructor TGLShader.Create(Code: PAnsiChar; ShaderType:Cardinal);
var
Len:GLInt;
begin
self.pShaderType:=ShaderType;
hid:=0;
hid:=glCreateShader(ShaderType);
Len:=Length(Code);
glShaderSource(hid, 1, @Code, @Len);
glCompileShader(hid);
end;
destructor TGLShader.Destroy;
begin
if IsValidShader then
begin
glDeleteShader(hid);
hid:=0;
end;
inherited;
end;
function TGLShader.getLastError: String;
var
blen, slen: GLInt;
InfoLog: PGLCharARB;
begin
glGetShaderiv(Handle, GL_INFO_LOG_LENGTH , @blen);
if blen>1 then
begin
GetMem(InfoLog, blen * SizeOf(GLCharARB));
glGetShaderInfoLog(Handle, blen, slen, InfoLog);
Result:=String(PAnsiChar(InfoLog));
Dispose(InfoLog);
end
else
Result:='';
end;
function TGLShader.IsValidShader: Boolean;
var
res:GLInt;
begin
if hid=0 then begin result:=false; exit; end;
glGetObjectParameterivARB(hid,GL_OBJECT_COMPILE_STATUS_ARB,@res);
result:=(res=GL_TRUE);
end;
Im Code sehen wir, das wir mit glCreateShader(ShaderType) uns wieder einen Namen für unser Shaderobjekt holen. Danach übergeben wir unseren Code mit glShaderSource(hid, 1, @Code, @Len); an den OpenGL und compilieren ihn mit glCompileShader(hid);. Falls alles funktioniert hat gibt die Funktion IsValidShader True zurück und wir können unseren Shader zu einem oben beschriebenen Program hinzufügen.
Shader Code
Nachdem wir nun mit OpenGL Programme und Shader ansprechen können fehlt uns nur noch eines: Der Shader Code selber! Shader werden mit der oben beschriebenen Shader Sprache glsl geschrieben, welche im Aufbau der C-Syntax ähnelt. Für dieses Tutorial benötigen wir einen Vertex, welcher unsere Vertices positioniert, sowie Normalen oder Texturen festlegt und einen Fragment Shader, welcher die Berechnung der Pixel übernimmt.
Vertex Shader
Der Vertex Shader beginnt mit der Versionsangabe der Shader Sprache, diese legt die verfügbaren Befehle und Schnittstellen fest. Für OpenGL 3.1 muss mindestens Version 1.40 gewählt werden. Als nächstes sehen wir 2 Deklarationen mit dem Schlüsselwort uniform. Diese Variablen können von unserem Programm gesetzt und anschließend im Shader verwendet werden. Im folgenden Shader werden die Projection- und die Modelview-Matrix vom Programm an den Shader übergeben, welche wir im Anschluss für die Berechnung der Vertex Positionen auf dem Bildschirm benötigen. Hiernach sehen wir das Schlüsselwort in, dieses regelt genauso wie das folgende out, den Zugriff auf die Variablen. Variablen, welche im Vertex Shader mit in deklariert sind, müssen von unserem Programm bereit gestellt werden. out Variablen im Vertex Shader werden an den Fragment Shader weitergereicht. Zuletzt sehen wir in der Funktion main das eigendliche Programm. Dieses berechnet aus Projektions- und Modelviewmatrix, sowie der Positionsdaten unseres VBOs die entgültige Vertexposition und übergibt diese an die Build-In Variable gl_Position. Die Parameter texture und normal werden nicht verändert und an den Fragmentshader weitergeleitet.
#version 330
uniform mat4 projection;
uniform mat4 modelview;
in vec4 position;
in vec2 texture;
in vec3 normal;
out vec2 tex;
out vec3 norm;
void main(void)
{
gl_Position=projection*modelview*position;
tex=texture;
norm=normal;
}
Fragment Shader
Die eigendliche Berechnung der Pixelfarbe, Tiefe und sonstiger Werte wird im Fragmentshader vorgenommen. Auch dieser Shader fängt wieder mit einer Versionsangabe an, welche, wie oben beschrieben, für verschiedene OpenGL Versionen eine minimale Versionsnummer benötigt. In diesem Shader wird zuerst eine zusätzliche sampler2D Uniform angegeben, welche eine, von uns gebundene, Textur repräsentiert. Danach sehen wir die out-Parameter des Vertex Shaders, welche nun mit in gekennzeichnet sind und somit in unserem Shader verarbeitet werden können. Zusätzlich werden 3 out Variablen deklariert. Diese werden mit einem zusätzlichen location-Attribut versehen, so dass wir sie später an einen Framebuffer binden können. Wollen wir direkt auf den Bildschirm zeichnen brauchen wir nur das frag_color-Varying zu berechnen. Dieses wird ohne gebundenen Framebuffer direkt auf dem Bildschirm ausgegeben. Zum Schluss sehen wir wieder das eigentliche Programm. Mit der Funktion texture2D können wir, durch Angabe der Textur und der Leseposition, Farbewerte aus der Texturlesen und diese als Farbe für den aktuellen Pixel setzen. Die restlichen Parameter werden nur für eine weitergehende Verarbeitung durch einen zweiten Renderpass benötigt und können zum einfachen Rendern verworfen werden.
#version 330
uniform sampler2D tex0;
in vec2 tex;
in vec3 norm;
layout(location=0) out vec3 frag_color;
layout(location=1) out float depth_component;
layout(location=2) out vec3 normal_component;
void main() {
frag_color=vec3(texture2D(tex0, tex));
depth_component=gl_FragCoord.z;
normal_component=vec3(0.5)+(norm/vec3(2.0));
}
Matrizen
Wie wir im vorherigen Abschnitt gesehen haben, müssen wir dem Shader Matrizen mitgeben. Diese Matrizen benutzen wir im folgenden, um unsere Objekte zu Positionieren (durch Verschiebung, Rotation und Skalierung) und sie auf unseren Viewport (meist den Bildschirm) zu projizieren. Daher nennen wir die folgenden Matrizen ModelView- und Projection- 4x4-Matrix. Zum initialisieren benutzt man am besten eine Math-Libary welche die benötigten Funktionen bereitstellt. Als erstes richten wir die Projection-Matrix ein, welche wir nur einmal in der Szene benötigen:
projectionMatrix.perspective(45,Viewport.Width/Viewport.Height,0.5,100);
Diese Initialisierung der Matrix bewirkt, das wir auf dem Bildschirm eine Abbildung der Vertices mit einem Blickwinkel von 45 Grad erhalten. Außerdem geben wir mit den Werten 0.5 bis 100 die Near- und Far-Clipping Plane an, welche angibt, ab welcher Entfernung Objekte nichtmehr sichtbar sein sollen. Als zweites Initialisieren wir unsere Modelview Matrix. Diese gibt, wie der Name schon sagt, die Verschiebung der Objekte in unserer Szene an. Diese wird als Identitäts-Matrix initialisiert und im Folgenden zB. Rotiert oder Verschoben.
mvMatrix.indentity();
mvMatrix.rotateX(degToRad(45)); //Rotation um 45 Grad
mvMatrix.translate(0,0,-5); //Verschiebung auf der Z-Achse
Es macht Sinn für die Kamera und für jedes Objekt eine eigene ModelView-Matrix zu erstellen, um bei einer Bewegung der Kamera, nicht jede Matrix neu berechnen zu müssen. Bei einer Änderung, zB. der Verschiebung der Kamera Position, kann man die neue modelviewMatrix der Objekte durch Multiplizieren der Objekt-ModelviewMatrix mit der Kamera-ModelviewMatrix erhalten. Da dieses Tutorial nur ein kurzer Einstieg in OpenGl ist, beschäftigen wir uns hier nicht ausführlich mit Matrizen, weiterführende Erklärungen zur Matrizenrechnung kann man im Tutorial_Nachsitzen finden.
Zeichnen
eab/vbo/shader binding
Weiterführendes/Aussicht
erläuterungen, weiterführende links