V lekci 24 jsem vám ukázal cestu, jak nahrávat nekomprimované 24/32 bitové TGA obrázky. Jsou velmi užitečné, když potřebujete alfa kanál, ale nesmíte se starat o jejich velikost, protože byste je ihned přestali používat. K diskovému místu nejsou zrovna šetrné. Problém velikosti vyřeší nahrávání obrázků komprimovaných metodou RLE. Kód pro loading a hlavičkové soubory jsou odděleny od hlavního projektu, aby mohly být snadno použity i jinde.
Začneme dvěma hlavičkový soubory. Texture.h, první z nich, popisuje strukturu textury. Každý hlavičkový soubor by měl obsahovat ochranu proti vícenásobnému vložení. Zajišťují ji příkazy preprocesoru jazyka C. Pokud není definovaná symbolická konstanta __TEXTURE_H__, nadefinujeme ji a do stejného bloku podmínky vepíšeme zdrojový kód. Při následujícím pokusu o inkludování hlavičkového souboru existence konstanty oznámí preprocesoru, že už byl soubor jednou vložen, a tudíž ho nemá vkládat podruhé.
#ifndef __TEXTURE_H__
#define __TEXTURE_H__
Budeme potřebovat strukturu informací o obrázku, ze kterého se vytváří textura. Ukazatel imageData obsahuje data obrázku, bpp barevnou hloubku, width a height rozměry. TexID je identifikátorem OpenGL textury, který se předává funkci glBindTexture(). Type určuje typ textury - GL_RGB nebo GL_RGBA.
typedef struct// Struktura textury
{
GLubyte* imageData;// Data
GLuint bpp;// Barevná hloubka v bitech
GLuint width;// Šířka
GLuint height;// Výška
GLuint type;// Typ (GL_RGB, GL_RGBA)
GLuint texID;// ID textury
} Texture;
#endif
Druhý hlavičkový soubor, tga.h, je speciálně určen pro loading TGA. Opět začneme ošetřením vícenásobného inkludování, poté vložíme hlavičkový soubor textury.
#ifndef __TGA_H__
#define __TGA_H__
#include "texture.h"// Hlavičkový soubor textury
Strukturu TGAHeader představuje pole dvanácti bytů, které ukládají hlavičku obrázku. Druhá struktura obsahuje pomocné proměnné pro nahrávání - např. velikost dat, barevnou hloubku a podobně.
typedef struct// Hlavička TGA souboru
{
GLubyte Header[12];// Dvanáct bytů
} TGAHeader;
typedef struct// Struktura obrázku
{
GLubyte header[6];// Šest užitečných bytů z hlavičky
GLuint bytesPerPixel;// Barevná hloubka v bytech
GLuint imageSize;// Velikost paměti pro obrázek
// GLuint temp;// Překl.: nikde není použitá
GLuint type;// Typ
GLuint Height;// Výška
GLuint Width;// Šířka
GLuint Bpp;// Barevná hloubka v bitech
} TGA;
Deklarujeme instance právě vytvořených struktur, abychom je mohli použít v programu.
TGAHeader tgaheader;// TGA hlavička
TGA tga;// TGA obrázek
Následující dvě pole pomohou určit validitu nahrávaného souboru. Pokud se hlavička obrázku neshoduje s některou z nich, neumíme ho nahrát.
GLubyte uTGAcompare[12] = { 0,0, 2,0,0,0,0,0,0,0,0,0 };// TGA hlavička nekomprimovaného obrázku
GLubyte cTGAcompare[12] = { 0,0,10,0,0,0,0,0,0,0,0,0 };// TGA hlavička komprimovaného obrázku
Obě funkce nahrávají TGA - jedna nekomprimovaný druhá komprimovaný.
bool LoadUncompressedTGA(Texture*, char*, FILE*);// Nekomprimovaný TGA
bool LoadCompressedTGA(Texture*, char*, FILE*);// Komprimovaný TGA
#endif
Přesuneme se k souboru TGALoader.cpp, který implementuje nahrávací funkce. Prvním řádkem kódu vložíme hlavičkový soubor. Inkludujeme pouze tga.h, protože texture.h jsme už vložili v něm.
#include "tga.h"// Hlavičkový soubor TGA
Funkce LoadTGA() je ta, kterou v programu voláme, abychom nahráli obrázek. V parametrech se jí předává ukazatel na texturu a řetězec diskové cesty. Nic dalšího nepotřebuje, protože si všechny ostatní parametry detekuje sama (ze souboru). Deklarujeme handle souboru a otevřeme ho pro čtení v binárním módu. Pokud něco selže, např. soubor neexistuje, vypíšeme chybovou zprávu a vrátíme false jako indikaci chyby.
bool LoadTGA(Texture* texture, char* filename)// Nahraje TGA soubor
{
FILE* fTGA;// Handle souboru
fTGA = fopen(filename, "rb");// Otevře soubor
if(fTGA == NULL)// Nepodařilo se ho otevřít?
{
MessageBox(NULL, "Could not open texture file", "ERROR", MB_OK);
return false;
}
Zkusíme načíst hlavičku obrázku (prvních 12 bytů souboru), která určuje jeho typ. Výsledek se uloží do proměnné tgaheader.
if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)// Načte hlavičku souboru
{
MessageBox(NULL, "Could not read file header", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
return false;
}
Právě načtenou hlavičku porovnáme s hlavičkou nekomprimovaného obrázku. Jsou-li shodné nahrajeme obrázek funkcí LoadUncompressedTGA(). Pokud shodné nejsou zkusíme, jestli se nejedná o komprimovaný obrázek. V tomto případě použijeme pro nahrávání funkci LoadCompressedTGA(). S jinými typy souborů pracovat neumíme, takže jediné, co můžeme udělat, je oznámení neúspěchu a ukončení funkce.
Překl.: Měla by se ještě testovat návratová hodnota, protože, jak uvidíte dále, funkce v mnoha případech vracejí false. Program by si bez kontroly ničeho nevšiml a pokračoval dále.
if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)// Nekomprimovaný
{
LoadUncompressedTGA(texture, filename, fTGA);
// Překl.: Testovat návratovou hodnotu !!!
// if(!LoadUncompressedTGA(texture, filename, fTGA))// Test návratové hodnoty
// {
// return false;
// }
}
else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)// Komprimovaný
{
LoadCompressedTGA(texture, filename, fTGA);
// Překl.: Testovat návratovou hodnotu !!!
// if(!LoadCompressedTGA(texture, filename, fTGA))// Test návratové hodnoty
// {
// return false;
// }
}
else// Ani jeden z nich
{
MessageBox(NULL, "TGA file be type 2 or type 10 ", "Invalid Image", MB_OK);
fclose(fTGA);
return false;
}
Pokud dosud nenastala žádná chyba, můžeme oznámit volajícímu kódu, že obrázek byl v pořádku nahrán a že může z jeho dat vytvořit texturu.
return true;// Vše v pořádku
}
Přistoupíme k opravdovému nahrávání obrázků, začneme nekomprimovanými. Tato funkce je z velké části založena na té z lekce 24, moc novinek v ní nenajdete. Zkusíme načíst dalších šest bytů ze souboru a uložíme je do tga.header.
bool LoadUncompressedTGA(Texture* texture, char* filename, FILE* fTGA)// Nahraje nekomprimovaný TGA
{
if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)// Šest užitečných bytů
{
MessageBox(NULL, "Could not read info header", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
return false;
}
Máme dost informací pro určení výšky, šířky a barevné hloubky obrázku. Uložíme je do obou struktur - textury i obrázku.
texture->width = tga.header[1] * 256 + tga.header[0];// Šířka
texture->height = tga.header[3] * 256 + tga.header[2];// Výška
texture->bpp = tga.header[4];// Barevná hloubka v bitech
// Kopírování dat do struktury obrázku
tga.Width = texture->width;
tga.Height = texture->height;
tga.Bpp = texture->bpp;
Otestujeme, jestli má obrázek alespoň jeden pixel a jestli je barevná hloubka 24 nebo 32 bitů.
// Platné hodnoty?
if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp != 32)))
{
MessageBox(NULL, "Invalid texture information", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
return false;
}
Nyní nastavíme typ obrázku. V případě 24 bitů je jím GL_RGB, u 32 bitů má obrázek i alfa kanál, takže použijeme GL_RGBA.
if(texture->bpp == 24)// 24 bitový obrázek?
{
texture->type = GL_RGB;
}
else// 32 bitový obrázek
{
texture->type = GL_RGBA;
}
Spočítáme barevnou hloubku v BYTECH a celkovou velikost paměti potřebnou pro data. Vzápětí se ji pokusíme alokovat.
tga.bytesPerPixel = (tga.Bpp / 8);// BYTY na pixel
tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);// Velikost paměti
texture->imageData = (GLubyte *)malloc(tga.imageSize);// Alokace paměti pro data
if(texture->imageData == NULL)// Alokace neúspěšná
{
MessageBox(NULL, "Could not allocate memory for image", "ERROR", MB_OK);
fclose(fTGA);
return false;
}
Pokud se podařila alokace paměti, nahrajeme do ní data obrázku.
// Pokusí se nahrát data obrázku
if(fread(texture->imageData, 1, tga.imageSize, fTGA) != tga.imageSize)
{
MessageBox(NULL, "Could not read image data", "ERROR", MB_OK);
if(texture->imageData != NULL)
{
free(texture->imageData);// Uvolnění paměti
}
fclose(fTGA);
return false;
}
Formát TGA se od formátu OpenGL liší tím, že má v pixelech přehozené R a B složky barvy (BGR místo RGB). Musíme tedy zaměnit první a třetí byte v každém pixelu. Abychom tuto operace urychlili, provedeme tři binární operace XOR. Výsledek je stejný jako při použití pomocné proměnné.
// Převod BGR na RGB
for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap += tga.bytesPerPixel)
{
texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=
texture->imageData[cswap] ^= texture->imageData[cswap+2];
}
Obrázek jsme úspěšně nahráli, takže zavřeme soubor a vrácením true oznámíme úspěch.
fclose(fTGA);// Zavření souboru
return true;// Úspěch
// Paměť dat obrázku se uvolňuje až po vytvoření textury
}
Nyní přistoupíme k nahrávání obrázku komprimovaného metodou RLE (RunLength Encoded). Začátek je stejný jako u nekomprimovaného obrázku - načteme výšku, šířku a barevnou hloubku, ošetříme neplatné hodnoty a spočítáme velikost potřebné paměti, kterou opět alokujeme. Všimněte si, že velikost požadované paměti je taková, aby do ní mohla být uložena data PO DEKOMPRIMOVÁNÍ, ne před dekomprimováním.
bool LoadCompressedTGA(Texture* texture, char* filename, FILE* fTGA)// Nahraje komprimovaný obrázek
{
if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)// Šest užitečných bytů
{
MessageBox(NULL, "Could not read info header", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
return false;
}
texture->width = tga.header[1] * 256 + tga.header[0];// Šířka
texture->height = tga.header[3] * 256 + tga.header[2];// Výška
texture->bpp = tga.header[4];// Barevná hloubka v bitech
// Kopírování dat do struktury obrázku
tga.Width = texture->width;
tga.Height = texture->height;
tga.Bpp = texture->bpp;
// Platné hodnoty?
if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp != 32)))
{
MessageBox(NULL, "Invalid texture information", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
return false;
}
if(texture->bpp == 24)// 24 bitový obrázek?
{
texture->type = GL_RGB;
}
else// 32 bitový obrázek
{
texture->type = GL_RGBA;
}
tga.bytesPerPixel = (tga.Bpp / 8);// BYTY na pixel
tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);// Velikost paměti
texture->imageData = (GLubyte *)malloc(tga.imageSize);// Alokace paměti pro data (po dekomprimování)
if(texture->imageData == NULL)// Alokace neúspěšná
{
MessageBox(NULL, "Could not allocate memory for image", "ERROR", MB_OK);
fclose(fTGA);
return false;
}
Dále potřebujeme zjistit přesný počet pixelů, ze kterých je obrázek složen. Jednoduše vynásobíme výšku obrázku se šířkou. Také musíme znát, na kterém pixelu se právě nacházíme a kam do paměti zapisujeme.
GLuint pixelcount = tga.Height * tga.Width;// Počet pixelů
GLuint currentpixel = 0;// Aktuální načítaný pixel
GLuint currentbyte = 0;// Aktuální načítaný byte
Alokujeme pomocné pole tří nebo čtyř bytů (podle barevné hloubky) k uložení jednoho pixelu. Překl.: Měla by se testovat správnost alokace paměti!
GLubyte* colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);// Paměť pro jeden pixel
// Překl.: Test úspěšnosti alokace paměti !!!
// if(colorbuffer == NULL)// Alokace neúspěšná
// {
// MessageBox(NULL, "Could not allocate memory for color buffer", "ERROR", MB_OK);
// fclose(fTGA);
// return false;
// }
V hlavním cyklu deklarujeme proměnnou k uložení bytu hlavičky, který definuje, jestli je následující sekce obrázku ve formátu RAW nebo RLE a jak dlouhá je. Pokud je byte hlavičky menší nebo roven 127, jedná se o RAW hlavičku. Hodnota v ní uložená, určuje počet pixelů mínus jedna, které vzápětí načteme a zkopírujeme do paměti. Po těchto pixelech se v souboru vyskytuje další byte hlavičky. Pokud je byte hlavičky větší než 127, představuje toto číslo (zmenšené o 127), kolikrát se má následující pixel v dekomprimovaném obrázku opakovat. Hned po něm se bude vyskytovat další hlavičkový byte. Načteme hodnoty tohoto pixelu a zkopírujeme ho do imageData tolikrát, kolikrát potřebujeme.
Podstatu komprese RLE tedy už znáte, podívejme se na kód. Jak jsem již zmínil, založíme cyklus přes celý soubor a pokusíme se načíst byte první hlavičky.
do// Prochází celý soubor
{
GLubyte chunkheader = 0;// Byte hlavičky
if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0)// Načte byte hlavičky
{
MessageBox(NULL, "Could not read RLE header", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
if(texture->imageData != NULL)
{
free(texture->imageData);
}
// Překl.: Uvolnění dynamické paměti !!!
// if(colorbuffer != NULL)
// {
// free(colorbuffer);
// }
return false;
}
Pokud se jedná o RAW hlavičku, přičteme k bytu jedničku, abychom získali počet pixelů následujících po hlavičce. Potom založíme další cyklus, který načítá všechny požadovaného pixely do pomocného pole colorbuffer a vzápětí je ve správném formátu ukládá do imageData.
if(chunkheader < 128)// RAW část obrázku
{
chunkheader++;// Počet pixelů v sekci před výskytem dalšího bytu hlavičky
for(short counter = 0; counter < chunkheader; counter++)// Jednotlivé pixely
{
// Načítání po jednom pixelu
if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
{
MessageBox(NULL, "Could not read image data", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
if(colorbuffer != NULL)
{
free(colorbuffer);
}
if(texture->imageData != NULL)
{
free(texture->imageData);
}
return false;
}
Při kopírování do imageData prohodíme pořadí bytů z formátu BGR na RGB. Pokud je v obrázku i alfa kanál, zkopírujeme i čtvrtý byte. Abychom se přesunuli na další pixel popř. byte hlavičky, zvětšíme aktuální byte o barevnou hloubku (+3 nebo +4). Inkrementujeme také počet načtených pixelů.
// Zápis do paměti, prohodí R a B složku barvy
texture->imageData[currentbyte] = colorbuffer[2];
texture->imageData[currentbyte + 1] = colorbuffer[1];
texture->imageData[currentbyte + 2] = colorbuffer[0];
if(tga.bytesPerPixel == 4)// 32 bitový obrázek?
{
texture->imageData[currentbyte + 3] = colorbuffer[3];// Kopírování alfy
}
currentbyte += tga.bytesPerPixel;// Aktualizuje byte
currentpixel++;// Přesun na další pixel
Zjistíme, jestli je pořadová číslo aktuálního pixelu větší než celkový počet pixelů. Pokud ano, je soubor obrázku poškozen nebo je v něm někde chyba. Jak jsme na to přišli? Máme načítat další pixel, ale defakto je už máme všechny načtené, protože aktuální hodnota je větší než maximální. Nestačila by alokovaná paměť pro dekomprimovanou verzi obrázku. Tuto skutečnost musíme každopádně ošetřit.
if(currentpixel > pixelcount)// Jsme za hranicí obrázku?
{
MessageBox(NULL, "Too many pixels read", "ERROR", NULL);
if(fTGA != NULL)
{
fclose(fTGA);
}
if(colorbuffer != NULL)
{
free(colorbuffer);
}
if(texture->imageData != NULL)
{
free(texture->imageData);
}
return false;
}
}
}
Vyřešili jsme část RAW, nyní implementujeme sekci RLE. Ze všeho nejdříve od bytu hlavičky odečteme číslo 127, abychom získali kolikrát se má následující pixel opakovat.
else// RLE část obrázku
{
chunkheader -= 127;// Počet pixelů v sekci
Načteme jeden pixel po hlavičce a potom ho požadovaně-krát vložíme do imageData. Opět zaměňujeme formát BGR za RGB. Stejně jako minule inkrementujeme aktuální byte i pixel a ošetřujeme přetečení.
if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)// Načte jeden pixel
{
MessageBox(NULL, "Could not read from file", "ERROR", MB_OK);
if(fTGA != NULL)
{
fclose(fTGA);
}
if(colorbuffer != NULL)
{
free(colorbuffer);
}
if(texture->imageData != NULL)
{
free(texture->imageData);
}
return false;
}
for(short counter = 0; counter < chunkheader; counter++)// Kopírování pixelu
{
// Zápis do paměti, prohodí R a B složku barvy
texture->imageData[currentbyte] = colorbuffer[2];
texture->imageData[currentbyte + 1] = colorbuffer[1];
texture->imageData[currentbyte + 2] = colorbuffer[0];
if(tga.bytesPerPixel == 4)// 32 bitový obrázek?
{
texture->imageData[currentbyte + 3] = colorbuffer[3];// Kopírování alfy
}
currentbyte += tga.bytesPerPixel;// Aktualizuje byte
currentpixel++;// Přesun na další pixel
if(currentpixel > pixelcount)// Jsme za hranicí obrázku?
{
MessageBox(NULL, "Too many pixels read", "ERROR", NULL);
if(fTGA != NULL)
{
fclose(fTGA);
}
if(colorbuffer != NULL)
{
free(colorbuffer);
}
if(texture->imageData != NULL)
{
free(texture->imageData);
}
return false;
}
}
}
Hlavní cyklus opakujeme tak dlouho, dokud v souboru zbývají nenačtené pixely. Po konci loadingu soubor zavřeme a vrácením true indikujeme úspěch.
} while(currentpixel < pixelcount);// Pokračuj dokud zbývají pixely
// Překl.: Uvolnění dynamické paměti !!!
// if(colorbuffer != NULL)
// {
// free(colorbuffer);
// }
fclose(fTGA);// Zavření souboru
return true;// Úspěch
// Paměť dat obrázku se uvolňuje až po vytvoření textury
}
Nyní jsou data obrázku připravena pro vytvoření textury a to už jistě zvládnete sami. V tomto tutoriálu nám šlo především o nahrávání TGA obrázků. Ukázkové demo bylo vytvořeno jen proto, abyste viděli, že kód opravdu funguje.
A jak je to s úspěšností komprimace metody RLE? Je jasné, že nejmenší paměť bude zabírat obrázek s rozsáhlými plochami stejných pixelů (na řádcích). Pokud chcete čísla, tak si vezmeme na pomoc obrázky použité v tomto demu: oba jsou 128x128 pixelů veliké, nekomprimovaný zabírá na disku 48,0 kB a komprimovaný pouze 5,29 kB. Na obou je sice něco jiného, ale devítinásobné zmenšení velikosti mluví za vše.
napsal: Evan Pipho - Terminate <terminate (zavináč) gdnmail.net>
přeložil: Michal Turek - Woq <WOQ (zavináč) seznam.cz>