Lekcja 33. Wczytywanie pliku TGA
Autor: Jarosław 'DarkJarek' Dutka
Oryginał: TGA Loading (Evan 'terminate' Pipho)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson33.zip

Zauważyłem, że wiele ludzi na kanale #gamedev, na forum gamedev i różnych innych miejscach pyta o ładowanie plików TGA. Poniższy kod i wyjaśnienia powinny pokazać Ci jak używać nieskompresowanych plików TGA i skompresowanych metodą RLE.

Zaczniemy od dwóch plików nagłówkowych. W pierwszym będziemy przechowywać strukturę naszej tekstury a w drugim struktury i zmienne używane przez kod.

Jak w każdym pliku nagłówkowym (ang. header file) musimy się zabezpieczyć przed wielokrotnym dołączeniem pliku.

#ifndef __TEXTURE_H__         // Jeżeli nagłówek niezostał zdefiniowany
#define __TEXTURE_H__         // To zrób to

A na końcu pliku:

#endif         // __TEXTURE_H__ Koniec zabezpieczenia

Te trzy linie gwarantują, że plik zostanie dołączony tylko raz. Reszta kodu powinna znaleźć się między pierwszymi dwiema a ostatnią linją.

Do tego pliku nagłówkowego powinniśmy dołączyć pozostałe niezbędne pliki. Dodaj poniższe linie po #define __TEXTURE_H__ command.

#pragma comment(lib, "OpenGL32.lib")         // Dołączamy Opengl32.lib do zlinkowania
#include <windows.h>         // Obsługa okien Windowsa
#include <stdio.h>         // Operacje I/O (wejścia/wyjścia ang. input/output)
#include <gl\gl.h>         // Obsługa OGL'a

Potrzebujemy miejsce do zapamiętania obrazka oraz specyfikacji dla tekstury. Użyjemy poniższej struktury.

typedef struct
{
GLubyte* imageData;         // Wskaźnik na nasz obrazek
GLuint bpp;         // Ilość bpp (bitów na piksel ang. Bits Per Pixel)
GLuint width;         // Szerokość obrazka
GLuint height;         // Wysokość obrazka
GLuint texID;         // ID tekstury potrzebne dla glBindTexture
GLuint type;         // Format obrazka (GL_RGB lub GL_RGBA)
} Texture;

Następnie w drugim pliku nagłówkowym (tga.h) tworzymy takie dwie struktury używane podczas obróbki pliku TGA.

typedef struct
{
GLubyte Header[12];         // Nagłówek pliku aby określić typ pliku
} TGAHeader;
typedef struct
{
GLubyte header[6];         // Pierwsze 6 użytecznyh bajtów pliku
GLuint bytesPerPixel;         // Liczba BAJTÓW na piksel (3 lub 4)
GLuint imageSize;         // Ilość potrzebnej pamięci na przechowanie obrazka
GLuint type;         // Format obrazka (GL_RGB lub GL_RGBA)
GLuint Height;         // Wysokość obrazka
GLuint Width;         // Szerokość obrazka
GLuint Bpp;         // Liczba BITÓW na piksel (24 lub 32)
} TGA;

Deklarujemy sobie dwa obiekty powyższych struktur.

TGAHeader tgaheader;         // Przechowuje nagłówek pliku
TGA tga;         // Przechowuje resztę informacji

Musimy zdefiniować dwa nagłówki aby program wiedział z jakim plikiem TGA ma do czynienia. Pierwsze 12 bajtów wygląda tak: 0 0 2 0 0 0 0 0 0 0 0 0 dla nieskompresowanego TGA a tak: 0 0 10 0 0 0 0 0 0 0 0 0 dla skompresowanego metodą RLE. Dzięki tym nagłówkom wiemy, także czy odczytywany plik jest poprawny.

GLubyte uTGAcompare[12] = {0,0, 2,0,0,0,0,0,0,0,0,0};         // Nagłówek nieskompresowanego TGA
GLubyte cTGAcompare[12] = {0,0,10,0,0,0,0,0,0,0,0,0};         // Nagłówek skompresowanego TGA

Na koniec musimy zadeklarować parę funkcji odpowiedzialnych za ładowanie obrazka.

bool LoadUncompressedTGA(Texture *, char *, FILE *);         // Ładuje nieskompresowany plik
bool LoadCompressedTGA(Texture *, char *, FILE *);         // Ładuje skompresowany plik

Teraz przechodzimy do pliku cpp. Opuścimy sobie informacje o błędach aby tutorial był krótszy i czytelniejszy. Możesz jeszcze przedtem ściągnąc i zobaczyć jak wyglądają gotowe pliki "h" (link na dole artykułu).

W porządku. Na początku musimy dołączyć plik nagłówkowy.

#include "tga.h"         // Dołączamy plik nagłówkowy który właśnie zrobiliśmy

Nie musimy dołączać niczego więcej ponieważ zrobiliśmy to już w pliku nagłówkowym.

Pierwsze co widzimy to funkcja LoadTGA(...).

bool LoadTGA(Texture * texture, char * filename)         // Load A TGA File!
{

Pobiera ona dwa parametry. Pierwszy to wskaźnik do tekstury, który musisz zadeklarować gdzieś w kodzie (zobacz w przykładzie). Drugi to ścieżka do pliku.

W pierwszych dwóch liniach deklarujemy wskaźnik do pliku i otwieramy go.

FILE * fTGA;         // Wskaźnik do pliku
fTGA = fopen(filename, "rb");         // Otwieramy plik do odczytu

Następne kilka lini sprawdza czy plik został otwarty prawidłowo.

if(fTGA == NULL)         // Jeżeli mamy błąd
{
...Kod błędu tutaj...
return false;         // Zwracamy fałsz
}

Następnie odczytujemy pierwsze dwanaście bajtów pliku i zapisujemy je w naszej strukturze TGAHeader. Jeżeli się nam to nie uda to znaczy że plik jest zamknięty więc wypisujemy komunikat o błędzie i zwracamy fałsz.

if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)         // Wczytujemy nagłówek
{
...Kod błędu tutaj...
return false;         // Zwracamy fałsz
}

Odczytany nagłówek porównujemy z wcześniej zadeklarowanymi. Ta operacja powie nam czy plik jest skompresowany. Użyjemy do tego celu funkcji memcmp(...).

if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)         // Jeżeli wczytany nagłówek zgadza się z nagłówkiem pliku nieskompresowanego
{
LoadUncompressedTGA(texture, filename, fTGA);         // To uruchamiamy funkcję wczytującą taki plik
}
else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) ==         // Jeżeli wczytany nagłówek zgadza się z nagłówkiem pliku skompresowanego
{
LoadCompressedTGA(texture, filename, fTGA);         // To uruchamiamy funkcję wczytującą taki plik
}
else         // Jeżeli nagłówek jest całkiem inny
{
...Kod błędu tutaj...
return false;         // To nic nam z takiego pliku
}

Pora na funkcję ładującą NIESKOMPRESOWANY plik TGA. Ta funkcja w większości opiera się na lekcji 25.

Jak zawsze pierwszą rzeczą jest nagłówek funkcji.

bool LoadUncompressedTGA(Texture * texture, char * filename, FILE *         // Ładuje nieskompresowany plik TGA
{

Funkcja pobiera trzy parametry. Pierwsze dwa są takie same jak w funkcji LoadTGA. Trzeci to wskaźnik do pliku z pierwszej funkcji.

Następnie prubujemy odczytać kolejne 6 bajtów pliku i zapisać je w tga.header. Jeżeli wystąpi błąd powiadamiamy o tym i zwracamy fałsz.

if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)         // Wczytujemy 6 kolejnych bajtów
{
...Kod błędu tutaj...
return false;         // Zwracamy fałsz
}

Mamy teraz wszyskie niezbędne informacje aby obliczyć szerokość, wysokość i bpp. Zapiszemy je w specyfikacji tekstury i lokalnej strukturze.

texture->width = tga.header[1] * 256 + tga.header[0];         // Obliczamy wysokość
texture->height = tga.header[3] * 256 + tga.header[2];         // Obliczamy szerokość
texture->bpp = tga.header[4];         // Obliczamy ilość bitów na piksel
tga.Width = texture->width;         // Kopiujemy wysokość do lokalnej struktury
tga.Height = texture->height;         // Kopiujemy szerokość do lokalnej struktury
tga.Bpp = texture->bpp;         // Kopiujemy bpp do lokalnej struktury

Musimy, także sprawdzić czy rozmiar obrazka jest większy od 0 i czy bpp wynosi 24 lub 32. Jeżeli jakaś warość jest z poza zakresu informujemy o tym, zamykamy plik, i opuszczamy funkcję.

if((texture->width <= 0) || (texture->height <= 0) ||         // Sprawdzamy czy wartości są prawidłowe
((texture->bpp != 24) && (texture->bpp !=32)))
{
...Kod błędu tutaj...
return false;         // Zwracamy fałsz
}

Kolej na ustawienie formatu pliku. 24 bitowy obrazek ma format: GL_RGB natomiast 32 bitowy: GL_RGBA

if(texture->bpp == 24)         // Czy obrazek ma 24bpp?
texture->type = GL_RGB;         // Ustawiamy format obrazka na GL_RGB
else         // Jeżeli nie to musi mieć 32bpp
texture->type = GL_RGBA;         // Ustawiamy format obrazka na GL_RGBA

Następnie obliczamy ilość BAJTÓW na piksel i całkowity rozmiar obrazka.

tga.bytesPerPixel = (tga.Bpp / 8);         // Obliczamy ilość BAJTÓW na piksel
tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);         // Obliczamy ilość pamięci potrzebnej na wczytanie obrazka

Potrzebujemy jakiegoś miejsca aby przechować nasz obrazek więc użyjemy funkcji malloc aby zarezerwować odpowiednią ilość pamięci.

Sprawdzamy czy pamięć została zarezerwowana. Jeżeli nie wiadomo co robimy.

texture->imageData = (GLubyte *)malloc(tga.imageSize);         // Rezerwujemy pamięć
if(texture->imageData == NULL)         // Sprawdzamy czy OK
{
...Kod błędu tutaj...
return false;         // Jeżeli nie zwracamy fałsz
}

Tutaj kopiujemy obrazek od pamieci. Jeżeli są problemy wypisujemy komunikat o błędzie.

if(fread(texture->imageData, 1, tga.imageSize, fTGA) !=         // Wczytujemy obrazek
{
...Kod błędu tutaj...
return false;         // Jeżeli niemożemy to zwracamy fałsz
}

Plik TGA przechowuje swoje piksele w innej kolejności niż OpenGL tego wymaga więc musimy zmienić format z BGR na RGB. Czyli musimy zamienić pierwszy i trzeci bajt każdego piksela.

Steve Thomas Adds: Znam małe przyśpieszenie dla operacji zmiany kolorów. Można to zrobić poprzez 3 operacje binarne. Używając zmiennej tymczasowej i XOR'ując po dwa bajty 3 razy.

Na koniec zamykamy plik i kończymy funkcję powodzeniem.

for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap +=         // Początek pętli
{
texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=         // Zamień kolor czerwony z niebieskim przez XOR'owanie
texture->imageData[cswap] ^= texture->imageData[cswap+2];
}
fclose(fTGA);         // Zamykamy plik
return true;         // Zwracamy powodzenie
}

To wszystko na tema ładowania nieskompresowanego pliku TGA. Ładowanie skompresowanego metodą RLE pliku jest tylko troszkę trudniejsze. Odczytujemy nagłówek i zbieramy informacje i wysokości/szerokości/bpp jak w nieskompresowanej wesrji więc poprostu przepiszę kod, zobacz wyjaśnienia wyżej jeżeli czegoś nie rozumiesz.

bool LoadCompressedTGA(Texture * texture, char * filename, FILE * fTGA)
{
if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
{
...Kod błędu tutaj...
}
texture->width = tga.header[1] * 256 + tga.header[0];
texture->height = tga.header[3] * 256 + tga.header[2];
texture->bpp = tga.header[4];
tga.Width = texture->width;
tga.Height = texture->height;
tga.Bpp = texture->bpp;
if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
{
...Kod błędu tutaj...
}
if(texture->bpp == 24)         // Czy obrazek ma 24bpp?
texture->type = GL_RGB;         // Ustawiamy format obrazka na GL_RGB
else         // Jeżeli nie to musi mieć 32bpp
texture->type = GL_RGBA;         // Ustawiamy format obrazka na GL_RGBA
tga.bytesPerPixel = (tga.Bpp / 8);
tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);

Musimy zarezerwować odpowiednią ilość pamięci na obrazek zanim go zdekompresujemy, użyjemy jak poprzednio funkcji malloc.

texture->imageData = (GLubyte *)malloc(tga.imageSize);         // Rezerwujemy pamięć na obrazek
if(texture->imageData == NULL)         // Jeżeli takowej brak
{
...Kod błędu tutaj...
return false;         // Zwracamy fałsz
}

Musimy wiedzieć ile pikseli tworzy obrazek. Zapiszemy to w zmiennej "pixelcount".

Musimy, także zapisać na którym pikselu aktualnie pracujemy oraz który bajt obrazka jest zapisywany aby zapobiec przepełnieniu i nadpisywaniu danych.

Następnie rezerwujemy potrzebną ilość pamięci aby zapisać jeden piksel.

GLuint pixelcount = tga.Height * tga.Width;         // Ilość pikseli w obrazku
GLuint currentpixel = 0;         // Aktualny piksel obrazka
GLuint currentbyte = 0;         // Aktualny bajt pliku
GLubyte * colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);         // Przechowuje jeden piksel

Czas na wielką pętle.

Na początku deklarujemy zmienną aby zapisać nagłówek sekcji pliku (ang. chunk header). Nagłówek mówi nam czy sekcja jest RLE czy RAW i jaka jest długa. Jeżeli warość nagłówka jest mniejsza lub równa 127 wtedy jest to sekcja RAW. Wartością nagłówka jest liczba kolorów minus jeden, czytamy plik dalej i kopiujemy do pamięci do czasu aż napotkamy kolejny bajt nagłówka. Jeżeli wartość nagłówka jest POWYŻEJ 127 jest to ilość razy ile dany piksel sie powtarza. Lecz wcześniej musimy odjąć od tego 127. Wtedy wczytujemy piksel i kopiujemy go do obrazka odpowiednią ilość razy.

Na początku odczytujemy wartość nagłówka.

do         // Początek pętli
{
GLubyte chunkheader = 0;         // Zmienna do przechowywania wartości nagłówka
if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0)         // Wczytujemy nagłówek
{
...Kod błędu tutaj...
return false;         // Jeżeli są problemy zwracamy fałsz
}

Następnie sprawdzamy czy jest to nagłówek sekcji RAW. Jeżeli tak musimy pobrać wartość nagłówka aby wiedzieć ile pikseli jest za nim.

if(chunkheader < 128)         // Jeżel sekcja jest typu 'RAW'
{
chunkheader++;         // Dodajemy 1 aby mieć ilość pikseli RAW

W tej pętli odczytujemy kolejne piksele. Pętla zostanie wykonana tyle razy jaką miał wartość nagłówek i tyle pikseli zostanie odczytane.

Najpierw odczytujemy i sprawdzamy informacje o pikselu. Piksel zostaje zapisany w zmiennej colorbuffer.

for(short counter = 0; counter < chunkheader; counter++)         // Początek pętli wczytującej piksele
{
if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) !=         // Prubujemy wczytać jeden piksel
{
...Kod błędu tutaj...
return false;         // Jeżeli są problemy zwracamy fałsz
}

Następna część naszej pętli pobiera wartości ze zmiennej colorbuffer i zapisuje je do imageData. Dodatkowo zmienia format z BGR na BRG lub BGRA na RGBA w zalieżności od ilości bajtów na piksel. Kiedy to skończymy interkremujemy nasze liczniki pikseli i bajtów.

texture->imageData[currentbyte] = colorbuffer[2];         // Zapisz bajt 'R'
texture->imageData[currentbyte + 1 ] = colorbuffer[1];         // Zapisz bajt 'G'
texture->imageData[currentbyte + 2 ] = colorbuffer[0];         // Zapisz bajt 'B'
if(tga.bytesPerPixel == 4)         // Jeżeli plik jest 32 bitowy
{
texture->imageData[currentbyte + 3] = colorbuffer[3];         // Zapisz bajt 'A'
}
currentbyte += tga.bytesPerPixel;         // Interkremujemy liczniki bajtów i pikseli
currentpixel++;

Jeżeli nie jest to sekcja RAW więc musi być to sekcja RLE. Mmusimy więc odjąć 127 od wartości nagłówka aby wiedzieć ile razy dany piksel się powtarza.

else         // Jeżeli jest to sekcja RLE
{
chunkheader -= 127;         // Odejmujemy 127 aby wiedzieć ile razy dany piksel mamy skopiować

Wczytujemy piksel do bufora.

if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)         // Wczytujemy piksel
{
...Kod błędu tutaj...
return false;         // Jeżeli są problemy zwracamy fałsz
}

Kolejna pętla zapisuje do obrazka odpowiednią ilość piksela przed chwilą wczytanego dodatkowo zmieniając kolory czerwony z niebieskim.

Na końcu dodajemy odpowiednie wartości do liczników bajtów i pikseli.

for(short counter = 0; counter < chunkheader; counter++)         // Początek pętli
{
texture->imageData[currentbyte] = colorbuffer[2];         // Kopiujemy bajt 'R'
texture->imageData[currentbyte + 1 ] = colorbuffer[1];         // Kopiujemy bajt 'G'
texture->imageData[currentbyte + 2 ] = colorbuffer[0];         // Kopiujemy bajt 'B'
if(tga.bytesPerPixel == 4)         // Jeżeli plik jest 32 bitowy
{
texture->imageData[currentbyte + 3] = colorbuffer[3];         // Kopiujemy bajt 'A'
}
currentbyte += tga.bytesPerPixel;         // Interkremujemy licznik bajtów
currentpixel++;         // Interkremujemy licznik pikseli

Kontynuujemy główną pętlę dopuki są piksele do wczytania.

I na końcu zamykamy plik i kończymy funkcję sukcesem.

while(currentpixel < pixelcount);         // Są jeszcze piksele do wczytania? ... Koniec pętli
fclose(fTGA);         // Zamykamy plik
return true;         // Zwracamy sukces
}

Teraz masz już dane do wykorzystania w glGenTextures() oraz glBindTexture(). Informacje o nich znajdziesz w lekcji 6. i 24. To kończy mój pierwszy tutorial. Nie mogę zagwarantować, że w moim kodzie nie ma błędów, ale starałem się, żeby ich nie było. Specjalne podziękowania dla Jeff 'NeHe' Molofee za jego świetny kurs oraz dla Trent "ShiningKnight" Polack za poprawki w tym tutorialu. Jeżeli znajdziesz błąd lub chcesz mi coś powiedzieć lub zasugerować, nie krępuj się - napisz do mnie: e-mail (terminate@gdnmail.net), ICQ (38601160).