OpenGL-Sampler-Objekte

Thema des heutigen Artikels sind die mit den OpenGL Versionen 3.3 und 4.0 neu eingeführten Sampler-Objekte. Gegenüber früheren OpenGl Versionen ist es nun das erste Mal möglich, Texturfilter-Einstellungen separat von den eigentlichen Grafikdaten zu handhaben, wodurch im Wesentlichen die folgenden beiden Nachteile beseitigt werden. Da viele der verwendeten Texturen naturgemäß die gleichen Filtereinstellungen (z.B. Mip Mapping, anisotropische Filterung, usw.) nutzen, ist es völlig unnötig, diese mehrfach (zigfach) zu speichern. Darüber hinaus ist es der Performance abträglich, wenn bei jedem Texturwechsel auch die Filtereinstellungen neu gesetzt werden müssen, selbst wenn sich diese gar nicht ändern. Die mithilfe eines Sampler-Objekts festgelegten Filtereinstellungen werden so lange verwendet, bis man die Einstellungen mithilfe eines anderen Sampler-Objekts wieder ändert.


Zusätzlich zu den Filtereinstellungen wird mithilfe der Sampler-Objekte auch der Textur-Adressmodus (Texture Address Mode) festgelegt. Dieser bestimmt, wie eine Textur auf eine Polygonfläche gemappt wird – unter Berücksichtigung der Texturkoordinaten der Flächen-Eckpunkte (Vertices). Arbeitet man mit zweidimensionalen Texturen, dann benötigt man entsprechend zwei Koordinaten. Standardmäßig wird ein solches Textur-Koordinatenpaar durch die Buchstaben u und v symbolisiert (wir verwenden die Bezeichnungen tu und tv, um auf den Verwendungszweck hinzuweisen: texture-u und texture-v). Beim Einsatz von Cube Maps im Rahmen des Environment-Mappings benötigt man drei Texturkoordinaten (u v w Mapping).
Die nachfolgende Abbildung verdeutlicht den Umgang mit Texturkoordinaten und demonstriert die Texturierung eines Vertexquads sowie die zweier Dreiecke:


































Damit eine Textur vollständig auf ein Polygon gemappt wird, müssen Texturkoordinaten in einem Bereich von 0 bis 1 verwendet werden. Dabei repräsentiert das Koordinatenpaar (0, 0) in unserer Abbildung die linke obere Ecke und das Koordinatenpaar (1, 1) die rechte untere Ecke des als Textur verwendeten Bitmaps.
Im Standard-Textur-Adressmodus (Texture Wrap: GL_REPEAT) lässt sich die Textur auch mehrfach über ein Polygon mappen. Hierbei müssen Texturkoordinaten > 1 bzw. < 0 verwendet werden.

Nutzt man eine Textur als Render Target, dann bietet sich die Verwendung des Adressmodus (Texture Wrap: GL_CLAMP_TO_EDGE) an. Hierbei werden lediglich Texturkoordinaten in einem Bereich von 0 bis 1 berücksichtigt. Die Textur kann dann also nicht mehrfach auf ein Polygon gemappt werden.

Kommen wir nun zu den Texturfilter-Einstellungen. Die folgenden Methoden werden heutzutage von jeder Grafikkarte unterstützt:

Nearest Point Sampling: In Abhängigkeit von der Größe einer Textur lässt sich jedem Texel (Textur-Pixel) ein mehr oder weniger großer Texturkoordinaten-Bereich zuordnen (Für eine 1*1 Textur erstreckt sich der Bereich von 0 bis 1; für eine 2*2 Textur beträgt der Texturkoordinaten-Bereich 0.5; für eine 4*4 Textur beträgt der Bereich 0.25; usw.). Liegen die Texturkoordinaten bei einer 2*2 Textur im Bereich (0, 0) bis (0.5, 0.5), dann wird das erste Texel auf die Polygonfläche gemappt. Liegen die Texturkoordinaten in einem Bereich von (0.5, 0) bis (1, 0.5), dann wird das zweite Texel verwendet, liegen die Koordinaten im Bereich von (0, 0.5) bis (0.5, 1), wird das dritte Texel ausgewählt, und für alle Koordinaten im Bereich (0.5, 0.5) bis (1, 1) wird das vierte Texel gezeichnet.
Da die Farbwerte benachbarter Texel nicht gemittelt werden und die Texel je nach Größe der Textur mehrfach nebeneinander auf eine Polygonfläche gemappt werden, wirkt das Resultat mitunter wie eine grobe Klötzchengrafik.

Bilineare Texturfilterung (auch lineare Filterung genannt): Es wird ein quadratischer Bereich der Textur (2*2 Texel) – isotropes Sample genannt – für die Berechnung der gemittelten Texelfarbe verwendet. Richtig gut funktioniert die Technik nur dann, wenn die zu texturierende Fläche senkrecht zum Betrachter steht. In einem anderen Winkel sind aus Sicht des Betrachters die Bereiche, die zur Filterung verwendet werden, nicht mehr quadratisch, wodurch es zu Störeffekten und Unschärfen kommen kann.

Anisotrope Filterung: Hierbei wird in Abhängigkeit vom Blickwinkel des Betrachters eine größere Anzahl von Texeln bei der Berechnung der gemittelten Texelfarbe verwendet. Im Unterschied zur bilineraren Filterung werden hierbei anisotrope Sample-Bereiche berücksichtigt. Die Qualität der anisotropen Filterung können Sie im Kontrollzentrum ihrer Grafikkarte (ATI Catalyst Control Center, NVIDIA Control Panel) festlegen.


Bilineare Texturfilterung mit einfachem Mip-Map-Wechsel: Hierbei kommen zusätzlich zur bilinearen Texturfilterung sogenannte Mipmaps (siehe Kapitel Textur-Objekte) zum Einsatz, um Flimmer-Effekte, die bei der Bewegung der Kamera auftreten, so weit wie möglich zu reduzieren. Die Flimmer-Effekte treten insbesondere bei weiter entfernten Oberflächen in Erscheinung, auf die eine im Verhältnis zur Oberfläche viel zu große Textur gemappt werden soll. Versucht man beispielsweise auf eine 10*10 Pixel große Oberfläche eine 1024*1024 Texel große Textur zu legen, dann müsste ein Texturfilter für eine flimmerfreie Darstellung pro Pixel die Farben von ca. 102*102 Texeln mitteln, was viel zu rechenaufwändig wäre. Da bei der bilinearen Filterung jedoch nur 2*2 Texel berücksichtigt werden, ändert sich der gemittelte Farbwert bei jeder noch so kleinen Positions- und Richtungsänderung. Abhilfe schafft hier die Verwendung einer Mip Map mit einer Auflösung von 16*16 Texeln. Nachteilig an dieser Filtertechnik ist der abrupte Mip-Map-Wechsel, welcher als Linie erkennbar ist, an der sich der Schärfegrad abrupt ändert (MIP Banding).

Hinweis:
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 eineMip-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


Trilineare Texturfilterung: Zur Vermeidung des MIP-Bandings wird die bilineare Texturfilterung mit zwei benachbarten Mip-Map-Stufen durchgeführt. Im Anschluss daran werden beide Resultate gemittelt.

Anisotrope Filterung mit Mipmaps: Hierbei wird die Anisotrope Filterung mit dem Einsatz von Mipmaps kombiniert. Auch hier kann der Mip-Map-Wechsel entweder abrupt erfolgen oder es werden wie bei der trilinearen Filterung zwei benachbarten Mip-Map-Stufen berücksichtigt.


Sampler-Objekte initialisieren

Kommen wir nun zur Initialisierung der Sampler-Objekte. Erzeugt wird ein neuer Textur-Sampler mithilfe der glGenSamplers()-Methode. In unseren Programmbeispielen erfolgt die Initialisierung aller benötigten Sampler-Objekte bei Programmstart innerhalb der Funktion Init_SamplerObjects(). Damit die einzelnen Sampler-Objekte global verfügbar sind, verwenden wir globale Sampler-Objekt-ID-Variablen. Insgesamt erzeugen wir vier verschiedene Sampler-Objekte – zwei für das Texture Mapping (mit bzw. ohne Mip Maps) sowie zwei Render To Texture Sampler, die in Verbindung mit 2D-Texturen bzw. Cube Maps zum Einsatz kommen.

Hinweis:
Unsere Sampler-Objekte verwenden standardmäßig bilineare Filterung mit und ohne Mip Maps. Darüber hinausgehende Einstellungen wie die Verwendung von anisotropischer Filterung, Mip Map Details, usw. müssen im Kontrollzentrum ihrer Grafikkarte (ATI Catalyst Control Center, Nvidia Control Panel) getroffen werden.


// zur Auswahl stehende Sampler-Objekte:

//
Texture Mapping mit Verwendung von Mip Maps:
GLuint g_MipMappingSamplerID          = 0;

//
Texture Mapping ohne Verwendung von Mip Maps:
GLuint g_NonMipMappingSamplerID       = 0;

// Render To Texture, 2D-Texturen:
GLuint g_RenderToTextureSamplerID     = 0;

// Render To Texture, Cube Maps:
GLuint g_RenderToCubeTextureSamplerID = 0;

// Das im jeweiligen Texture Stage aktuell verwendete
// Sampler-Objekt:
// Hinweis:
// Wir legen 10 Stages als Obergrenze fest (dies ermöglicht
// die Verarbeitung von bis zu 10 Texturen im Fragment Shader)
GLuint g_IdOfUsedSamplerObject[10];

INLINE void Init_SamplerObjects(void)
{
for(long i = 0; i < 10; i++)
    g_IdOfUsedSamplerObject[i] = 0;

glGenSamplers(1, &g_MipMappingSamplerID);

glSamplerParameteri(g_MipMappingSamplerID, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR_MIPMAP_LINEAR);
glSamplerParameteri(g_MipMappingSamplerID, GL_TEXTURE_MAG_FILTER,
                    GL_LINEAR);
glSamplerParameteri(g_MipMappingSamplerID, GL_TEXTURE_WRAP_S,
                    GL_REPEAT);
glSamplerParameteri(g_MipMappingSamplerID, GL_TEXTURE_WRAP_T,
                    GL_REPEAT);


glGenSamplers(1, &g_NonMipMappingSamplerID);

glSamplerParameteri(g_NonMipMappingSamplerID, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR);
glSamplerParameteri(g_NonMipMappingSamplerID, GL_TEXTURE_MAG_FILTER,
                    GL_LINEAR);
glSamplerParameteri(g_NonMipMappingSamplerID, GL_TEXTURE_WRAP_S,
                    GL_REPEAT);
glSamplerParameteri(g_NonMipMappingSamplerID, GL_TEXTURE_WRAP_T,
                    GL_REPEAT);


glGenSamplers(1, &g_RenderToTextureSamplerID);

glSamplerParameteri(g_RenderToTextureSamplerID, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR);
glSamplerParameteri(g_RenderToTextureSamplerID, GL_TEXTURE_MAG_FILTER,
                    GL_LINEAR);
glSamplerParameteri(g_RenderToTextureSamplerID, GL_TEXTURE_WRAP_S,
                    GL_CLAMP_TO_EDGE);
glSamplerParameteri(g_RenderToTextureSamplerID, GL_TEXTURE_WRAP_T,
                    GL_CLAMP_TO_EDGE);


glGenSamplers(1, &g_RenderToCubeTextureSamplerID);

glSamplerParameteri(g_RenderToCubeTextureSamplerID,
                    GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glSamplerParameteri(g_RenderToCubeTextureSamplerID,
                    GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glSamplerParameteri(g_RenderToCubeTextureSamplerID,
                    GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glSamplerParameteri(g_RenderToCubeTextureSamplerID,
                    GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glSamplerParameteri(g_RenderToCubeTextureSamplerID,
                    GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
}

Einen Textur-Sampler verwenden

Mithilfe der glBindSampler()-Methode können Sie das zu verwendende Sampler-Objekt auswählen. Bitte beachten Sie, dass das gewählte Sampler-Objekt nur für einen einzelnen Texture Stage Gültigkeit hat:

glBindSampler(TextureStage, g_MipMappingSamplerID);

Sollen in einem Fragment Shader beispielsweise 3 Texturen verarbeitet werden, und soll auf alle Texturen der gleiche Sampler angewendet werden, so sind drei glBindSampler()-Aufrufe erforderlich:

glBindSampler(0, g_MipMappingSamplerID);
glBindSampler(1, g_MipMappingSamplerID);
glBindSampler(2, g_MipMappingSamplerID);

Hinweis:
Die ID des im jeweiligen Texture Stage aktuell verwendeten Sampler-Objekts speichern wir im g_IdOfUsedSamplerObject-Array ab. Bei einem Texturwechsel kann dann anhand der Array-Eintragungen überprüft werden, welcher Sampler gerade aktiv ist. Soll für die neue Textur der gleiche Sampler verwendet werden wie für die alte Textur, kann man sich so einen unnötigen Sampler-Wechsel sparen.


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



Sampler-Objekte freigeben

Werden die Sampler-Objekte nicht mehr benötigt, so muss der von ihnen belegte Speicherplatz mithilfe der glDeleteSamplers()-Methode wieder freigegeben werden. In unseren Programmbeispielen erfolgt die Freigabe bei Programmende innerhalb der Funktion Delete_SamplerObjects().

INLINE void Delete_SamplerObjects(void)
{
    if(g_MipMappingSamplerID > 0)
        glDeleteSamplers(1, &g_MipMappingSamplerID);

    if(g_NonMipMappingSamplerID > 0)
        glDeleteSamplers(1, &g_NonMipMappingSamplerID);

    if(g_RenderToTextureSamplerID > 0)
        glDeleteSamplers(1, &g_RenderToTextureSamplerID);

    if(g_RenderToCubeTextureSamplerID > 0)
        glDeleteSamplers(1, &g_RenderToCubeTextureSamplerID);
}