OpenGL-Textur-Objekte

Nachdem wir uns im letzten Artikel damit beschäftigt haben, Grafikdateien mithilfe von SDL-Image zu laden, befassen wir uns heute mit den OpenGL-Textur-Objekten.

Erzeugt wird ein neues Textur-Objekt mithilfe der glGenTextures()-Funktion. Wie wir es bereits beim Anlegen von anderen OpenGL-Objekten her gewohnt sind, erhalten wir als Rückgabewert die Objekt-ID in Form einer vorzeichenlosen 32-Bit-Ganzzahl vom Typ unsigned int, mit deren Hilfe wir im weiteren Verlauf auf unser neu erzeugtes Textur-Objekt zugreifen können. Wird eine Textur schließlich nicht mehr benötigt, so muss der von ihr belegte Speicherplatz mithilfe der glDeleteTextures()-Funktion wieder freigegeben werden.



GLuint TextureID; // ID des Textur-Objekts.

[...]

// neues Textur-Objekt erzeugen:
glGenTextures(1, &TextureID);

[...]

// Speicherplatz eines nicht mehr benötigten
// Textur-Objekts wieder freigeben:
glDeleteTextures(1, &TextureID);


Mit Veröffentlichung der OpenGL Versionen 3.3 und 4.0 wurden die Textur-Objekte in einem entscheidenden Punkt überarbeitet. In früheren Versionen speicherte ein Textur-Objekt nicht nur die Grafikdaten, sondern war darüber hinaus auch für das Handling der Texturfilter-Einstellungen verantwortlich. Da viele der verwendeten Texturen naturgemäß die gleichen Filtereinstellungen (z.B. Mip Mapping, anisotrope Filterung, usw.) nutzen, ist es eigentlich völlig unnötig, diese mehrfach (zigfach) zu speichern. Aus diesem Grund wurden die sogenannten Sampler-Objekte eingeführt, mit denen wir uns im nächsten Artikel befassen werden. Die mithilfe eines Sampler-Objekts festgelegten Filtereinstellungen werden so lange verwendet, bis man die Einstellungen mithilfe eines anderen Sampler-Objekts wieder ändert.


Grafikdaten in ein Textur-Objekt kopieren

Im letzten Artikel haben wir uns damit beschäftigt, Grafikdateien mithilfe von SDL-Image zu laden, die eingelesenen Grafikdaten zu bearbeiten und in einem vorzeichenlosen 32-Bit-Datenarray (data) zu speichern. Damit wie diese Grafikdaten als Textur nutzen können, müssen wir sie nun in den Speicher der Grafikkarte kopieren. Hierbei kommt die Funktion glTexImage2D() zum Einsatz. Mithilfe der glGenerateMipmap()-Funktion können wir zudem eine vollständige Mip-Map-Kette erzeugen. Eine Mip-Map-Kette besteht aus einer Abfolge von Texturen mit abnehmender Auflösung. Jede Mip-Map-Stufe hat dabei die halbe Höhe und Breite der vorangegangenen Stufe.

Beispiel für eine Mip-Map-Kette:

Die Ausgangstextur (Mip-Map-Stufe 0) hat eine Größe von 64 mal 64 Texeln

  • Mip-Map-Stufe 1: Auflösung 32 mal 32 Texel
  • Mip-Map-Stufe 2: Auflösung 16 mal 16 Texel
  • Mip-Map-Stufe 3: Auflösung 8 mal 8 Texel
  • Mip-Map-Stufe 4: Auflösung 4 mal 4 Texel
  • Mip-Map-Stufe 2: Auflösung 2 mal 2 Texel
  • Mip-Map-Stufe 5: Auflösung 1 mal 1 Texel


Hinweis:
Die auskommentierten glTexParameteri()-Anweisungen im nachfolgenden Source Code sollen zeigen, wie die Texturfilter-Einstellungen früher – vor der Einführung der Sampler-Objekte – festgelegt wurden. Die Bedeutung der einzelnen Parameter behandeln wir im nächsten Kapitel, wenn wir uns den Sampler-Objekten widmen.


// Vorzeichenloser 32-Bit-Datentyp bietet genügend Platz für drei
// 8-Bit-Farbkanäle (rot, grün und blau) sowie einen zusätzlichen
// 8-Bit-Alpha-Kanal (siehe vorherigen Artikel). Jedes Element des
//
data-Arrays speichert die Farbe eines Pixels
GLuint* data;

[...]


// zu verwendendes Textur-Objekt auswählen:
glBindTexture(GL_TEXTURE_2D, TextureID);

if
(MipMapping == TRUE)
{
    // Vor der Einführung der Sampler-Objekte, mussten die Textur-
    // Filtereinstellungen für jedes Textur-Objekt separat
    // vorgenommen werden:
    /*
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);*/


    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_BGRA,
                 GL_UNSIGNED_BYTE, data);

   // Hinweise:
   // GL_TEXTURE_2D: die zu erzeugende Textur hat 2 Dimensionen
   // GL_RGBA8:      Texturformat
   // GL_BGRA:       Pixelformat, legt fest, wie die Grafikdaten im
   //                Grafikspeicher verwaltet werden sollen


    glEnable(GL_TEXTURE_2D);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    // keine zusätzlichen Mip Map Texturen erzeugen:
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

    // Vor der Einführung der Sampler-Objekte, mussten die Textur-
    // Filtereinstellungen für jedes Textur-Objekt separat
    // vorgenommen werden:
    /*
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);*/

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_BGRA,
    GL_UNSIGNED_BYTE, data);
}

Texturen als Render Targets

Textur-Objekte kommen heutzutage nicht nur beim Texturieren von 3D-Modellen zum Einsatz, sie können darüber hinaus – vergleichbar mit einer Leinwand – auch als Render Targets (Renderziele) verwendet werden. In diesem Zusammenhang spricht man von sogenannten Render To Texture (RTT) Techniken.
Zu diesem Zweck erzeugt man ein leeres Textur-Objekt, welches genügend Platz zum Speichern einer 3D-Szene bereitstellt.

Schauen wir uns zunächst an, wie man eine leere RGBA8-Textur (4 Farbkanäle mit jeweils 8 Bit – 256 Farbabstufungen) erzeugt:

glGenTextures(1, &TextureID);
glBindTexture(GL_TEXTURE_2D, TextureID);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

// Vor der Einführung der Sampler-Objekte, mussten die Textur-
// Filtereinstellungen für jedes Textur-Objekt separat
// vorgenommen werden:
/*
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);*/

if(alpha == true) // Textur mit Alpha-Kanal
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_BGRA,
                 GL_UNSIGNED_BYTE, 0);
else // Textur ohne Alpha-Kanal
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, Width, Height, 0, GL_BGR,
                 GL_UNSIGNED_BYTE, 0);


Im Zuge des Environment-Mappings kommen häufig sogenannte Cube Maps zum Einsatz. Cube Maps bestehen gewissermaßen aus 6 Einzeltexturen und sind dadurch in der Lage, einen „Rundumblick“ der 3D-Szene zu speichern. Jede dieser Einzeltexturen speichert hierbei einen 90°-Ausschnitt der 3D-Szene aus einem anderen Blickwinkel (positive und negative x-, y- und z-Richtungen):

glGenTextures(1, &TextureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, TextureID);

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0);

// Vor der Einführung der Sampler-Objekte, mussten die Textur-
// Filtereinstellungen für jedes Textur-Objekt separat
// vorgenommen werden:
/*
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S,
                GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T,
                GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R,
                GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,
                GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER,
                GL_LINEAR);*/


// 6 Seiten der Cube Map definieren:
if(alpha == true)
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+0, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+1, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+2, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+3, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+4, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+5, 0, GL_RGBA8,
                 Width, Height, 0, GL_BGRA, GL_UNSIGNED_BYTE, 0);
}
else
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+0, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+1, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+2, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+3, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+4, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X+5, 0, GL_RGB8,
                 Width, Height, 0, GL_BGR, GL_UNSIGNED_BYTE, 0);
}


RGBA8-Integer-Texturen bieten mit ihren 8-Bit-Farbkanälen eine zu geringe Genauigkeit für qualitativ hochwertiges Shadow Mapping oder HDR-Rendering (High Dynamic Range Rendering). Abhilfe schaffen hier Floating-Point-Texturformate, die pro Farbkanal 16-, 32- oder mehr Bit an Information speichern. Bedauerlicherweise haben ATI und NVIDIA zunächst jeder ihr eigenes Süppchen gekocht und ihre eigenen Formate definiert (z.B. GL_RGBA_FLOAT32_ATI sowie GL_FLOAT_RGBA32_NV). Wollte man stattdessen die OpenGL-ARB-Formate verwenden, so wurden diese oftmals nicht unterstützt. Seit dem Erscheinen von OpenGL 3.0 gehören die ARB-Floating-Point-Formate zu den Core-Features und müssen dementsprechend von allen GPUs unterstützt werden:

32-Bit-RGBA-Floating-Point-Texturformat:
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA32F, width, height,0, GL_BGRA,
             GL_FLOAT, 0);


16-Bit-RGBA-Floating-Point-Texturformat:
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA16F, width, height,0, GL_BGRA,
             GL_HALF_FLOAT, 0);


32-Bit-RGB-Floating-Point-Texturformat:
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB32F, width, height,0, GL_BGR,
             GL_FLOAT, 0);


16-Bit-RGB-Floating-Point-Texturformat:
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB16F, width, height,0, GL_BGR,
             GL_HALF_FLOAT, 0);


Möchten Sie Floating-Point-Texturformate auch unter OpenGL 2.0 und 2.1 verwenden, dann ist etwas mehr Aufwand erforderlich.
Der nachfolgende Source Code demonstriert die Suche nach einen geeigneten Floating-Point-Format. Zunächst wird versucht, ein herstellerunabhängiges 32-Bit-Format zu verwenden (GL_RGBA32F_ARB bzw. GL_RGBA32F_ARB). Falls dies nicht möglich ist, wird nach einem herstellerspezifischen (ATI und NVIDIA) Format gesucht. Sollten keinerlei 32-Bit-Formate unterstützt werden, so wird nach einem 16-Bit-Format gesucht.

glGenTextures(1, &TextureID);
glBindTexture(GL_TEXTURE_2D, TextureID);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

// Vor der Einführung der Sampler-Objekte, mussten die Textur-
// Filtereinstellungen für jedes Textur-Objekt separat
// vorgenommen werden:
/*
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);*/

if(alpha == true)
{
// 32-Bit-Floating-Point-Format verwenden wenn möglich:
if(g_Use_32BitInsteadOf16Bit_FloatingPointTextures == TRUE)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA32F_ARB, width, height,0,
             GL_BGRA, GL_FLOAT, 0);

if(glGetError() !=
GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_FLOAT_RGBA32_NV, width, height,0,
             GL_BGRA, GL_FLOAT, 0);
}
if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA_FLOAT32_ATI, width, height,0,
             GL_BGRA, GL_FLOAT, 0);
}

if(glGetError() != GL_NO_ERROR)
    g_Use_32BitInsteadOf16Bit_FloatingPointTextures = FALSE;
}

// wird kein 32-Bit-Floating-Point-Format unterstützt, nach einem
// geeigneten 16-Bit-Floating-Point-Format suchen:
if(g_Use_32BitInsteadOf16Bit_FloatingPointTextures == FALSE)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA16F_ARB, width, height,
             0, GL_BGRA,
GL_HALF_FLOAT, 0);

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_FLOAT_RGBA16_NV, width, height,
             0, GL_BGRA, GL_HALF_FLOAT, 0);
}

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA_FLOAT16_ATI, width, height,
             0, GL_BGRA, GL_HALF_FLOAT, 0);
}}}

else // if(alpha == false)
{
// 32-Bit-Floating-Point-Format verwenden wenn möglich:
if(g_Use_32BitInsteadOf16Bit_FloatingPointTextures == TRUE)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB32F_ARB, width, height,
             0, GL_BGR, GL_FLOAT, 0);

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_FLOAT_RGB32_NV, width, height,
             0, GL_BGR, GL_FLOAT, 0);
}

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB_FLOAT32_ATI, width, height,
             0, GL_BGR, GL_FLOAT, 0);
}

if(glGetError() != GL_NO_ERROR)
    g_Use_32BitInsteadOf16Bit_FloatingPointTextures = FALSE;
}

// wird kein 32-Bit-Floating-Point-Format unterstützt, nach einem
// geeigneten 16-Bit-Floating-Point-Format suchen:
if(g_Use_32BitInsteadOf16Bit_FloatingPointTextures == FALSE)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB16F_ARB, width, height,
             0, GL_BGR, GL_HALF_FLOAT, 0);

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_FLOAT_RGB16_NV, width, height,
             0, GL_BGR, GL_HALF_FLOAT, 0);
}

if(glGetError() != GL_NO_ERROR)
{
glTexImage2D(GL_TEXTURE_2D,0, GL_RGB_FLOAT16_ATI, width, height,
             0, GL_BGR, GL_HALF_FLOAT, 0);
}}}

if(glGetError() !=
GL_NO_ERROR)
    Add_To_Log("Texture Format unsupported");