Tutorial glsl

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Präambel

Ave und willkommen bei meiner "Einführung" in die recht frische und mit OpenGL1.5 eingeführte Shadersprache "glSlang". In diesem umfangreichen Dokument werde ich versuchen sowohl auf die Nutzung (sprich das Laden und Anhängen von Shadern im Quellcode), als auch auf die Programmierung von Shadern selbst einzugehen, inklusive aller Sprachelemente der OpenGL Shadersprache. Es wird also auch recht viele Informationen zu der C-ähnlichen Programmstruktur und den von glSlang angebotenen Variablen und Attributen gehen. Am Ende dieser Einführung sollten alle die, die sich für das Thema interessieren in der Lage sein zumindest einfach Shader zu schreiben und auch in ihren Programmen zu nutzen. Ausserdem soll dieses Dokument gleichzeitig als ein deutsches "Pendant" zu den von 3DLabs veröffentlichten Shaderspezifikationen, und damit als alltägliches Nachschlagewerk dienen.


Vorkenntnisse

Wie auch schon mein ARB_VP-Tutorial richtet sich auch diese Einführung aufgrund ihrer Thematik eher an die fortgeschritteneren GL-Programmierer und neben sehr guten GL-Kenntnissen sollten sich alle, die sich daran versuchen wollen, mit den technischen Hintergründen der GL, wie z.B. dem Aufbau der Renderpipeline auskennen. Weiterhin sind C-Kenntnisse absolut erforderlich, da die Shader ja in einer an ANSI-C angelehnten Syntax geschrieben werden. Auch Begriffsdefinitionen zu Vertex oder Fragment werden zum Verständis dieser Einführung benötigt. Wer also noch am Anfang seiner GL-Karriere steht, dem wird dieses Dokument nicht viel nützen. Ganz nebenbei solltet ihr auch noch eine gehörige Portion Zeit (am besten nen kompletten Nachmittag) mitbringen, denn die folgende Kost ist nicht nur umfangreich sondern auch manchmal recht schwer verdaulich.




Was ist glSlang?

Wie Eingangs kurz angesprochen handelt es sich bei glSlang um eine Shadersprache, also um eine Hochsprache in der man die programmierbaren Teile aktueller Grafikbeschleuniger nach eigenem Belieben programmieren kann. Sie stellt quasi den Nachfolger zu den in assembler geschriebenen Vertex- und Fragmentprogrammen (GL_ARB_Vertex_Program/GL_ARB_Fragment_Program) dar und basiert auf ANSI C, erweitert um Vektor- und Matrixtypen sowie einige C++-Mechanismen.

Die in glSlang geschriebenen Programme nennen sich, angepasst an die Termonologie von RenderMan und DirectX, Shader (im Gegensatz zu "Programme" bei ARB_VP/FP) und werden entweder auf Vertexe (VertexShader) oder Fragmente (FragmentShader) angewendet, andere noch nicht programmierbare Teile der GL-Pipeline wie z.B. die Rasterisierung können momentan noch nicht über Shader beeinflusst werden.


Voraussetzungen

glSlang ist ein brandneues Feature, das mit OpenGL1.5 eingeführt wurde, weshalb eine entsprechend moderne Grafikkarte (DX9-Generation) inklusive aktuellster Treiber von Nöten ist. Der Stand zum Zeitpunkt als diese Einführung geschrieben wurde (24.12.2003) ist folgender : ATI hat seit Catalyst 3.10 glSlang-fähige Treiber, deren glSlang-Compiler allerdings noch nicht alle Features unterstützt. Bei 3DLabs ist dieser Fall ähnlich, deren Treiber bieten allerdings schon etwas länger glSlang-Support. NVidia lassen sich diesmal allerdings recht lange Zeit und haben noch keine Treiber am Start die glSlang beherrschen, hier ist also noch etwas Geduld angesagt. Allerdings sollten auch die Kalifornier bald mit glSlang-fähigen Treibern für ihre GeForce FX-Reihe rausrücken.

Natürlich benötigt ihr auch einen passenden OpenGL-Header der die für glSlang nötigen Extensions und Funktionen exportiert. Ich verweise dazu auf unseren internen OpenGL-Header DGLOpenGL.pas der da einwandfrei seine Dienste verrichtet und auch in der Beispielanwendung Verwendung findet.


Neue Extensions

Die GL-Shadersprache "besteht" in ihrer aktuellen Version aus folgenden Extensions, fürs Verständnis wäre es nicht schlecht wenn ihr euch zumindest die Einleitungen dazu durchlest :

Definiert die API-Aufrufe die zum Erstellen, Kompilieren, Linken, Anhängen und Aktivieren von Shader- und Programmobjekten nötig sind.
Fügt der OpenGL Programmierbarkeit auf Vertexebene hinzu.
Fügt der OpenGL Programmierbarkeit auf Fragmentebene hinzu.
Gibt die unterstützte Version von glSlang an, momentan 1.00.


Objekte

Im Zuge der Vereinheitlichung der GL wird immer häufiger in Objekte gekapselt, deren API dann auch aneinander angelehnt ist. Ziel ist dabei die Programmierung der GL uniform zu machen, so das z.B. zwischen dem Erstellen und Verwalten eines Vertex-Buffer-Objektes oder eines Shader-Objektes kaum ein Unterschied besteht (demnächst kommen dann auch Pixel-Buffer-Objekte). Mit glSlang wurden dann im Zuge dieser Aktion zwei neue Objekte eingeführt, deren Definition ihr euch unbedingt einprägen solltet :

  • Programmobjekt
Ein Objekt an das die Shader später angebunden werden. Bietet Funktionalität zum Linken der Shader und prüft dabei die Kompatibilität zwischen Vertex- und Fragmentshader.
  • Shaderobjekt
Dieses Objekt verwaltet den Quellcodestring eines Shaders und ist entweder vom Typ GL_VERTEX_SHADER_ARB oder GL_FRAGMENT_SHADER_ARB.


Resourcen

Die Shadersprache ist keinesfalls final und es wurden bereits diverse Ausdrücke für zukünftige Verwendung reserviert, denn ein Ziel bei ihrer Entwicklung war es, sie so zukunftsorientiert zu gestalten das auch Grafikkarten der nächsten und übernächsten Generation voll ausgenutzt werden können. Damit einher geht die Tatsache das sich die Spezifikationen in Zukunft ändern/erweitern werden, weshalb man da immer einen Blick hineinwerfen sollte. Die Anlaufstelle dafür ist natürlich die GL2-Seite von 3D-Labs, wo u.a. auch ein OGL2-SDK und diverse Whitepapers als PDFs angeboten werden, in denen auch stattgefundene Änderungen an glSlang dokumentiert sind.


glSlang im Programm

Bevor wir uns mit der Syntax von glSlang beschäftigen, zeige ich euch erstmal wie ihr Shader in euer Programm einbindet und nutzen tut. Warum das zuerst? Ganz einfach deshalb, weil ihr dann das was ihr im glSlang-Syntaxteil lernt direkt in eurer Testanwendung verwenden könnt. Hoffe diese Entscheidung klingt logisch und findet Anklang.

Zuerst benötigen wir natürlich unsere Objekte. Zum einen ein Programmobjekt, an das unsere Shader gebunden werden, und zwei Shaderobjekte die den Quellcode unseres Vertex bzw. Fragment Shaders aufnehmen. Dazu wurde eigens der neue "Datentyp" glHandleARB eingeführt, der ein Objekthandle repräsentiert. Wir deklarieren also wie folgt :

ProgramObject        : GLhandleARB;
VertexShaderObject   : GLhandleARB;
FragmentShaderObject : GLhandleARB;


Nach dieser Deklaration können wir dann damit beginnen unsere Objekte zu erstellen. Den Anfang macht das Programmobjekt :

ProgramObject        := glCreateProgramObjectARB;

Die Funktion glCreateProgramObjectARB erstellt uns oben ein leeres Programmobjekt und gibt ein gültiges Handle darauf zurück.

Weiter gehts mit der Erstellung unseres Vertex bzw. Fragment Shaders :

VertexShaderObject   := glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
FragmentShaderObject := glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);

glCreateShaderObjectARB dient zur Generierung eines leeren Shaderobjektes. Momentan unterstützt diese Funktion VertexShader und FragmentShader.

Nachdem wir nun also zwei gültige Shaderobjekte haben, wollen wir diese auch mit entsprechendem Quellcode versorgen :

glShaderSourceARB(VertexShaderObject, 1, @ShaderText, @ShaderLength);
glShaderSourceARB(FragmentShaderObject, 1, @ShaderText, @ShaderLength);

Via glShaderSourceARB setzen wir den Quellcode eines Shaderobjektes komplett neu. Zum Laden des Quellcodes bietet sich unter Delphi übrigens eine TStringList gradezu an. Es sollte beachtet werden dass der Quellcode zu diesem Zeitpunkt nicht geparst wird, also keine Fehleruntersuchung stattfindet.

Der Quellcode wurde jetzt also an unsere Shaderobjekte gebunden und sollte dann natürlich auch noch kompiliert werden :

glCompileShaderARB(VertexShaderObject);
glCompileShaderARB(FragmentShaderObject);

Der glSlang-Compiler des Treibers wird bei einem Aufruf von glCompileShaderARB versuchen unsere Shader zu kompilieren. Sofern diese keine Fehler aufweisen sollte dies auch erfolgreich sein. Wenn nicht, dann spuckt uns der ShaderKompiler je nach Treiber recht detaillierte Infos aus. Wie man an diese Infos kommt könnt ihr gleich nachlesen.

Wenn unsere Shader dann kompiliert werden konnten, ist es Zeit diese an unser anfangs erstelltes Programmobjekt anzuhängen :

glAttachObjectARB(ProgramObject, VertexShaderObject);
glAttachObjectARB(ProgramObject, FragmentShaderObject);


Nachdem die Shaderobjekte nun an das Programmobjekt angehangen wurden, werden diese nicht mehr benötigt und ihre Resourcen können freigegeben werden :

glDeleteObjectARB(VertexShaderObject);
glDeleteObjectARB(FragmentShaderObject);


Am Schluß müssen wir dann noch unsere ans Programmobjekt gebundenen Shader linken :

glLinkProgramARB(ProgramObject);

Während glCompileShaderARB unsere Shader auf syntaktische Fehler innerhalb ihres lokalen Raums geprüft hat, werden beim Linken durch glLinkProgramARB die angehangenen Shader zu einem ausführbaren Shader gelinkt. Folgende Bedingungen führen zu einem Linkerfehler:

  • Die Zahl der von der Implementation unterstützten Attributvariablen wurde überschritten
  • Der Speicherplatz für Uniformvariablen wurde überschritten
  • Die Zahl der von der Implementation angebotenen Sampler wurde überschritten
  • Die main-Funktion fehlt
  • Die Liste der Varying-Variablen des Vertexshaders stimmt nicht mit der des Fragmentshaders überein
  • Funktions- oder Variablenname nicht gefunden
  • Eine gemeinsame Globale ist mit unterschiedlichen Werten oder Typen initialisiert worden
  • Zwei Sampler unterschiedlichen Typs zeigen auf die selbe Textureneinheit
  • Ein oder mehrere angehangene(r) Shader wurden nicht erfolgreich kompiliert

Die Nutzung von glSlang im eigenen Programm ist wie oben erkennbar also nicht wirklich schwer und innerhalb kurzer Zeit realisiert. Natürlich ist es auch möglich z.B. nur einen VertexShader oder nur einen FragmentShader an ein Programmobjekt zu binden.


Fehlererkennung

Natürlich wird es ohne Fehlerausgabe recht schwer, etwaige Probleme in einem Vertex oder Fragment Shader zu finden. Doch auch in diesem Bereich wurde glSlang recht gut durchdacht und es wurden zwei Funktionen eingeführt, welche im Zusammenspiel die Fehlersuche recht einfach machen, nämlich glGetInfoLogARB und glGetObjectParameterivARB mit dem Argument GL_OBJECT_INFO_LOG_LENGTH_ARB. Erstere Funktion liefert uns einen Logstring, während uns letztere Funktion dessen Länge angibt. Der Logstring wird verändert, sobald ein Shader kompiliert oder ein Programm gelinkt wird.

Um die Ausgabe dieses Logs so einfach wie möglich zu machen, bietet es sich an beide in einer einfach Funktion unterzubringen :

function glSlang_GetInfoLog(glObject : GLHandleARB) : String;
var
 blen,slen : GLInt;
 InfoLog   : PGLCharARB;
begin
glGetObjectParameterivARB(glObject, GL_OBJECT_INFO_LOG_LENGTH_ARB , @blen);
if blen > 1 then
 begin
 GetMem(InfoLog, blen*SizeOf(GLCharARB));
 glGetInfoLogARB(glObject, blen, slen, InfoLog);
 Result := PChar(InfoLog);
 Dispose(InfoLog);
 end;
end;


Die Funktion ist recht leicht erklärt : Zuerst lassen wir uns über glGetObjectParameterivARB mitteilen wie lang der aktuelle Inoflog ist. Sollte dort tatsächlich etwas drinstehen (blen > 1), dann lassen wir uns dessen Inhalt via glGetInfoLogARB in InfoLog ausgeben und liefern diesen als Ergebnis zurück.

Wie bereits gesagt wird nur nach dem Kompilieren eines Shaders bzw. dem Linken eines Programmobjektes ein Infolog erstellt. Es bietet sich dadurch an, direk danach einen solchen Aufruf zu machen :

glCompileShaderARB(VertexShaderObject);
ShowMessage(glSlang_GetInfoLog(VertexShaderObject));

Wenn unser Vertex Shader komplett fehlerfrei kompiliert werden konnte, dann sehen wir als Ergebnis nur einen leeren Dialog. Ist dies nicht der Fall, so werden wir vom Treiber mit recht detaillierten Fehlerinformationen "belohnt", z.B. so :

GLSL error vshader.jpg

Auch das Infolog nach dem Linken des Programmobjektes dürfte, selbst wenn keine Fehler vorkommen, recht interessant sein, das sieht dann nämlich so aus :

GLSL info programobject.jpg

Wie zu sehen wird uns nach dem erfolgreichen Linken auch gesagt ob und welcher Shader in Hardware bzw. Software läuft. Für Debuggingzwecke sicherlich eine mehr als brauchbare Information.


Parameterübergabe

Uniformparmater (mehr dazu später) stellen die Schnittstelle zwischen eurem Programm und dem Shader dar, werden also genutzt um Daten aus dem Programm heraus an einen Shader zu übergeben. Zur Übergabe dieser Parameter bietet OpenGL diverse Funktionen, die alle Abkömmlinge von glUniformARB sind. Während mit glUniform4fARB z.B. ein Vier-Komponentenvektor an das Programmobjekt übergeben wird, kann man mittels glUniformMatrix4fvARB ganze Matrizen schnell und einfach übergeben. Ausserdem gibt es nun die Möglichkeit Uniformparameter direkt über ihren Namen, statt wie unter ARB_FP/VP über einen festen Index zu adressieren. Die Funktion glGetUnifromLocationARB gibt anhand des übergebenen Parameternamens dessen Position zurück. Man kann also ganz einfach über den Namen drauf zugreifen :

glUniform3fARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('LightPosition')), LPos[0], LPos[1], LPos[2]);
glUniform1iARB(glGetUniformLocationARB(ProgramObject, PGLCharARB('texSamplerTMU3')), 3);


Wichtig ist hier, das man je nach Parametertyp auch die passende Anzahl von Argumenten übergibt. Also für einen 4-Komponenten Floatvektor glUniform4fARB und für einen einfachen Integerwert (z.B. Textureinheit für einen Sampler) glUnifrom1iARB. Auch nicht vergessen dürft ihr, das die Namen der Parameter genauso wie im Shader geschrieben werden müssen, also Groß- und Kleinschreibung beachtet werden müssen.



Hinweis: Dieser Artikel ist noch unvollständig.
(Mehr Informationen/weitere Artikel)

{{{1}}}

Incomplete.jpg

Rest folgt.



Vorhergehendes Tutorial:
-
Nächstes Tutorial:
tutorial_glsl2

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.