Grafikdateien (Textur-Daten) mithilfe von SDL-Image laden

Was den Umgang mit Texturen angeht, kommt DirectX insbesondere den Programmieranfängern sehr entgegen. Beschäftigt man sich jedoch mit OpenGL, ist schon ein wenig mehr Aufwand erforderlich, um eine Textur zu laden.

Zunächst müssen die in einer Grafikdatei gespeicherten Daten ausgelesen werden, wobei auf das verwendete Grafikformat zu achten ist. Im zweiten Schritt müssen diese Daten mehr oder weniger aufwändig bearbeitet werden (definieren der transparenten Bereiche mithilfe eines Color Keys, berechnen einer Normal Map aus einer Height Map, usw.). Im letzten Schritt werden die Texturdaten in den Speicher der Grafikkarte kopiert, wo sie uns dann als „Textur“ zur Verfügung stehen.



Zum gegenwärtigen Zeitpunkt werden in unseren Programmbeispielen zwei Grafikformate unterstützt – das PNG-Format (Portable Network Graphics) und das BMP-Format (Bitmaps), beide mit jeweils 24-Bit-Farbtiefe (True Color, etwa 16,78 Millionen Farben).

Die Farben werden bei beiden Formaten im sogenannten RGB-Farbraum gespeichert. Hierbei stehen für jeden Farbkanal (Rot, Grün, Blau) jeweils 8 Bit zur Verfügung (256 Farbabstufungen).

Über das Format müssen wir uns beim Laden einer Grafikdatei zunächst keine Gedanken machen. Die SDL-Image-Funktion IMG_Load() benötigt lediglich den Speicherpfad einer existierenden Grafikdatei. Als Rückgabewert erhalten wir dann einen Zeiger auf einen SDL_Surface-Datentyp, in dem alle eingelesenen Informationen gespeichert sind. Werden diese Daten zu einem späteren Zeitpunkt nicht mehr benötigt, kann der belegte Speicherbereich mithilfe der Funktion SDL_FreeSurface() wieder freigegeben werden.
Um die eingelesenen Grafikdaten korrekt weiterverarbeiten zu können, benötigen wir die Zusatzinformation, ob nun eine BMP- oder eine PNG-Grafik eingelesen wurde. Beim Einlesen einer BMP-Datei werden die Farbkanäle in der SDL_Surface in der Reihenfolge Rot-Grün-Blau (RGB) gespeichert, beim Einlesen einer PNG-Datei in der Reihenfolge Blau-Grün-Rot (BGR).
Die Information über das verwendete Grafikformat kann anhand der Dateiendung ermittelt werden:

bool isPNG = false;

long len = strlen(FileName);

long i;

for(i = 0; i < len; i++)
{
    if(FileName[i] == '.')
    {
        if(FileName[i+1] == 'p' || FileName[i+1] == 'P')
        {
            isPNG = true;
            break;
        }
    }
}

Nachdem das verwendete Grafikformat ermittelt wurde, kann die Grafikdatei mithilfe der Funktion IMG_Load() geladen werden.

SDL_Surface *image = IMG_Load(FileName);

if(!image)
{
    Add_To_Log(FileName,"IMG_Load() failed");
}

// Höhe, Breite, Farbtiefe (BytesPerPixel) und
// Anzahl der Farbpixel ermitteln:

Width  = image->w;
Height = image->h;

long BytesPerPixel = image->format->BytesPerPixel;

long NumPixel = Width * Height;

Um die Farbinformationen in den Speicher der Grafikkarte kopieren zu können, müssen wir ein Datenarray (data) vom Typ GLuint anlegen. Dieser vorzeichenlose 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 (für spätere Transparenzberechnungen im Fragment Shader).

GLuint* data;

data = new GLuint[NumPixel];

ZeroMemory(data, NumPixel*sizeof(GLuint));

Zugriff auf die einzelnen Farbpixel in der SDL_Surface erhalten wir mithilfe des pColor-Zeigers:

unsigned char* pColor = (unsigned char*)image->pixels;

Nun können wir Pixel für Pixel den Rot-, Grün- und Blaufarbwert aus dem jeweiligen Farbkanal auslesen und bei Bedarf bearbeiten. Im konkreten Fall definieren wir mithilfe von Color Keys die transparenten Bereiche der Textur und speichern diese im Alpha-Kanal. Zu diesem Zweck werden die ausgelesenen Farbwerte der einzelnen Kanäle mit den jeweiligen TransparentColorBorder-Werten verglichen. Betrachten wir hierzu zwei Beispiele:

Beispiel 1:

// Color Key:
TransparentColorBorderBlue  = 10;
TransparentColorBorderRed   = 10;
TransparentColorBorderGreen = 10;

// Farbwerte:
colorBlue  = 5;
colorRed   = 5;
colorGreen = 5;

Pixel ist transparent, da alle Farbwerte kleiner als die zugehörigen TransparentColorBorder-Werte sind (bsp. colorBlue < TransparentColorBorderBlue)

Beispiel 2:

// Color Key:
TransparentColorBorderBlue  = 10;
TransparentColorBorderRed   = 10;
TransparentColorBorderGreen = 10;

// Farbwerte:
colorBlue  = 200;
colorRed   = 5;
colorGreen = 5;

Pixel ist sichtbar, da der Blau-Farbwert größer als der zugehörige TransparentColorBorder-Wert ist (colorBlue > TransparentColorBorderBlue).
Im letzten Schritt müssen wir die einzelnen Farbkanäle miteinander kombinieren und in einem unbenutzten GLuint-Datenarray-Element speichern.
Nachdem alle Farbwerte Pixel für Pixel in das GLuint-Array übertragen worden sind, können die Daten in den Speicher der Grafikkarte kopiert werden (Textur- und Sampler-Objekte werden im nächsten Artikel ausführlich behandelt). Da nun weder die SDL_Surface noch das GLuint-Array weiter benötigt werden, kann der von ihnen belegte Speicherplatz wieder freigegeben werden.

long Index, j, k;

// Farbkanäle:
unsigned char colorRed;
unsigned char colorGreen;
unsigned char colorBlue;
unsigned char colorAlpha;

k = 0;

for(i = 0; i < Height; i++)
{
    for(j = 0; j < Width; j++)
    {
        Index = BytesPerPixel*j + i*Width*BytesPerPixel;

        // Farbwerte der einzelnen Farbkanäle auslesen:
        if(isPNG == false)
        {
            colorBlue  = pColor[Index];
            colorGreen = pColor[Index+1];
            colorRed   = pColor[Index+2];
        }
        else
        {
            colorBlue  = pColor[Index+2];
            colorGreen = pColor[Index+1];
            colorRed   = pColor[Index];
        }

        // transparente Bereiche der Textur bestimmen
        // (Color Keying):
        if(colorBlue > TransparentColorBorderBlue ||
           colorGreen > TransparentColorBorderGreen ||
           colorRed > TransparentColorBorderRed)
            colorAlpha = 255;
        else
           
// Pixel ist vollständig transparent:
            colorAlpha = 0;

        // Farbkanäle miteinander kombinieren und in einem
        // unbenutzten GLuint-Array-Element speichern:
        *(data+k) = (colorAlpha << 24) +
                    (colorRed << 16)  +
                    (colorGreen << 8)  +
                     colorBlue;

        k++;
    }
}

// an dieser Stelle wird die eigentliche Textur erzeugt:
. . .

// Speicherbereich der SDL_Surface wieder freigeben:
SDL_FreeSurface(image);

SAFE_DELETE_ARRAY(data)