Benutzer:Mori/OGL3 Quickstart: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
K
(Shader)
Zeile 139: Zeile 139:
 
</source>
 
</source>
 
==Shader==
 
==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 <<hier>> zu finden.
+
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===
 
===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.
 
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.
Zeile 298: Zeile 298:
 
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.
 
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===
 
===Shader Code===
Nachdem wir nun mit OpenGL Programme und Shader ansprechen können fehlt uns nur noch eines: Der Shader Code selber!
+
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.
-Unterschiede Vertex Fragment / Reihnfolge
+
====Vertex Shader====
-Demo Shader Code
+
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.
-Einbinden in das Programm
+
<source lang="C">#version 330
-vorrausetzungen (benötigte matrizen + daten vbo mapping)
+
 
-shader arten
+
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;
 +
}</source>
 +
====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|Tutorial_Framebufferobject]] 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.
 +
<source lang="C">#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));
 +
}</source>
 +
 
 
==Matrizen==
 
==Matrizen==
  

Version vom 1. April 2013, 12:39 Uhr

OpenGL 3.x Quickstart

Info DGL.png 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.

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

error handling

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.

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;
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 Tutorial_Framebufferobject 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