SDL-Tutorial #9 - OpenGL: Blits

Autor: Nicolai 'Prefect' Haehnle

Seit dem letzten Tutorial können wir mit OpenGL farbige Rechtecke auf den Bildschirm bringen. Auf Dauer sind Farbverläufe allerdings nicht so berauschend. Man müßte ein Bild auf die Rechtecke „kleben“ können, um so richtige Blits vornehmen zu können. Und genau dafür sind Texturen da.

In diesem Tutorial werde ich zeigen, wie man die Bilddaten aus einer SDL_Surface in eine OpenGL-Textur laden und diese Textur dann auf den Bildschirm bringen kann.

Texturen

Texturen sind rechteckige Flächen, deren Seitenlänge immer Zweierpotenzen sind. Texturen müssen aber nicht quadratisch sein. Die maximale Texturgröße ist von der OpenGL-Implementation (im Klartext: vom Treiber) abhängig. Ältere Karten/Treiber haben ein Maximum von 256×256, neuere haben auch mit 2048×2048 keine Probleme.
Texturen können auch einen 1 Pixel breiten Rand haben. Das ist eventuell interessant, wenn ein größeres Bild in mehrere kleinere Texturen aufgespalten und danach noch gestreckt wird. Dieser Texturrand ist aber mit Vorsicht zu genießen: als ich für die Tiles in Return to the Shadows probehalber einen Texturrand verwendet habe ist die Framerate auf meiner TNT2-basierten Grafikkarte auf unter ein vierzigstel (!) gesunken.

Es gibt übrigens auch eindimensionale und dreidimensionale Texturen (zumindest bei OpenGL Version 1.3), aber die finden eher selten Verwendung - für manche Effekte können sie dennoch durchaus nützlich sein. Ich gehe jedoch nur auf zweidimensionale Texturen ein.

Erstellen von Texturen

Der erste Schritt beim Arbeiten mit Texturen ist natürlich, die Texturen in den Speicher zu laden, und da haperts oft schon ;)
Ich stelle hier eine simple Methode vor, um Texturen aus SDL-Surfaces zu holen. Das hat den netten Seiteneffekt, daß man Bibliotheken wie SDL_image verwenden kann.
SDL_image kann eine Vielzahl von Bilddateien einladen. Es gibt aber auch diverse Hilfsbibliotheken für OpenGL, die ebenfalls die Arbeit erledigen. Und man kann Bitmaps und andere Bildformate auch immer per Hand laden ;)

Den Code zum Laden einer Textur habe ich in eine Funktion gepackt. Sie lädt zunächst eine Bitmapdatei in eine SDL-Surface und erledigt dann die nötigen Schritte, um die Bilddaten in eine OpenGL-Textur zu kopieren.

Um den Überblick zu behalten: Es bietet sich an, die Texturdaten immer im 32bit RGBA-Format zu halten. Mit diesem Format kann man generell am einfachsten umgehen, und es bietet die maximale Farbtiefe. Wenn der Alphakanal nicht benötigt wird kann man OpenGL anweisen, ihn einfach zu ignorieren.

Genug der Vorrede:

unsigned LoadTexture(const char *filename)
{
    SDL_Surface *bitmap, *conv;
    unsigned texture;
 
    bitmap = SDL_LoadBMP(filename);
    if (!bitmap) {
        fprintf(stderr, "%s: konnte nicht geladen werden\n", filename);
        exit(1);
    }

Bis jetzt dürfte noch alles bekannt sein. Der Rückgabewert der Funktion ist vielleicht etwas überraschend. Im Gegensatz zu vergleichbaren APIs sind Texturnamen, wie sie im OpenGL-Standard genannt werden, einfach positive Zahlen. Die Zahl 0 wird als ungültige Textur angesehen. Statt einer Struktur oder eines COM-Objekts (oh mein Gott!) hat man eine simple Zahl.

Die Surface bitmap ist jetzt natürlich in dem Format, in dem die Bitmapdatei im Editor gespeichert wurde. Wir benötigen die Bilddaten aber in einem 32bit-Format.

    conv = SDL_CreateRGBSurface(SDL_SWSURFACE, bitmap->w, bitmap->h, 32,
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
            0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff);
#else
                        0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
#endif
    SDL_BlitSurface(bitmap, 0, conv, 0);

Das Umwandeln geht einiges einfacher, als man zuerst befürchten könnte. Ich erstelle hier zunächst eine leere Surface im gewünschten Farbformat, nämlich 32 Bit mit Alphakanal. Die ersten vier Parameter dürften leicht verständlich sein: die Surface soll im Systemspeicher liegen, sie soll gleich groß sein wie das geladene Bitmap, und sie soll 32 Bit Farbtiefe haben.
Die nächsten vier Parameter geben an, wie die verschiedenen Farbkanäle aufgeteilt werden. Der Rotkanal soll immer in dem Byte liegen, daß die niedrigste Speicheradresse hat, danach kommt der Grün-, dann der Blau- und schließlich der Alphakanal. Hier muß man, ähnlich wie im Tutorial #7 beim direkten Pixelzugriff auf eine Surface, auf das Byteordering des Prozessors achten.

Wenn die Surface dann erstellt wurde blitten wir einfach das Bitmap in die neue Surface. SDL kümmert sich dann automatisch um die Konvertierung von Farben.
Übrigens ist diese Verwendung von SDL_BlitSurface durchaus zulässig, obwohl wir uns im OpenGL-Modus befinden, denn dieser Blit greift nicht auf die Bildschirmsurface zu.

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, conv->pitch / conv->format->BytesPerPixel);
    glTexImage2D(GL_TEXTURE_2D, 0, 3, conv->w, conv->h, 0, GL_RGBA,
        GL_UNSIGNED_BYTE, conv->pixels);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);

Das ist der eigentlich interessante Teil der Funktion. Zunächst wird mit glGenTextures eine Textur erstellt. Im Prinzip kann man mit dieser Funktion beliebig viele Texturen auf einmal erstellen: der erste Parameter ist die Zahl der gewünschten Texturen, der zweite Parameter ist ein Zeiger auf ein Array aus unsigned ints das mit den Texturnummern gefüllt wird.
In unserem Fall besteht das „Array“ eben aus einem einzigen Element.

Als nächstes teilen wir OpenGL mit glBindTexture mit, daß wir mit der neu erstellten Textur als zweidimensionale Textur arbeiten möchten.

Dann wird OpenGL angewiesen, den linearen Filter zu verwenden, falls die Textur bei der Bildschirmausgabe gedehnt werden muß. Im Prinzip kann man für stauchen (GL_TEXTURE_MIN_FILTER) und strecken (GL_TEXTURE_MAG_FILTER) verschiedene Filter verwenden (bei Mipmapping wird das auch gemacht).
Solange man die Texturen nicht dehnt kann man statt GL_LINEAR eventuell auch GL_NEAREST verwenden. Wenn Texturen allerdings gedehnt werden (wie in diesem Tutorial der Fall sein wird) sieht GL_NEAREST sehr gepixelt und daher nicht besonders schön aus.

Da ja eine Pixelreihe eventuell mehr Speicher als Anzahl der Pixel mal Byte pro Pixel einnimmt teilen wir OpenGL nun mit glPixelStorei die tatsächliche Länge einer Pixelreihe mit. Später wird dieser Wert wieder auf den Standard, nämlich 0, gebracht. Wenn dieser Wert auf 0 gesetzt ist geht OpenGL davon aus, daß eine Pixelreihe genau Anzahl Pixel mal Byte pro Pixel Speicher einnimmt. Da unsere Surface im Systemspeicher liegt müßte pitch eigentlich genau diesen Wert haben, und man könnte sich die beiden Aufrufe von glPixelStorei auch sparen. Aber ich gehe einfach mal auf Nummer sicher.

Erst mit glTexImage2D wird die Textur dann tatsächlich mit den dazugehörigen Bilddaten erstellt. Ich nehme die Parameter nun einmal einzeln auseinander.

GL_TEXTURE_2D gibt an, daß wir eine zweidimensionale Textur verwenden wollen. Wie schon vorher gesagt gibt es auch ein- und dreidimensionale Texturen, auf die ich aber nicht näher eingehen werde.

0 ist das Mipmappinglevel, oder Level of Detail-Level. Da wir kein Mipmapping verwenden ist der Parameter 0. Mipmapping ist in 3D-Anwendungen sinnvoll, wenn Texturen sehr weit weg (d.h. stark verkleinert) erscheinen, da dann auch der übliche GL_LINEAR-Filter für gute Qualität beim Stauchen der Texture nicht mehr ausreicht.

3 ist das „Format“, das von der Grafikkarte zum Ablegen der Textur verwendet wird. Dabei gibt diese Zahl lediglich an, daß die Textur drei Komponenten (Rot, Grün und Blau) hat. Für Texturen mit verwendeter Alphakomponente würde man hier 4 angeben. Es gibt auch vordefinierte Konstanten, mit denen man die verwendete Farbtiefe exakt bestimmen kann, z.B. GL_RGB5 (5 Bit pro Farbkanal) oder GL_RGB8 (8 Bit pro Farbkanal).

conv→w und conv→h sind natürlich Breite und Höhe der Textur.

0 ist die Dicke des Rands der Textur. Der Rand ist entweder null oder einen Pixel breit, und wenn man meinen Erfahrungen trauen darf sollte man ihn besser auf null belassen ;)

GL_RGBA gibt an, daß unser Programm die Bilddaten im RGBA-Format an OpenGL schickt. Der Alpha-Anteil wird allerdings ignoriert, da das im dritten Parameter angegebene interne Format keinen Alphakanal besitzt.

GL_UNSIGNED_BYTE bedeutet, daß jede einzelne Farbkomponente als vorzeichenloses Byte vorliegt.

conv→pixels ist selbstverständlich der Zeiger auf die Bilddaten. Da die Surface ja im Systemspeicher liegt muß sie nicht erst gelockt werden.

Jetzt muß lediglich noch reservierter Speicher freigegeben werden:

    SDL_FreeSurface(bitmap);
    SDL_FreeSurface(conv);
 
    return texture;
}

Übrigens können die Bilddaten einer einmal erstellten Textur durchaus wieder verändert werden. Dies geht durch einen einfachen Aufruf von glTexImage2D. Oft muß aber lediglich ein Teilbereich der Textur verändert werden. Dafür gibt es die effizientere Funktion glSubTexImage2D. Darüber hinaus gibt es natürlich die Möglichkeit, Teile des Bildschirms in eine Textur zu kopieren. Das ist mit glCopyTexImage2D machbar. Diese Funktionen arbeiten sehr ähnlich wie glTexImage2D, ich werde daher erstmal nicht näher auf sie eingehen.

... und nun auf den Bildschirm

Fehlt noch das eigentliche Texturieren. Ich will nun ein einfaches Bild auf den Bildschirm bringen. Es handelt sich dabei um das galaxien.bmp aus dem zweiten Tutorial.
Allerdings ist dieses Bild auf Grund seiner Breite und Höhe nicht als Textur geeignet: Texturen müssen ja eine Zweierpotenz als Breite und Höhe haben. Ich habe das Bitmap einfach in einem Editor auf 256×256 gescaled. Im Ernstfall sollte man natürlich, falls möglich, eine höhere Auflösung verwenden, oder das Bild in mehrere Teile aufspalten.
Ich habe 256×256 gewählt, weil diese Texturgröße von allen OpenGL-Implementationen korrekt unterstützt werden müßte.

Wir benötigen als erstes eine neue globale Variable um den Texturindex zu speichern:

#include <SDL.h>
 
unsigned g_texPicture;
 
unsigned LoadTexture(const char *filename)

Dann muß die Textur zu Programmbeginn geladen werden. Dies geschieht nach der Initialisierung von SDL:

    screen = SDL_SetVideoMode(640, 480, 0, SDL_OPENGL);
    if (!screen) {
        fprintf(stderr, "Konnte Bildschirmmodus nicht setzen: %s\n",
            SDL_GetError());
        exit(1);
    }
 
    g_texPicture = LoadTexture("galaxien.bmp");
 
    glViewport(0, 0, screen->w, screen->h);

Die Textur sollte auch wieder gelöscht werden, wenn das Programm beendet wird. Dies geschieht mit glDeleteTextures. glDeleteTextures verwendet genau die selben Parameter wie glGenTextures:

        SDL_GL_SwapBuffers();
    }
 
    glDeleteTextures(1, &g_texPicture);
 
        return 0;
}

Nun da die Textur erfolgreich eingeladen wurde muß sie nur noch auf den Bildschirm gebracht werden. Dies geschieht natürlich in der Funktion RefreshScreen, die nun wie folgt aussieht:

void RefreshScreen()
{
    glColor3ub(255, 255, 255);
 
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, g_texPicture);
 
    glBegin(GL_QUADS);
        glTexCoord2f(0, 0);    glVertex2i(0, 0);
        glTexCoord2f(1, 0);    glVertex2i(640, 0);
        glTexCoord2f(1, 1);    glVertex2i(640, 480);
        glTexCoord2f(0, 1);    glVertex2i(0, 480);
    glEnd();
}

Zunächst fällt auf, daß glClear nicht mehr aufgerufen wird. Das ist auch nicht mehr notwendig, da das Quad nun den gesamten Bildschirm überschreibt. Zusätzliche Buffer wie der Z-Buffer werden nicht verwendet, so daß auch diese nicht gelöscht werden müssen.

Der Aufruf von glColor setzt die Farbe immer auf weiß, damit die Textur auch so dargestellt wird, wie sie im Bitmap vorliegt (ändert die Farbe doch einfach mal um zu sehen was passiert…).

Über glEnable teilen wir OpenGL mit, daß beim Zeichnen von Polygonen von jetzt an Texturen verwendet werden sollen. Wenn dieser Aufruf weggelassen wird erscheinen alle Polygone so, als ob keine Textur vorhanden wäre (auch wenn glBindTexture aufgerufen wird), in diesem Fall also komplett weiß. Um später wieder Polygone ohne Textur darstellen zu können muß glDisable(GL_TEXTURE_2D) aufgerufen werden.
Das Funktionspaar glEnable/glDisable wird fast immer benötigt um bestimmte Effekte an- und auszuschalten.

Wie schon in LoadTexture teilen wir OpenGL mit glBindTexture mit, welche Textur verwendet werden soll.

Schließlich folgt der gewohnte Begin/End-Block. Wir rufen nun aber die Funktion glTexCoord2f auf. Wie der Name vermuten läßt bestimmt diese Funktion, welcher Punkt der Textur auf den Eckpunkt gelegt werden soll, der als nächstes angegeben wird. Im Prinzip funktioniert diese Funktion genauso wie glColor: Man könnte den Aufruf von glTexCoord vor einem der glVertex-Aufrufe auch weglassen. Das ist aber wohl nur selten erwünscht…
Es ist wichtig zu beachten, daß (0,0) in der linken oberen Ecke der Textur liegt, während (1,1) die rechte untere Ecke der Textur beschreibt. Für Koordinaten, die außerhalb dieses Bereichs liegen, wird die Textur normalerweise gekachelt. Dieses Kacheln kann mit glTexParameter auch ausgestellt werden. Meistens ist es jedoch erwünscht.

Wenn man alle 1er in den glTexCoord-Aufrufen durch 2er ersetzt erscheint die Textur ganze vier mal auf dem Bildschirm statt nur einmal. Wenn man die Vorzeichen ändert wird die Textur gespiegelt. Wenn man jeden x-Wert um eine Konstante Zahl erhöhr wird die Textur nach links verschoben, usw. Manche dieser Effekte kann man natürlich auch erreichen indem man die Eckpunkte des Quads vertauscht, oder indem man mehrere kleinere Quads rendert, etc… Genauso kann man die Textur auf dem Quad drehen oder auf aberwitzigste Art verzerren. Die Kombinationsmöglichkeiten sind endlos.

Das war's! Viel Spaß beim Blitten mit OpenGL :)

Der Quellcode zu diesem Tutorial sowie die Makefile und MSVC++-Projektdateien sind zum Download verfügbar: (sdl_tut9.tgz sdl_tut9.zip)

Anmerkung

Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.

Die Verwendung aller Dokumente einschließlich der Abbildungen ausschließlich zu nichtkommerziellen Zwecken. Verbreitung des Dokuments auf Speichermedien, (insbesondere auf CD-ROMs als Beilage zu Zeitschriften und Magazinen oder sog. "Mission-Packs" etc.) ist untersagt.
 
coding/grafik_api/sdl_tut_9.txt · Zuletzt geändert: 2010/07/29 20:49 von Adrian_Broher