GLSL Shader Programme in eine OpenGL-Anwendung laden

Aufbauend auf den vorangegangenen Artikel befassen wir uns heute damit, wie sich GLSL Shader Programme in eine OpenGL-Anwendung laden lassen.
In unseren Programmbeispielen wird der Source Code der verwendeten Shader in separaten Dateien im Ordner Shader gespeichert, der seinerseits im Verzeichnis Bin zu finden ist. Vertex Shader Dateien sind durch die Dateiendung vert gekennzeichnet, Fragment Shader Dateien durch die Endung frag.


Für OpenGL-Anwendungen, die eine sehr große Anzahl von unterschiedlichen Shadern verwenden, sind separate Shader Dateien mitunter von Nachteil. Zum einen ist es zeitaufwendig, eine Vielzahl von Dateien zu öffnen und auszulesen und zum anderen möchte (bzw. darf) man als Entwickler den Shader Source Code nicht jedermann zugänglich machen.

Eine mögliche Lösung für dieses Problem besteht nun darin, den Source Code aus den separaten Dateien mithilfe eines geeigneten Tools in eine Header-Datei zu kopieren und diese dann in die entsprechende Anwendung einzubinden. Für jeden Shader wird hierbei ein simples char-Array zum Abspeichern des Source Codes generiert.

Hinweis:
Wir werden zu diesem Thema noch ein entsprechendes Programmbeispiel samt Tool veröffentlichen.


Auslesen einer Shader-Datei

Zurück zu unseren Programmbeispielen. Da sich der Shader Source Code in separaten Dateien befindet, benötigen wir zunächst einmal eine Methode zum Auslesen des Codes. Hierbei kommt die Funktion ReadTextFile() zum Einsatz, die uns als Rückgabewert ein char-Array mitsamt dem Source Code liefert:

INLINE char* ReadTextFile(char *pFileName)
{
    FILE *fp;
    char *content = NULL;

    int count=0;

    Add_To_Log(pFileName,"opening...");

    if(pFileName != NULL)
    {
        fp = fopen(pFileName, "rt");

        if(fp != NULL)
        {
            fseek(fp, 0, SEEK_END);
            count = ftell(fp);
            rewind(fp);

            if(count > 0)
            {
                content = (char *)malloc(sizeof(char) * (count+1));
                count = fread(content,sizeof(char),count,fp);
                content[count] = '\0';
            }
            fclose(fp);
        }
        else
            Add_To_Log(pFileName,"file not found");
    }
    return content;
}


Protokollierung der Kompilierungs-Informationen

Fehler bei der Programmierung neuer Shader lassen sich natürlich niemals völlig ausschließen. Aus diesem Grund benötigen wir eine Helper-Funktion für die Protokollierung sämtlicher Kompilierungs-Informationen:

INLINE void printShaderInfoLog(GLuint obj)
{
    int infologLength = 0;
    int charsWritten  = 0;
    char *infoLog;

    glGetShaderiv(obj, GL_INFO_LOG_LENGTH,&infologLength);

    if(infologLength > 0)
    {
        infoLog = (char *)malloc(infologLength);
        glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog);

        Add_To_Log(infoLog);
        free(infoLog);
    }
}


Initialisierungsschritte

Verantwortlich für die Initialisierung eines neuen Shader-Programms ist die CGLSLShader-Methode Init_UserDefined_Shader(). Als Parameter erwartet die Methode die Namen der Dateien (vollständige Pfadangabe), in denen der Vertex Shader bzw. Fragment Shader Source Code gespeichert ist, sowie die Angabe des für die 3D-Modelle verwendeten Vertexformats, damit die Vertexdaten im Vertex Shader korrekt verarbeitet werden können. Momentan werden die folgenden Formate unterstützt:

enum VertexFormats {Format_CSimpleTexturedVertexWithNormal,
                    Format_CSimpleTexturedVertexWithNormalEx,
                    Format_CSimpleTexturedVertex,
                    Format_CTexturedVertexWithNormal_NM,
                    Format_CTexturedAnimatedVertexWithNormal_NM,
                    Format_CScreenVertex,
                    Format_CSimpleVertex,
                    Format_CSimpleDoubleTexturedVertex};


Betrachten wir nun die einzelnen Initialisierungsschritte:

Erzeugen eines Vertex Array Objekts mithilfe von glGenVertexArrays() (Wird zum Speichern der Vertex Attribute des verwendeten Vertexformats benötigt. Notwendig, damit der Vertex Shader die Vertexdaten korrekt interpretieren kann.)

Erzeugen eines Vertex Shader und Fragment Shader Objekts mithilfe der Funktion glCreateShader().

Den Source Code von Vertex und Fragment Shader mithilfe von ReadTextFile() aus den zugehörigen externen Dateien auslesen.

Den ausgelesenen Source Code mithilfe von glShaderSource() an das Vertex Shader bzw. Fragment Shader Objekt übergeben.

Kompilieren des an die Shader Objekte weitergeleiteten Source Codes mithilfe der Funktion glCompileShader().

Kompilierungs-Informationen protokollieren.

Erzeugen eines Shader Programm Objekts mithilfe von glCreateProgram().

Sowohl das Vertex Shader als auch das Fragment Shader Objekt mithilfe von glAttachShader() an das Shader Programm Objekt anhängen und den bereits kompilierten Shader Code mithilfe von glLinkProgram() zu einem auf der Grafikkarte ausführbaren Programm linken.

Den Funktionszeigern pSetVertexAttributes und pResetVertexAttributes mithilfe des Funktionsparameters VertexFormat die Adressen derjenigen Funktionen zuweisen, mit deren Hilfe sich die beim Rendern zu berücksichtigenden Vertex Attribute festlegen bzw. zurücksetzen lassen (siehe hierzu den Artikel Verwendete Vertexformate in den OpenGL-Tutorials, Vertex-Attribute für einen GLSL Vertex Shader verfügbar machen).

void CGLSLShader::Init_UserDefined_Shader(char* pVertexShaderFile,
                                          char* pFragmentShaderFile,
                                          long VertexFormat)
{
    glGenVertexArrays(1, &VertexArrayObjectID);
    glBindVertexArray(VertexArrayObjectID);

    VertexShader   = glCreateShader(GL_VERTEX_SHADER);
    FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

    char* vs  = NULL;
    char* fs  = NULL;

    vs = ReadTextFile(pVertexShaderFile);
    fs = ReadTextFile(pFragmentShaderFile);

    // Typecast ist notwendig, da glShaderSource() einen Zeiger auf
    // die Adresse eines konstanten Zeichenketten-Arrays erwartet!
    const char * vv = vs;
    const char * ff = fs;

    glShaderSource(VertexShader, 1, &vv, NULL);
    glShaderSource(FragmentShader, 1, &ff, NULL);

    free(vs);
    free(fs);

    glCompileShader(VertexShader);
    glCompileShader(FragmentShader);

    printShaderInfoLog(VertexShader);
    printShaderInfoLog(FragmentShader);

    ShaderProgram = glCreateProgram();
    glAttachShader(ShaderProgram, VertexShader);
    glAttachShader(ShaderProgram, FragmentShader);

    glLinkProgram(ShaderProgram);

    if(VertexFormat == Format_CSimpleTexturedVertexWithNormal)
    {
        pSetVertexAttributes =
        Set_Attributes_CSimpleTexturedVertexWithNormal;
        pResetVertexAttributes =
        Reset_Attributes_CSimpleTexturedVertexWithNormal;
    }
    else if(VertexFormat == Format_CSimpleTexturedVertexWithNormalEx)
    {
        pSetVertexAttributes =
        Set_Attributes_CSimpleTexturedVertexWithNormalEx;
        pResetVertexAttributes =
        Reset_Attributes_CSimpleTexturedVertexWithNormalEx;
    }
    else if(VertexFormat == Format_CSimpleTexturedVertex)
    {
        pSetVertexAttributes =
        Set_Attributes_CSimpleTexturedVertex;
        pResetVertexAttributes =
        Reset_Attributes_CSimpleTexturedVertex;
    }
    else if(VertexFormat == Format_CTexturedVertexWithNormal_NM)
    {
        pSetVertexAttributes =
        Set_Attributes_CTexturedVertexWithNormal_NM;
        pResetVertexAttributes =
        Reset_Attributes_CTexturedVertexWithNormal_NM;
    }
    else if(VertexFormat == Format_CTexturedAnimatedVertexWithNormal_NM)
    {
        pSetVertexAttributes =
        Set_Attributes_CTexturedAnimatedVertexWithNormal_NM;
        pResetVertexAttributes =
        Reset_Attributes_CTexturedAnimatedVertexWithNormal_NM;
    }
    else if(VertexFormat == Format_CScreenVertex)
    {
        pSetVertexAttributes = Set_Attributes_CScreenVertex;
        pResetVertexAttributes = Reset_Attributes_CScreenVertex;
    }
    else if(VertexFormat == Format_CSimpleVertex)
    {
        pSetVertexAttributes = Set_Attributes_CSimpleVertex;
        pResetVertexAttributes = Reset_Attributes_CSimpleVertex;
    }
    else if(VertexFormat == Format_CSimpleDoubleTexturedVertex)
    {
        pSetVertexAttributes =
        Set_Attributes_CSimpleDoubleTexturedVertex;
        pResetVertexAttributes =
        Reset_Attributes_CSimpleDoubleTexturedVertex;
    }
}


Aufräumarbeiten

Wird ein Shader Programm nicht länger benötigt, muss die betreffende CGLSLShader-Instanz zerstört und der durch die zugehörigen OpenGL Objekte belegte Speicherplatz wieder freigegeben werden:

CGLSLShader::~CGLSLShader()
{
    glDeleteVertexArrays(1, &VertexArrayObjectID);

    pSetVertexAttributes = NULL;
    pResetVertexAttributes = NULL;

    glDetachShader(ShaderProgram, VertexShader);
    glDetachShader(ShaderProgram, FragmentShader);

    glDeleteShader(VertexShader);
    glDeleteShader(FragmentShader);
    glDeleteProgram(ShaderProgram);
}