Benutzer:Mori/OGL3 Quickstart

Aus DGL Wiki
Wechseln zu: Navigation, Suche

OpenGL 3.x Quickstart

Info DGL.png Diese Seite ist noch in Bearbeitung/Planung

Vorwort

OpenGL gibt es nun schon seit einigen Jahren und entwickelt sich auch seitdem kontinuierlich weiter. Eine der größten Änderungen hielt 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 weiteren Änderungen zählen auch das Wegfallen der statischen Renderpipeline und vorgegebene Variablen in Shadern. Dies gibt uns als Programmierern größere Freiheiten in der Verwendung von OpenGL zwingt uns 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, es ist weiterhin sinnvoll sich weitere Tutorials anzuschauen da eine ausführliche Erklärung aller Befehle und Techniken ganze Bücher füllen kann. Das Tutorial setzt außerdem einen gewisse Grundkenntniss mit OpenGL Begriffen wie Rendercontext, VBOs, EABs oder Texturen vorraus auch wenn diese nochmals explizit behandelt werden.

Ich unterteile das Tutorial in 2 Abschnitte, welche es einfacher machen sollen die neue OpenGL Struktur zu verstehen. Der erste Abschnitt behandelt die Struktur und Beziehungen der einzelnen OpenGL Objekte, sowie die benötigten Vorraussetzungen für das Rendern. Der 2te Teil beschäftigt sich danach mit der praktischen Umsetzung und Ausführung.

"Theorie"

Contexte

was ist ein kontext? Einstellungen (clearColor, DepthTest)

OpenGL Objekte

VBOs

EABs

Texturen

Shader

Matrizen

szenen aufbau +kamera matrix +model matrix übergabe per shader

Umsetzung

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.

Info DGL.png glClearColor gibt hierbei die Hintergrundfarbe der Zeichenfläche an, hierfür sollte zum Entwickeln eine andere Farbe als Weiß bzw. Schwarz gewählt werden (zB. wie im Beispiel ein Blauton), da sich hierdurch Objekte farbig vom Hintergrund abheben und Fehler leichter zu erkennen sind

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.
oglObjects.png
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.
VBODaten.png

In unserer VBO Klasse benötigen wir diese Informationen für Verticies,Texturen,Farben und Normalen. Zusätzlich brauchen wir wieder ein Handle hid welches das VBO im OpenGL Treiber repräsentiert und Funktionen um Daten in das VBO schreiben zu können und dieses zu "binden". Eine solche Umsetzung könnte ungefähr so aussehen:
  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;
Wir sehen, dass wir im Konstruktor einen Buffer mit glGenBuffers für unsere Daten anfordern und diesem im Destructor mit glDeleteBuffers wieder löschen. Zusätzlich binden wir den Buffer im Konstruktor als GL_ARRAY_BUFFER. Das bedeutet, das unser Buffer (welcher im Moment noch keine Daten enthält) als aktueller Array Buffer aktiviert wird. Das bedeutet, dass alle folgenden Befehle für Array Buffer auf diesen Buffer angewendet werden. Solche Befehle werden zum Beispiel in den folgenden Edit-Funktionen verwendet.
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;
Diese beiden Funktionen erlauben das Verändern unseres VBOs. Zuerst wird wieder das VBO gebunden, damit wir nicht versehentlich ein anderes noch gebundenes VBO verändern. Danach wird ein optionaler Parameter geprüft, welcher die Größe des VBOs in Bytes angibt (diese Größe wird meißt durch die Vertex Anzahl * die Vertex Größe angegeben). Dieser Parameter muss beim ersten Editieren angegeben werden und legt durch den glBufferData Befehl den eigendlichen Buffer an. Je nach Aufgabe des VBOs kann hier auch ein zweiter Parameter angegeben werden, welcher (je nach Treiber) das VBO auf Zeichne-, Lese- oder Schreiboperationen optimiert. Der glMapBuffer Befehl gibt uns danach einen Pointer auf einen Speicherbereich mit der vorher angegeben Größe, welchen wir jetzt mit unseren Vertex Daten füllen können. Nach dem Befüllen des Buffers müssen wir den Befehl glUnmapBuffer aufrufen, welcher das Editieren beendet und die Daten im VBO sichert.

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.

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;
Die Funktionen glAttachShader und glDetachShader, addieren oder ziehen ein Shader vom Program ab. Mittels glGetObjectParameterivARB in der Funktion IsValidProgram können wir vor dem verwenden des Programms nochmals prüfen, ob das erstellen funktioniert hat.

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.

Uniforms

Bevor wir zum Zeichnen kommen, müssen wir noch eine Verbindung zwischen dem Shader und unseren Matrix Daten erstellen. Die Verbindung wird als Uniform Variable bezeichnet. Mit dem Befehl glGetUniformLocation können wir für Shader die Positionen unserer Shader-Inputs abfragen und Daten an diese binden. Für unseren oben gezeigten Shader wären dies die Variablen projection und modelview.

pLocation:=glGetUniformLocation(SProgram.Handle,'projection');
mLocation:=glGetUniformLocation(SProgram.Handle,'modelview');

Die Funktion gibt ein GLInt zurück, welcher die Position der Variablen im Shader-Programm angibt und welche wir im nächsten Abschnitt verwenden werden.

Zeichnen

Nun kommen wir zum interessanten Teil des Programms, dem eigendlichen Rendern. Nachdem wir nun unsere VBOs, EAB und Shaderprogramme geladen haben, müssen wir OpenGL nur noch mitteilen, welche Daten wir rendern wollen. Zum Rendern benötigen wir eine einfache Render-Loop, wie sie im Quickstart Tutorial beschrieben ist. Die dort beschriebene Methode Render passen wir im folgenden an unser OpenGL 3 Programm an. Zunächst fangen wir wie gewohnt mit dem initialisieren der Szene an. Hierzu setzen wir mit glClearColor eine neue Hintergrundfarbe und reseten mit glClear den Farb- und Tiefenbuffer des vorherigen Frames. Zuletzt setzen wir die Auflösung unseres Programms, diese entspricht meistens der Fenstergröße.

  glClearColor(0.0,0.7,0.9,0.0);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
  glViewPort(0,0,Resolution[0],Resolution[1]);

Nachdem wir die Szene jetzt eingerichtet haben, müssen wir unsere Shader laden und diesen die notwendigen Parameter bereitstellen. Hierzu binden wir zuerst das aktuelle Programm, damit, wie oben gezeigt, sich die folgenden Änderungen auf unseren Shader beziehen. Hiernach müssen wir vor dem zeichnen nur noch die Uniforms binden. Je nach Datentyp müssen wir verschiedene Funktionen zum binden verwenden diese sind im Artikel glUniform aufgelistet. Für den Datentyp mat4 verwenden wir die Funktion glUniform4fv. Sowie für die Textur den Befehl glUniform1i zusätzlich müssen wir auch diese Texturunit aktivienen, um sie überhaupt benutzen zu können.

  SProgram.bind();
  glUniform4fv(pLocation,1,@projectionMatrix[0]);

  glUniform1i(glGetUniformLocation(MProgram.Handle,'tex0'),0);
  glActiveTexture(GL_TEXTURE0+0);

  glUniform4fv(mLocation,1,@mvMatrix[0]);

Nachdem wir nun alles eingestellt haben fehlt uns nur noch das Zeichnen, dieses ist relativ einfach. Zuerst binden wir unser VBO und unser EAB mit den schon oben gezeigten Methoden. Danach binden wir die verschiedenen Attribute (=Vertices, Farbe, Normalen, usw) unseres VBOs an unsere Shader inputs. Hierzu können wir das von uns oben erzeugte Objekt TVBOElementOffset benutzen. Welches wir für jeden Shader Input erzeugen. Danach können wir die Werte folgendermaßen binden. (Shadermap beinhaltet im folgenden Beispiel verschiedene TVBOElementOffsets)

procedure setupVAttribs();
begin
  glVertexAttribPointer(
  shaderMap.vertices,
    data.vertices.count,
    data.vertices.dtype,
    data.vertices.normalized,
    data.vertices.stride,
    pointer(data.vertices.offset)
  );
  glEnableVertexAttribArray(shaderMap.vertices);

  if (shaderMap.textures<>-1) then
  	begin
      glVertexAttribPointer(
        shaderMap.textures,
        data.textures.count,
        data.textures.dtype,
        data.textures.normalized,
        data.textures.stride,
        pointer(data.textures.offset)
      );
      glEnableVertexAttribArray(shaderMap.textures);
    end;

  ...
end;
Nach diesem letzten Binden der Resourcen, fehlt nur noch der eigendliche Zeichen-Befehl glDrawElements dieser erwartet von uns eine angabe der Geometrie, sowie die Anzahl der zu zeichnenden Indices und deren Datentyp. (Die letzten beiden Parameter haben wir schon im Abschnitt EAB angelegt). Somit sieht die restliche Zeichenmethode folgendermaßen aus:
VBO.bind();
EAB.bind();
setupVAttribs()

glDrawElements(GL_TRIANGLES,EAB.elementsCount*3,EAB.valType,nil);

Aussicht

Ich hoffe, dass du nach diesem Tutorial dein erstes OpenGL 3 Programm zum laufen gebracht hast und ein besseres Verständnis für die neue API Struktur erhalten hast. Falls du noch Probleme, Vorschläge oder Fragen zum Tutorial oder OpenGl allgemein hast kannst du jederzeit im Forum fragen.

Moritz Willig