GLSL Shader Framework Entwurf

Eine der wichtigsten Neuerungen mit Einführung der OpenGL Version 3 war zweifelsohne der Wegfall der Fixed-Function-Pipeline. Zwar konnte bereits in früheren OpenGL Versionen die komplette Szenendarstellung (Beleuchtungsberechnungen, Animationen, Schatten, usw.) mithilfe von sogenannten Shader-Programmen realisiert werden, jedoch blieb ihr Einsatz optional. Besonders für Programmieranfänger bedeutet die Verwendung der OpenGL Versionen 3 und 4 einen nicht zu unterschätzenden Mehraufwand, da die Shader Programmierung nun zu einem Pflichtfach wird und man gezwungen ist, Transformations- und Beleuchtungsroutinen selbstständig zu implementieren.



In OpenGL besteht jedes Shader Programm aus mehreren Shader Typen, mindestens jedoch aus einem Vertex Shader und einem Fragment Shader.

Überblick über die verschiedenen Shader Typen wie sie in der Render-Pipeline zum Einsatz kommen:

Vertex Shader: Der Vertex Shader ist für die Verarbeitung der Vertexdaten eines 3D-Modells verantwortlich.

Tessellation Control Shader (DirectX: Hull Shader): Der Tessellation Control Shader berechnet für eine detailarme Oberfläche (ein grobes Dreiecksnetz) eine Reihe von Kontrollpunkten, mit deren Hilfe der Tessellator im Anschluss daran die Oberfläche in ein feingliedrigeres Dreiecksnetz unterteilt (Subdivision).

Tessellation Evaluation Shader (DirectX: Domain Shader): Der Tessellation Evaluation Shader kommt nach dem Tessellator zum Einsatz und berechnet die Positionen der durch die Tessellation neu erzeugten Vertices. Auf diese Weise lassen sich einem 3D-Modell neue Oberflächendetails hinzufügen.

Geometry Shader: Der Geometry Shader kommt bei Bedarf nach der Verarbeitung der Vertexdaten im Vertex Shader oder nach beendeter Tessellation zum Einsatz. Mithilfe dieses Shaders lassen sich neue geometrische Primitiven (Punkte, Linien, Dreiecke) aus bereits vorhandenen Primitiven erzeugen und in die Render-Pipeline einfügen. Zudem ist es möglich, geometrische Primitiven aus der Render-Pipeline zu entfernen.
Achtung, der Geometry Shader eignet sich nur für kleine Modifikationen!!!

Fragment Shader (DirectX: Pixel Shader): Der Fragment Shader ist für die Beleuchtung und Texturierung eines 3D-Modells auf Pixelbasis verantwortlich. Pixelbasierte Berechnungen erfolgen stets nach der Verarbeitung der Vertexdaten im Vertex Shader bzw. Geometry Shader.


Im Rahmen des heutigen Artikels werden wir uns zunächst einen Überblick über das in den Programmbeispielen verwendete GLSL Shader Framework verschaffen. Schauen wir uns einmal an, wie sich mithilfe der CGLSLShader-Klasse ein Shader Programm bestehend aus einem Vertex Shader (der Source Code befindet sich in der Datei TestShader.vert) und Fragment Shader (der Source Code befindet sich in der Datei TestShader.frag) initialisieren lässt (siehe hierfür Programmbeispiel 2). Als dritter Parameter muss das für die 3D-Modelle verwendete Vertexformat angegeben werden, damit die Vertexdaten im Vertex Shader korrekt verarbeitet werden können:

TestShader = new CGLSLShader;
TestShader->Init_UserDefined_Shader("Shader/TestShader.vert",
                                    "Shader/TestShader.frag",
                                    Format_CSimpleTexturedVertex);

Hinweis:
Mit den Details der Implementierung befassen wir uns im nächsten Artikel.


Wird ein Shader Programm nicht mehr benötigt, muss die betreffende Instanz der CGLSLShader-Klasse wieder gelöscht werden:

SAFE_DELETE(TestShader)


Die CGLSLShader-Instanz TestShader ist für die Verarbeitung von texturierten Vertices ausgelegt (Vertexposition + Texturkoordinaten). Für die Darstellung eines texturierten Vertexquads (siehe hierfür Programmbeispiel 2) sind nun folgende CGLSLShader-Methodenaufrufe erforderlich:

// Den zu verwendenden Shader auswählen:
TestShader->Use_Shader();

// Matrix für die Transformation der Vertices an die korrekte
// Bildschirmposition (siehe TestShader.vert) übergeben:
TestShader->Set_ShaderMatrix4X4(&WorldViewProjectionMatrix,
                                "matWorldViewProjection");

// Hinweis:
// Die Namen der in den Shadern verwendeten Variablen sind in roter
// Schrift dargestellt!!

// Textur übergeben (siehe TestShader.frag):
TestShader->Set_Texture(GL_TEXTURE0, 0, pTexture, "TestTexture");

// Zusätzliche Farbe übergeben, die mit der Texturfarbe kombiniert
// werden soll (siehe TestShader.frag):
TestShader->Set_ShaderFloatVector4(&D3DXVECTOR4(1.5f, 1.5f, 1.5f, 1.0f),
                                   "AmbientColor");

// Vertexbuffer und Indexbuffer des zu rendernden Vertexquads binden:
glBindBuffer(GL_ARRAY_BUFFER, VertexBufferId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IndexBufferId);

// Vertex Attribute setzen, damit die Vertexdaten vom
// Vertex Shader korrekt verarbeitet werden können:
TestShader->Set_VertexAttributes();

// Draw Call – mit dem Rendern des Vertexquads beginnen:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);

// Arbeit mit dem Shader beenden:
TestShader->Stop_Using_Shader();


Kommen wir nun zum Entwurf der CGLSLShader-Klasse. Bitte beachten Sie, dass alle bisher veröffentlichten Programmbeispiele lediglich Vertex Shader und Fragment Shader verwenden:

class CGLSLShader
{
public:

    // ID des Shader-Programm-Objekts:
    GLuint ShaderProgram;

    // ID-Werte der einzelnen Shader Typen
    // (Teile des Shader Programms):

    GLuint VertexShader, FragmentShader;

    // Funktionszeiger zum Festlegen der Vertex Attribute:
    // Hinweis:
    // Welche Funktion zum Setzen der Vertex Attribute verwendet wird
    // ist abhängig vom jeweiligen Vertexformat, dass der Methode
    // Init_UserDefined_Shader() als Parameter übergeben wird!

    void (*pSetVertexAttributes)(GLuint*);
    void (*pResetVertexAttributes)(GLuint*);

    // ID des Vertex Array Objekts zum Speichern der Vertex Attribute
    // des verwendeten Vertexformats. Notwendig, damit der
    // Vertex Shader die Vertexdaten korrekt interpretieren kann!
    GLuint VertexArrayObjectID;

    long TextureStagesUsed;
    long CubeTextureStagesUsed;

    CGLSLShader();
    ~CGLSLShader();

    // Shader Programm initialisieren:
    void Init_UserDefined_Shader(char* pVertexShaderFile,
                                 char* pFragmentShaderFile,
                                 long VertexFormat);

    // Bevor der Shader verwendet werden kann, muss als erstes die
    // Use_Shader()-Methode aufgerufen werden:
    void Use_Shader(void);

    // Hinweis:
    // Die Use_Shader()-Methode bindet das Vertex Array Objekt und
    // aktiviert das Shader Programm.

    // Wird der Shader nicht mehr benötigt, muss die Stop_Using_Shader()-
    // Methode aufgerufen werden:

    void Stop_Using_Shader(void);

    // Vertex Attribute setzen, damit die Vertexdaten vom
    // Vertex Shader korrekt verarbeitet werden können:
    void Set_VertexAttributes(void);

    // Hinweis:
    // Durch den Aufruf der Set_VertexAttributes()-Methode wird
    // zudem das Vertex Array Objekt aktualisiert, das beim Aufruf der
    // Use_Shader()-Methode gebunden wird.

    // Zurücksetzen der Vertex Attribute:
    void Reset_VertexAttributes(void);

    // Einzelne Variablen und Arrays an den Shader übergeben
    // (Vektoren, Matrizen, float und int Variablen):

    // Hinweis:
    // pParameter: Name der jeweiligen Shader-Variable, an welche
    // die Wertübergabe erfolgen soll.

    void Set_ShaderFloatValue(float Value, char* pParameter);
    void Set_ShaderIntValue(int Value, char* pParameter);

    void Set_ShaderFloatArray(float* pArray, long NumArrayElements,
                              char* pParameter);
    void Set_ShaderIntArray(int* pArray, long NumArrayElements,
                            char* pParameter);

    void Set_ShaderFloatVector4(D3DXVECTOR4* pVec, char* pParameter);
    void Set_ShaderIntVector4(int* pVec, char* pParameter);

    void Set_ShaderFloatVector3Array(D3DXVECTOR3* pVecArray,
                                     long NumArrayElements,
                                     char* pParameter);

    void Set_ShaderFloatVector4Array(D3DXVECTOR4* pVecArray,
                                     long NumArrayElements,
                                     char* pParameter);

    void Set_ShaderFloatVector3(D3DXVECTOR3* pVec, char* pParameter);
    void Set_ShaderIntVector3(int* pVec, char* pParameter);

    void Set_ShaderFloatVector2(float* pVec, char* pParameter);
    void Set_ShaderIntVector2(int* pVec, char* pParameter);

    void Set_ShaderMatrix4X4(D3DXMATRIXA16* pMatrix, char* pParameter);
    void Set_ShaderMatrix4X4Array(D3DXMATRIXA16* pMatrixArray,
                                  long NumArrayElements, char* pParameter);

    void Set_ShaderMatrix3X3(D3DXMATRIXA16* pMatrix, char* pParameter);
    void Set_ShaderMatrix3X3Array(D3DXMATRIXA16* pMatrixArray,
                                  long NumArrayElements, char* pParameter);

    void Set_ShaderMatrix2X2(D3DXMATRIXA16* pMatrix, char* pParameter);
    void Set_ShaderMatrix2X2Array(D3DXMATRIXA16* pMatrixArray,
                                  long NumArrayElements,
                                  char* pParameter);

    // Texturen an den Shader übergeben:
    void Set_Texture(long GL_TEXTURE_Nr, long Stage, CTexture* pTexture,
                     char* pParameter);

    void Set_CubeTexture(long GL_TEXTURE_Nr, long Stage,
                         CTexture* pTexture, char* pParameter);
};