OpenGL-Texture-Array-Objekte

Geometry Instancing bietet uns die Möglichkeit, eine große Anzahl von 3D-Objekten (Terrain Tiles, Nebelwolken, Sterne, Asteroiden, etc.) mit nur wenigen Draw Calls darzustellen. Während man sich zunächst darauf beschränkte, die einzelnen Objekte lediglich individuell zu skalieren, wurde es mit der Einführung der Texture-Arrays zum ersten Male möglich, die Objekte auch gesondert zu texturieren, ohne dass man hierfür jede Textur einzeln an einen Shader übergeben muss. Auch kann bei der Auswahl der zu verwendenden Textur auf komplexe Conditional Statements verzichtet werden. Wie das nachfolgende Fragment-Shader-Beispiel zeigt, ist der Zugriff auf ein Texture-Array denkbar einfach:

uniform sampler2DArray NebulaTextureArray;

[...]

void main()
{
// Hinweise:
// gs_TexCoord[0].z:  Index der zu verwendenden Textur
// gs_TexCoord[0].xy: die normalen Texturkoordinaten

vec4 NebulaColor = texture(NebulaTextureArray, gs_TexCoord[0].xyz);

[...]

}



Für die Übergabe eines Texture-Arrays an einen Fragment Shader verwenden wir in unseren Programmbeispielen die CGLSLShader-Framework-Methode Set_TextureArray():

Shader->Set_TextureArray(0, pNebulaTextureArray, g_MipMappingSamplerID,
                            "NebulaTextureArray");


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

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

        glActiveTexture(GL_TEXTURE0+Stage);
        glBindTexture(GL_TEXTURE_2D_ARRAY, pTexture->TextureID);

        if(g_IdOfUsedSamplerObject[Stage] != SamplerID)
        {
            g_IdOfUsedSamplerObject[Stage] = SamplerID;
            glBindSampler(Stage, SamplerID);
        }

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

    g_StageTexture[Stage] = pTexture;
}


Bei der Initialisierung eines neuen Texture-Arrays bietet sich die folgende Vorgehensweise an:
Zunächst werden die Bildinformationen sämtlicher Texturen nacheinander mithilfe von SDL_Image in den RAM-Speicher kopiert. In diesem Zusammenhang verhindern die beiden Helper-Funktionen Enable_TextureArrayGeneration() sowie Disable_TextureArrayGeneration(), dass diese Daten nicht auch automatisch in den Texturspeicher der Grafikkarte übertragen werden.
Als konkretes Beispiel betrachten wir die Erzeugung eines Texture-Arrays, welches im Rahmen des Graphics-And-Physics-Framework-Programmbeispiels Procedural Galaxy Rendering für die volumetrische Nebeldarstellung benötigt wird:

void C3DNebulaSystem::Set_NewTexture(char* pTextureFile)
{
    if(NumTexturesUsed >= NumTexturesMax)
        return;

    // Texturdaten werden zunächst in ein Daten-Array im RAM-Speicher
    // kopiert (ppNebulaTexture beansprucht keinerlei Texturspeicher!):
    Enable_TextureArrayGeneration();

    ppNebulaTexture[NumTexturesUsed] = Load_Texture_And_Get_ResourceID(
            &NebulaTextureResourceID[NumTexturesUsed],
            pTextureFile, TRUE);

    NumTexturesUsed++;

    Disable_TextureArrayGeneration();
}

void Enable_TextureArrayGeneration(void)
{
    // Texturdaten sollen nicht in den Grafikspeicher übertragen werden
    // Dies geschieht erst dann, wenn das Texture Array erzeugt wird!!
    g_Enable_TextureArrayGeneration = true;
}

void Disable_TextureArrayGeneration(void)
{
    g_Enable_TextureArrayGeneration = false;
}

Nachdem alle benötigten Texturen mithilfe der C3DNebulaSystem-Methode Set_NewTexture() in den RAM-Speicher eingelesen wurden, wird das eigentliche Texture-Array nun auf folgende Weise erzeugt:

  • Anlegen eines leeren Texture-Array-Objekts (pNebulaTextureArray).
  • Anlegen eines temporären Arrays (pTextureArray), in dem die Speicheradressen der einzelnen Textur-Ressourcen (ppNebulaTexture) abgelegt werden.
  • Kopieren der Grafikdaten aus dem RAM- in den Grafikspeicher des Texture-Arrays unter Zuhilfenahme des temporären pTextureArray-Arrays mithilfe der CTexture-Methode Build_TextureArray().
  • Löschen des temporären Arrays sowie Freigabe der nun nicht mehr benötigten ppNebulaTexture-Ressourcen.

void C3DNebulaSystem::Build_TextureArray(void)
{
    if(NumTexturesUsed < 1)
        return;

    // einzigartige Bezeichnung für das Texture Array generieren:
    long UniqueID = GetTickCount();
    char strBuffer[100];
    sprintf(strBuffer,"3DNebulaTextureArray_%d", UniqueID);

    // leeres Texture-Array-Objekt anlegen:
    pNebulaTextureArray = Get_Texture_And_ResourceID(
                          &NebulaTextureArrayResourceID, strBuffer);

    CTexture** pTextureArray = new CTexture*[NumTexturesUsed];

    long i;

    for(i = 0; i < NumTexturesUsed; i++)
        pTextureArray[i] = ppNebulaTexture[i];

    // alle Grafikdaten in das Texture-Array-Objekt kopieren:
    pNebulaTextureArray->Build_TextureArray(pTextureArray, NumTexturesUsed);

    SAFE_DELETE_ARRAY(pTextureArray)

    if(NumTexturesUsed > 0)
    {
        for(i = 0; i < NumTexturesUsed; i++)
            Release_Texture(&ppNebulaTexture[i]);
    }
}


Um den entscheidenen Schritt – das Kopieren der Texturdaten aus dem RAM- in den Grafikspeicher – besser nachvollziehen zu können, betrachten wir uns die Build_TextureArray()-Methode unserer CTexture-Klasse im Folgenden einmal etwas genauer:

void CTexture::Build_TextureArray(CTexture** ppTextureList,
                                  long NumTextures)
{
    if(TextureObject_Initialized == true)
        return;

    if(TextureID > 0)
        glDeleteTextures(1, &TextureID);

    // leeres Texture-Array-Objekt im Grafikspeicher anlegen:
    glGenTextures(1, &TextureID);
    glBindTexture( GL_TEXTURE_2D_ARRAY, TextureID);

    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, ppTextureList[0]->Width,
                 ppTextureList[0]->Height, NumTextures, 0, GL_BGRA,
                 GL_UNSIGNED_BYTE, NULL);

    long i;

    for(i = 0; i < NumTextures; i++)
    {
    // Texturdaten nacheinander in den Grafikspeicher des Texture-Arrays
    // kopieren:
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i,
                    ppTextureList[i]->Width, ppTextureList[i]->Height, 1,
                    GL_BGRA, GL_UNSIGNED_BYTE,
                    ppTextureList[i]->TextureDataRAM);
    }

    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);

    for(i = 0; i < NumTextures; i++)
        ppTextureList[i]->Delete_Texture();

    RenderToTexture = FALSE;
    GenerateMipMaps = TRUE;

    TextureObject_Initialized = true;
}