GLSL Shader – Aktivieren eines Shader Programms, Übergabe von Variablen und Texturen

Bereits im Artikel GLSL Shader Framework Entwurf wurde Ihnen an einem konkreten Beispiel die Übergabe von Variablen und Texturen an ein Shader Programm demonstriert. Auf die Details der Parameterübergabe sind wir zum damaligen Zeitpunkt jedoch noch nicht eingegangen. Im heutigen Artikel werden wir dies unter anderem nachholen. Darüber hinaus werden wir uns mit den Einzelheiten der Implementierung unseres Shader Frameworks – der CGLSLShader-Klasse – befassen.




Basis für alle Parameterübergaben in OpenGL sind die glUniform-Funktionen:

glUniform1f: einen Parameter vom Typ float übergeben
glUniform2f: zwei Parameter vom Typ float übergeben
glUniform3f: drei Parameter vom Typ float übergeben
glUniform4f: vier Parameter vom Typ float übergeben

glUniform1i: einen Parameter (int, Textur oder bool) übergeben
glUniform2i: zwei Parameter (int, Textur oder bool) übergeben
glUniform3i: drei Parameter (int, Textur oder bool) übergeben
glUniform4i: vier Parameter (int, Textur oder bool) übergeben

glUniform1fv: Array von float-Variablen übergeben
glUniform2fv: Array von float[2]-Vektoren übergeben
glUniform3fv: Array von float[3]-Vektoren übergeben
glUniform4fv: Array von float[4]-Vektoren übergeben

glUniform1iv: Array von int-Variablen übergeben
glUniform2iv: Array von int[2]-Vektoren übergeben
glUniform3iv: Array von int[3]-Vektoren übergeben
glUniform4iv: Array von int[4]-Vektoren übergeben

glUniformMatrix2fv: 2x2-Matrix (einzeln oder als Array) übergeben
glUniformMatrix3fv: 3x3-Matrix (einzeln oder als Array) übergeben
glUniformMatrix4fv: 4x4-Matrix (einzeln oder als Array) übergeben

glUniformMatrix2x3fv: 2x3-Matrix (einzeln oder als Array) übergeben
glUniformMatrix2x4fv: 2x4-Matrix (einzeln oder als Array) übergeben

glUniformMatrix3x2fv: 3x2-Matrix (einzeln oder als Array) übergeben
glUniformMatrix3x4fv: 3x4-Matrix (einzeln oder als Array) übergeben

glUniformMatrix4x2fv: 4x2-Matrix (einzeln oder als Array) übergeben
glUniformMatrix4x3fv: 4x3-Matrix (einzeln oder als Array) übergeben


Um nun sicherzustellen, dass die Parameterübergabe an die korrekte Shader Variable erfolgt, muss mithilfe der glGetUniformLocation()-Funktion zunächst die Speicherposition (location) der Variable ermittelt werden. Wie in OpenGL üblich erhalten wir als Rückgabewert jedoch nicht die Speicheradresse selbst sondern stattdessen einen vorzeichenlosen 32-Bit-ID-Wert vom Typ GLuint.

Die Vielzahl an Möglichkeiten bei der Parameterübergabe mithilfe der glUniform-Funktionen lassen sich anhand der nachfolgenden CGLSLShader-Methoden gut nachvollziehen:


einzelne float-Variablen an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatValue(float Value, char* pParameter)
{
    glUniform1f(glGetUniformLocation(ShaderProgram, pParameter), Value);
}

Beispiel:

pSurfaceShader->Set_ShaderFloatValue(BrightnessDistanceFactor,
                                     "BrightnessDistanceFactor");


einzelne int-Variablen (Ganzzahlen) an einen Shader übergeben:

void CGLSLShader::Set_ShaderIntValue(int Value, char* pParameter)
{
    glUniform1i(glGetUniformLocation(ShaderProgram, pParameter), Value);
}


Arrays von float-Variablen an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatArray(float* pArray,
                                       long NumArrayElements,
                                       char* pParameter)
{
    glUniform1fv(glGetUniformLocation(ShaderProgram, pParameter),
                 NumArrayElements, pArray);
}


Arrays von int-Variablen an einen Shader übergeben:

void CGLSLShader::Set_ShaderIntArray(int* pArray,
                                     long NumArrayElements,
                                     char* pParameter)
{
    glUniform1iv(glGetUniformLocation(ShaderProgram, pParameter),
                 NumArrayElements, pArray);
}


einzelne D3DXVECTOR3-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatVector3(D3DXVECTOR3* pVec,
                                         char* pParameter)
{
    glUniform3fv(glGetUniformLocation(ShaderProgram, pParameter), 1,
                 Build_ShaderVector3(pVec));

    // alternativ:
    // glUniform3f(glGetUniformLocation(ShaderProgram, pParameter),
    //             pVec->x, pVec->y, pVec->z);
}


einzelne D3DXVECTOR4-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatVector4(D3DXVECTOR4* pVec,
                                         char* pParameter)
{
    glUniform4fv(glGetUniformLocation(ShaderProgram, pParameter),
                 1, Build_ShaderVector4(pVec));

    // alternativ:
    // glUniform4f(glGetUniformLocation(ShaderProgram, pParameter),
    //             pVec->x, pVec->y, pVec->z, pVec->w);

}

Beispiel:

pSurfaceShader->Set_ShaderFloatVector4(pLightColor, "LightColor");


Arrays von D3DXVECTOR3-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatVector3Array(D3DXVECTOR3* pVecArray,
                                              long NumArrayElements,
                                              char* pParameter)
{
    glUniform3fv(glGetUniformLocation(ShaderProgram, pParameter),
                 NumArrayElements,
                 Build_ShaderVector3Array(pVecArray, NumArrayElements));
}


Arrays von D3DXVECTOR4-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatVector4Array(D3DXVECTOR4* pVecArray,
                                              long NumArrayElements,
                                              char* pParameter)
{
     glUniform4fv(glGetUniformLocation(ShaderProgram, pParameter),
                  NumArrayElements,
                  Build_ShaderVector4Array(pVecArray, NumArrayElements));
}

zweidimensionale float-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderFloatVector2(float* pVec, char* pParameter)
{
    glUniform2fv(glGetUniformLocation(ShaderProgram, pParameter), 1, pVec);
}


zweidimensionale int-Vektoren an einen Shader übergeben:

void CGLSLShader::Set_ShaderIntVector2(int* pVec, char* pParameter)
{
    glUniform2iv(glGetUniformLocation(ShaderProgram, pParameter), 1, pVec);
}


dreidimensionale int-Vektoren (Ganzzahl-Vektoren) an einen Shader übergeben:

void CGLSLShader::Set_ShaderIntVector3(int* pVec, char* pParameter)
{
    glUniform3iv(glGetUniformLocation(ShaderProgram, pParameter), 1, pVec);
}


vierdimensionale int-Vektoren (Ganzzahl-Vektoren) an einen Shader übergeben:

void CGLSLShader::Set_ShaderIntVector4(int* pVec, char* pParameter)
{
    glUniform4iv(glGetUniformLocation(ShaderProgram, pParameter), 1, pVec);
}


einzelne 4x4-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix4X4(D3DXMATRIXA16* pMatrix,
                                      char* pParameter)
{
    glUniformMatrix4fv(glGetUniformLocation(ShaderProgram, pParameter), 1,
                       GL_FALSE /*Matrix nicht transponieren!*/,
                       Build_ShaderMatrix4X4(pMatrix));
}

Beispiel:

pSurfaceShader->Set_ShaderMatrix4X4(&WorldViewProjectionMatrix,
                                    "matWorldViewProjection");


Arrays von 4x4-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix4X4Array(D3DXMATRIXA16* pMatrixArray,
                                           long NumArrayElements,
                                           char* pParameter)
{
    glUniformMatrix4fv(glGetUniformLocation(ShaderProgram, pParameter),
                       NumArrayElements, GL_FALSE,
                       Build_ShaderMatrix4X4Array(pMatrixArray,
                                                  NumArrayElements));
}

Beispiel:

pShader->Set_ShaderMatrix4X4Array(pJointTotalTransformationMatrixArray,
                                  NumJoints,
                                  "JointTotalTransformationMatrixArray");


einzelne 3x3-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix3X3(D3DXMATRIXA16* pMatrix,
                                      char* pParameter)
{
    glUniformMatrix3fv(glGetUniformLocation(ShaderProgram, pParameter), 1,
                       GL_FALSE, Build_ShaderMatrix3X3(pMatrix));
}


Arrays von 3x3-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix3X3Array(D3DXMATRIXA16* pMatrixArray,
                                           long NumArrayElements,
                                           char* pParameter)
{
    glUniformMatrix3fv(glGetUniformLocation(ShaderProgram, pParameter),
                       NumArrayElements, GL_FALSE,
                       Build_ShaderMatrix3X3Array(pMatrixArray,
                                                  NumArrayElements));
}


einzelne 2x2-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix2X2(D3DXMATRIXA16* pMatrix,
                                      char* pParameter)
{
    glUniformMatrix2fv(glGetUniformLocation(ShaderProgram, pParameter), 1,
                       GL_FALSE, Build_ShaderMatrix2X2(pMatrix));
}


Arrays von 2x2-Matrizen an einen Shader übergeben:

void CGLSLShader::Set_ShaderMatrix2X2Array(D3DXMATRIXA16* pMatrixArray,
                                           long NumArrayElements,
                                           char* pParameter)
{
    glUniformMatrix2fv(glGetUniformLocation(ShaderProgram, pParameter),
                       NumArrayElements, GL_FALSE,
                       Build_ShaderMatrix2X2Array(pMatrixArray,
                                                  NumArrayElements));
}


Hinweis:
Die Parameterübergabe an ein Shader Programm ist deutlich performanter, wenn man mit standardisierten Shader-Variablennamen arbeitet!!!

Beispiel: Für die Transformation eines Vertex in den Bildraum soll standardmäßig eine 4x4-Matrix mit dem Namen matWorldViewProjection verwendet werden.
Für die Übergabe dieser speziellen Transformationsmatrix nutzen wir eine eigens hierfür definierte Funktion:

void CGLSLShader::Set_ShaderMatrixWorldViewProjection(
                  D3DXMATRIXA16* pMatrix)
{
    glUniformMatrix4fv(MatrixWorldViewProjectionID, 1,
                       GL_FALSE /*Matrix nicht transponieren!*/,
                       Build_ShaderMatrix4X4(pMatrix));
}

Auf den Aufruf der glGetUniformLocation()-Funktion kann bei der Übergabe der Matrix verzichtet werden. Stattdessen wird mithilfe des MatrixWorldViewProjectionID-Parameters direkt auf die entsprechende Shader Variable zugegriffen. Voraussetzung hierfür ist allerdings, dass die Speicherposition der matWorldViewProjection-Matrix im Shader bereits zuvor bei der Initialisierung des Shader Programms ermittelt wurde:

GLuint MatrixWorldViewProjectionID;

. . .

MatrixWorldViewProjectionID = glGetUniformLocation(ShaderProgram,
                              "matWorldViewProjection");


2D-Texturen an einen Shader übergeben:

Jede Textur muss bei der Übergabe an ein Shader Programm einer sogenannten Textureinheit (Texture Stage) zugeordnet werden. Bevor eine Textureinheit verwendet werden kann, muss diese mithilfe von glActiveTexture() aktiviert werden. Zwecks Identifikation ist jeder Textureinheit eine symbolische Konstante zugeordnet:

Textureinheit i: GL_TEXTUREi

Soll nun beispielsweise die nullte Textureinheit aktiviert werden, dann ist folgender Aufruf erforderlich:

glActiveTexture(GL_TEXTURE0);

Hinweis:
Anhand des Stage Index i lässt sich zudem überprüfen, ob in der Textureinheit i die zu übergebende Textur bereits verwendet wird. Sollte dies der Fall sein, ist eine erneute Übergabe unnötig, und die Methode kann somit vorzeitig verlassen werden (Achtung, jeder unnötige Texturwechsel erfolgt zu Lasten der Performance).

Zusätzlich zur Textur muss auch das in der Textureinheit zu verwendende Sampler-Objekt gebunden werden (siehe Artikel OpenGL-Sampler-Objekte). Um unnötige Bindings zu vermeiden, wird auch hier zunächst überprüft, ob das Sampler-Objekt bereits zuvor gebunden wurde.

Beispiel:

pSurfaceShader->Set_Texture(GL_TEXTURE0, 0, pSurfaceTexture,
                            "AsteroidSurfaceTexture");
pSurfaceShader->Set_Texture(GL_TEXTURE1, 1, pNormalTexture,
                            "AsteroidNormalTexture");


void CGLSLShader::Set_Texture(long GL_TEXTURE_Nr, long Stage,
                              CTexture* pTexture, char* pParameter)
{
    // Textur bereits gesetzt:
    if(g_StageTexture[Stage] == pTexture)
        return;

    if(pTexture)
    {
        if(Stage > TextureStagesUsed)
            TextureStagesUsed = Stage;

        // Texture Stage aktivieren:
        glActiveTexture(GL_TEXTURE_Nr);

        glBindTexture(GL_TEXTURE_2D, pTexture->TextureID);

        // Überprüfen, welches Sampler-Objekt gebunden ist und falls
        // erforderlich ein neues Objekt binden:

        if(pTexture->RenderToTexture == FALSE)
        {
            if(pTexture->GenerateMipMaps == TRUE)
            {
            if(g_IdOfUsedSamplerObject[Stage] != g_MipMappingSamplerID)
            {
                g_IdOfUsedSamplerObject[Stage] = g_MipMappingSamplerID;

                glBindSampler(Stage, g_MipMappingSamplerID);
            }
            }
            else
            {
            if(g_IdOfUsedSamplerObject[Stage] != g_NonMipMappingSamplerID)
            {
                g_IdOfUsedSamplerObject[Stage] = g_NonMipMappingSamplerID;

                glBindSampler(Stage, g_NonMipMappingSamplerID);
            }
            }
        }
        else //if(pTexture->RenderToTexture == TRUE)
        {
        if(g_IdOfUsedSamplerObject[Stage] != g_RenderToTextureSamplerID)
        {
            g_IdOfUsedSamplerObject[Stage] = g_RenderToTextureSamplerID;

            glBindSampler(Stage, g_RenderToTextureSamplerID);
        }
        }

        glUniform1i(glGetUniformLocation(ShaderProgram, pParameter),
                    Stage);
    }

    g_StageTexture[Stage] = pTexture;
}


Cube Maps an einen Shader übergeben:

Die Übergabe von Cube Maps an ein Shader Programm erfolgt genau wie die Übergabe von 2D-Texturen:

void CGLSLShader::Set_CubeTexture(long GL_TEXTURE_Nr, long Stage,
                                  CTexture* pTexture, char* pParameter)
{
    glActiveTexture(GL_TEXTURE_Nr);
    glBindTexture(GL_TEXTURE_CUBE_MAP, pTexture->TextureID);

    if(Stage > CubeTextureStagesUsed)
        CubeTextureStagesUsed = Stage;

    if(g_IdOfUsedSamplerObject[Stage] != g_RenderToCubeTextureSamplerID)
    {
        g_IdOfUsedSamplerObject[Stage] = g_RenderToCubeTextureSamplerID;

        glBindSampler(Stage, g_RenderToCubeTextureSamplerID);
    }

    glUniform1i(glGetUniformLocation(ShaderProgram, pParameter), Stage);
}


Vertex Attribute setzen

Ein 3D-Modell kann erst dann gerendert werden, wenn zuvor sowohl Vertex- wie auch Indexbuffer mithilfe von glBindBuffer() gebunden und darüber hinaus die zu berücksichtigenden Vertex Attribute mithilfe von Set_VertexAttributes() festgelegt worden sind.

Beispiel:

// 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);


Welche Attribute beim Aufruf des pSetVertexAttributes-Funktionszeigers gesetzt werden, hängt vom verwendeten Vertexformat ab, das bei der Initialisierung des Shader Programms der Init_UserDefined_Shader()-Methode als Parameter übergeben werden muss (siehe Artikel GLSL Shader Programme in eine OpenGL-Anwendung laden). Zurücksetzen lassen sich die Vertex Attribute mithilfe der CGLSLShader-Methode Reset_VertexAttributes().

void CGLSLShader::Set_VertexAttributes(void)
{
    pSetVertexAttributes(&ShaderProgram);
}

void CGLSLShader::Reset_VertexAttributes(void)
{
    pResetVertexAttributes(&ShaderProgram);
}


Shader Programm aktivieren (in die Render-Pipeline einbinden) und deaktivieren

Bevor ein Shader Programm verwendet werden kann, muss die CGLSLShader-Methode Use_Shader() aufgerufen werden. Mittels glUseProgram() wird das zu verwendende Shader Programm aktiviert (in die Render-Pipeline eingebunden). Darüber hinaus wird mittels glBindVertexArray() ein Vertex Array Objekt zum Speichern der Vertex Attribute gebunden, welche dann beim späteren Aufruf der Set_VertexAttributes()-Methode gesetzt werden.

void CGLSLShader::Use_Shader(void)
{
    if(g_NumActiveShaders > 0)
        Add_To_Log("Shaders not stopped:", &g_NumActiveShaders);

    glUseProgram(ShaderProgram);
    g_NumActiveShaders++;

    glBindVertexArray(VertexArrayObjectID);

    TextureStagesUsed = 0;
    CubeTextureStagesUsed = 0;
}


Bevor nun ein neues Shader Programm aktiviert werden kann, muss dass laufende Programm mithilfe von Stop_Using_Shader() deaktiviert werden:

void CGLSLShader::Stop_Using_Shader(void)
{
    pResetVertexAttributes(&ShaderProgram);

    glBindVertexArray(0);

    // verwendete Texturen zurücksetzen:
   
if(TextureStagesUsed > CubeTextureStagesUsed)
    {
        for(long i = 0; i <= TextureStagesUsed; i++)
            Set_Texture(GL_TEXTURE0+i, i, NULL, "");
    }
    else
    {
        for(long i = 0; i <= CubeTextureStagesUsed; i++)
            Set_Texture(GL_TEXTURE0+i, i, NULL, "");
    }

    glUseProgram(0);
    g_NumActiveShaders--;

    if(g_NumActiveShaders < 0)
        Add_To_Log("Shaders stopped:", &g_NumActiveShaders);
}