V tomto článku si ukážeme, jak vytvářet komprimované OpenGL textury a jak za pomoci knihovny SDL_Image snadno načítat obrázky s alfa kanálem nebo v paletovém režimu. Třídu Picture jsem se snažil navrhnou tak, aby byla co nejjednodušší a dala se snadno použít v každém programu, zároveň díky SDL_Image poskytuje velké možnosti.
První, co si ukážeme a popíšeme, je deklarace třídy Picture. Jak už je patrné z komentářů, SizeX a SizeY označují rozměr obrázku. U Bpp, které specifikuje velikost jednoho pixelu, pozor! Tato zkratka se většinou používá jako Bit Per Pixel, nicméně my v ní ukládáme Byte Per Pixel. Její hodnota nám tedy říká nejen, kolik zabere jeden pixel bytů v paměti, ale také kolik má složek (3 = RGB, 4 = RGBA, ...). Ukazatel Data bude v sobě ukládat informace obrázku, tedy jednotlivé pixely. Všechny funkce si rozepíšeme dále kromě Free(), FlipHorizontal() a FlipVertical(), u nichž je to zbytečné. Ty, které mají návratovou hodnotu typu bool, vracejí true jako úspěch a false jako neúspěch, ale to je, doufám, každému jasné.
class Picture// Třída obrázku
{
public:
Uint16 SizeX;// Šířka obrázku
Uint16 SizeY;// Výška obrázku
Uint16 Bpp;// Počet !BYTŮ! na pixel
Uint8 *Data;// Ukazatel na data obrázku
bool Load(const char *FileName);// Načte obrázek
void Free(void);// Uvolní obrázek z paměti
void FlipHorizontal(void);// Obrátí obrázek vodorovně
void FlipVertical(void);// Obrátí obrázek svisle
bool HalfSize(void);// Zmenší obrázek na polovinu
GLuint CreateTexture(int MinFilter, int MagFilter, int BitsPerColor, bool MipMaps, bool Compress);// Vytvoření textury
// Konstruktor a destruktor
Picture(void) { memset(this, 0 , sizeof(Picture)); }// Vyčistí paměť objektu
~Picture() { Free(); }// Uvolní obrázek z paměti
};
bool GetCompressTexExt(int Format);// Zjistí přítomnost požadovaného rozšíření formátu pro texturu
unsigned int GetInternalFormat(int Pixel_Format, int BitsPerColor);// Pomocná funkce pro zjištění internal formátu
První a nejdůležitější metoda ve třídě Picture je Load(), která načte obrázek a převede ho na v OpenGL použitelný formát. Pro jeho načtení používáme funkci IMG_Load(), která vrací SDL_Surface, jenže ten má několik nevýhod, které ho při přímém použití v OpenGL vyřazují. Většina načtených obrázků (kromě JPG) má prohozené červené a modré složky pixelů. Dalším problémem jsou obrázky uložené v paletovém režimu, ty budeme muset převést na normální formát, a aby toho nebylo málo, obrázek má i prohozené řádky :-(
bool Picture::Load(const char *FileName)// Načte obrázek
{
Free();// Zkontroluje, jestli už není načtený jiný obrázek a případně ho uvolni
SDL_Surface *Image = IMG_Load(FileName);// Načte SDL_Surface pomocí knihovny SDL_Image
if(Image == NULL)// Nelze načíst
{
fprintf(stderr, "Nelze nacist soubor \"%s\" : %s\n", FileName, SDL_GetError());
return false;
}
SizeX = Image->w;// Nastavení proměnných ve třídě
SizeY = Image->h;
if(Image->format == NULL)
{
fprintf(stderr, "Chyba v nactenem obrazku \"%s\", neni udaj o formatu\n", FileName);
SDL_FreeSurface(Image);
return false;
}
Uint32 x, y, pix, Change = 0, PalIndex;// Proměnné pro řízení cyklu
Uint8 *Pixels = (Uint8 *)Image->pixels;
Obrázek bez palety poznáme jen podle ukazatele pallete, který bude v takovém případě nastaven na hodnotu NULL, u paletových obrázků obsahuje barvy pro indexy v obrázku. Ještě by se dal poznat podle počtu bytů na pixel, který bývá roven jedné, ale kdybychom načetli obrázek s jednou složkou na pixel, byl by automaticky považován za paletový, což by mohlo vést k chybám - například u obrázku obsahujícím pouze alfa kanál (i když si nejsem jist, zda takový formát existuje).
if(Image->format->palette == NULL)// Obrázek bez palety
{
Bpp = Image->format->BytesPerPixel;
Alokujeme potřebnou paměť podle rozměrů obrázku a počtu barevných složek. Na tomto místě by mohli rejpalové namítat, že je zbytečné přidávat příkaz sizeof(Uint8), který v tomto případě vrátí hodnotu jedna. Možná ano, ale nemuselo by to tak být, ušetříte si mnoho problémů.
Data = (Uint8 *) malloc(sizeof(Uint8) * SizeX * SizeY * Bpp);// Alokace paměti obrázku
if(Data == NULL)
{
fprintf(stderr, "Nelze pridelit pamet (%d kB) potrebnou pro obrazek \"%s\"\n", (SizeX * SizeY * Bpp * sizeof(Uint8)) / 1024, FileName);
SDL_FreeSurface(Image);
return false;
}
Proměnná Change obsahuje hodnotu určující, kolik složek barev se má prohodit. Skoro vždy je třeba prohodit dvě složky (z BGR na RGB), ale někdy ne, určuje to Bshift v SDL_Surface. Také si musíme ověřit, zda má obrázek alespoň tři složky, pokud ne, nemá smysl je prohazovat. Tento případ může nastat například u obrázků jen s alfa hodnotou.
if(Image->format->Bshift == 0)// Pokud je obrázek BGR/BGRA, musí se prohodit R a B
{
if(Bpp >= 3)
{
Change = 3;// Proměnná change zajišťuje přehazování prvních tří složek barvy
}
}
Kopírovací cyklus zároveň se swapováním složek barev prohazuje i řádky, které jsou v SDL_Surface opačně. První z vnořených cyklů, který kopíruje pixely nemusí proběhnout, protože proměnná Change může být nula a to znamená, že se neprohazují barevné složky. Druhý proběhne ve dvou případech. Hodnota v Change je nula a první cyklus neproběhl nebo je počet složek (Bpp) větší než tři a musí se přidat k prvním třem prohozeným ještě další hodnoty (např. alfa).
for(y = 0; y < SizeY; y++)// Kopírovací cyklus
{
for(x = 0 ; x < SizeX ; x++)
{
for(pix = 0 ; pix < Change ; pix++)// Hodnoty, které se prohodí (vždy 3) BGR na RGB
{
Data[(x + (y * SizeY)) * Bpp + (Change-1 - pix)] = Pixels[(x + ((SizeY-1 - y) * SizeY)) * Bpp + pix];
}
for(pix = Change ; pix < Bpp ; pix++)// Hodnoty, které zůstanou neprohozeny (např. alfa)
{
Data[(x + (y * SizeY)) * Bpp + pix] = Pixels[(x + ((SizeY-1 - y) * SizeY)) * Bpp + pix];
}
}
}
}
Obrázek v paletovém režimu je díky uspořádání palety v SDL_Surface, která v sobě obsahuje strukturu SDL_Color, daleko jednodušší. Stačí jen prohazovat řádky. Obrázek obsahuje místo složek barev jen indexy do palety, pomocí nichž se v ní orientujeme. Tímto způsobem načteme barvy do pole Data.
else// Obrázek v paletovém režimu
{
Bpp = 3;// Předpokládáme RGB formát a tudíž zabírá jeden pixel 3 byty
if(Image->format->palette->colors == NULL)// Kontrola palety
{
fprintf(stderr, "Chyba v palete obrazku \"%s\"\n", FileName);
SDL_FreeSurface(Image);
return false;
}
Data = (Uint8 *) malloc(sizeof(Uint8) * SizeX * SizeY * Bpp);// Paměť pro obrázek
if(Data == NULL)
{
fprintf(stderr, "Nelze pridelit pamet(%d kB) potrebnou pro obrazek \"%s\"\n", (SizeX * SizeY * Bpp * sizeof(Uint8)) / 1024, FileName);
SDL_FreeSurface(Image);
return false;
}
for(y = 0 ; y < SizeY ; y++)// Kopírovací cyklus
{
for(x = 0 ; x < SizeX ; x++)
{
PalIndex = Pixels[x + ((SizeY-1 - y) * SizeY)];// Index v paletě
Data[(x + (y * SizeY)) * Bpp] = Image->format->palette->colors[PalIndex].r;// Červená
Data[(x + (y * SizeY)) * Bpp + 1] = Image->format->palette->colors[PalIndex].g;// Zelená
Data[(x + (y * SizeY)) * Bpp + 2] = Image->format->palette->colors[PalIndex].b;// Modrá
}
}
}
SDL_FreeSurface(Image);// Úklid
return true;// Vše OK
}
Než se pustíme do vytváření textury, dáme si trochu oddych. Podíváme se na funkci HalfSize, z jejíhož názvu vyplývá i její účel - zmenší obrázek na polovinu. Možná se někteří ptáte, k čemu je taková funkce dobrá, když vlastně snižuje kvalitu obrázku. Právě o to jde, můžete tak snadno ve svém programu všechny textury při načítání zmenšit na polovinu a tím šetřit paměť a výkon slabších strojů.
Tato funkce ovšem není úplně primitivním vynecháním jednoho řádku jako v jistém nejmenovaném kreslícím programu od firmy Microsoft®. Při zmenšení o polovinu se stává ze čtyř pixelů jeden, který je jejich průměrem. To zajistí, aby nevymizely důležité detaily. Podobně pracuje i rozšíření multisample u grafických karet, kde se pro zlepšení kvality obrazu a zahlazení hran vyrenderuje větší obrázek, který je následně zmenšen a zobrazen. Kdyby někdo chtěl vidět, jak to vypadá bez tohoto efektu, ať odkomentuje variantu bez zahlazení a odstraní tu se zahlazením.
bool Picture::HalfSize(void)// Zmenší obrázek na polovinu
{
// Kontrola velikosti, dat a počtu bytů na složku barvy
if(Data == NULL || SizeX < 2 || SizeY < 2 || Bpp < 1)
{
return false;
}
int NewSizeX = SizeX / 2;// Nová velikost
int NewSizeY = SizeY / 2;
BYTE *NewPic = (BYTE *) malloc(sizeof(BYTE) * NewSizeX * NewSizeY * Bpp);// Přidělení paměti pro nový poloviční obrázek
if(NewPic == NULL)
{
fprintf(stderr, "Nelze pridelit pamet(%d kB)\n", (sizeof(BYTE) * NewSizeX * NewSizeY * Bpp) / 1024);
return false;
}
// Projdeme starý obrázek a nahrajeme ho do polovičního. Přitom vždy vytvoříme ze 4 pixelů jeden, který bude jejich průměrem.
int x, y, b;
for(y = 0 ; y < NewSizeY ; y++)
{
for(x = 0 ; x < NewSizeX ; x++)
{
for(b = 0 ; b < Bpp ; b++)
{
// NewPic[(x + y * NewSizeX) * Bpp + b] = Data[(x*2 + y*2 * SizeX) * Bpp + b];// Bez vyhlazení
NewPic[(x + y * NewSizeX) * Bpp + b] = (BYTE) ((float) (Data[(x*2 + y*2 * SizeX) * Bpp + b] + Data[(x*2+1 + y*2 * SizeX) * Bpp + b] + Data[(x*2 + (y*2+1) * SizeX) * Bpp + b] + Data[(x*2+1 + (y*2+1) * SizeX) * Bpp + b]) / 4.0f);// S vyhlazením
}
}
}
// Uvolnění starého obrázku a nastavení ukazatele na nový
free(Data);
SizeX = NewSizeX;
SizeY = NewSizeY;
Data = NewPic;
return true;// OK
}
Pomalu se vrhneme na funkci vytvářející z obrázku texturu, ale předtím se nejdříve podíváme na její pomocnou funkci, která ověřuje podporu požadovaného formátu. Pomocí glGetIntegerv() zjistíme počet podporovaných formátů, abychom mohli alokovat dostatečně velkou paměť pro jejich seznam. Poté si pomocí stejné funkce vyžádáme onen seznam, který následně prohledáme. Pokud nalezneme shodu s formátem zadaným v jediném parametru, funkce vrátí true. Pokud nebude shoda nalezena, což znamená že tento formát není podporován, vrátíme false.
bool GetCompressTexExt(int Format)// Zjistí přítomnost požadovaného rozšíření formátu pro texturu
{
GLint NumFormat = 0;
GLint *Formats = NULL;
glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS_ARB, &NumFormat);
Formats = (GLint *) malloc(sizeof(GLint) * NumFormat);
if(Formats == NULL)
{
return false;
}
glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS_ARB, Formats);
for(GLint i = 0 ; i < NumFormat ; i++)
{
if(Format == Formats[i])
{
free(Formats);
return true;
}
}
free(Formats);
return false;
}
Konečně se dostáváme k funkci CreateTexture(), která po vytvoření textury vrátí její OpenGL adresu. Napřed si vysvětlíme parametry. Do MinFilter a MagFilter se zadává filtrování textury. V těchto parametrech můžete použít klasické hodnoty OpenGL (GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST, ...) nebo pro zjednodušení RV_LINEAR a RV_NEAREST, které jsou definované v hlavičkovém souboru naší třídy. Za tyto hodnoty vám funkce sama dosadí podle dalšího parametru MipMaps správné filtrování pro normální nebo mipmapové textury. Parametr BitsPerColor určuje velikost jedné složky barvy v paměti grafické karty. Jedna složka může být třeba i 4 bity, což je polovina bytu a to je taky důvod proč se zadává v bitech. Poslední parametr Compress zapíná komprimaci textur, u které odpadá nutnost nastavovat počet bitů na složku barvy.
GLuint Picture::CreateTexture(int MinFilter, int MagFilter, int BitsPerColor, bool MipMaps, bool Compress)
{
// Zjistí, jestli jsou hodnoty filtrování GL* nebo RV* - předělá je na GL
if(MinFilter == RV_NEAREST)
{
if(MipMaps)
{
MinFilter = GL_NEAREST_MIPMAP_NEAREST;
}
else
{
MinFilter = GL_NEAREST;
}
}
else if(MinFilter == RV_LINEAR)
{
if(MipMaps)
{
MinFilter = GL_LINEAR_MIPMAP_LINEAR;
}
else
{
MinFilter = GL_LINEAR;
}
}
if(MagFilter == RV_NEAREST)
{
MagFilter = GL_NEAREST;
}
else if(MagFilter == RV_LINEAR)
{
MagFilter = GL_LINEAR;
}
Zde podle počtu bytů na pixel určíme OpenGL formát textury. GL_ALPHA má jednu složku, RGB má tři a RGBA má čtyři složky. Pokud je obrázek v jiném formátu, nastane chyba. Není ale problém, podle potřeby dopsat i jiné formáty nebo napsat přetíženou funkci která bude mít o parametr víc právě pro formát textury. Já jsem zvolil tento postup proto, aby byla funkce co nejvíce samostatná a nemuselo se zadávat zbytečně moc parametrů, jejichž zjišťování by pouze zdržovalo psaní programu a snižovalo jeho přehlednost.
unsigned int glFormat;// Nastaví formát podle počtu bytů na barvu
switch(Bpp)
{
case 1:
glFormat = GL_ALPHA;
break;
case 3:
glFormat = GL_RGB;
break;
case 4:
glFormat = GL_RGBA;
break;
default:
fprintf(stderr, "Nelze vybrat format textury. Obrazek obsahuje %d bytu na pixel\n", Bpp);
return 0;
}
Zde se podle formátu textury vyhodnocuje její internal formát, který udává, jak se má textura v paměti uložit. To může být jeden z komprimačních formátů nebo obyčejné GL_RGB8, které za nás podle parametru BitsPerColor vybere funkce GetInternalFormat(). Nebudu ji zde popisovat (je to jen seznam, prohlédněte si ji ve zdrojích).
// Nastaví internal format podle počtu Bitů na barvu, nebo vybere compress program
unsigned int InternalFormat;
if(Compress)
{
if(glFormat == GL_RGB)
{
InternalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
}
else if(glFormat == GL_RGBA)
{
InternalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
}
else
{
fprintf(stderr, "Chyba, komprimovane textury mohou byt pouze ve formatu RGB nebo RGBA\n");
return 0;
}
if(GetCompressTexExt(InternalFormat) == false)
{
fprintf(stderr, "Graficka karta nepodporuje rozsireni potrebne pro komprese textur\n");
return 0;
}
}
else
{
if((InternalFormat = GetInternalFormat(glFormat, BitsPerColor)) == 0)
{
fprintf(stderr, "Nelze vybrat internal format. glFormat %d, bytu na slozku barvy %d\n", glFormat, Bpp);
return 0;
}
}
Vytvoření textury z načteného obrázku - rutina, kterou každý OpenGL programátor vygeneruje i o půlnoci. Použijeme zde hodnoty, které jsme předtím pracně shromažďovali a vybírali.
// Vytvoření textury
GLuint TexID;
glGenTextures(1, &TexID);
glBindTexture(GL_TEXTURE_2D, TexID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, MinFilter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, MagFilter);
if(MipMaps)
{
gluBuild2DMipmaps(GL_TEXTURE_2D, InternalFormat, SizeX, SizeY, glFormat, GL_UNSIGNED_BYTE, Data);
}
else
{
glTexImage2D(GL_TEXTURE_2D, 0, InternalFormat, SizeX, SizeY, 0, glFormat, GL_UNSIGNED_BYTE, Data);
}
return TexID;
}
Popis třídy Picture je šťastně za námi, ale jak ji v programu použít? Stačí includovat Picture.h (+ Picture.cpp) a napsat něco na tento způsob:
Picture Pic;// Objekt třídy
if(!Pic.Load("Alien2.tga"))// Nahrání obrázku
{
return false;
}
GLuint Texture = Pic.CreateTexture(RV_LINEAR, RV_LINEAR, 8, true, false);// Vytvoření OpenGL textury
if(Texture == 0)// Chyba při vytváření textury
{
return false;
}
V tomto případě jsem ani nepoužil funkci free na uvolnění paměti, protože se po skončení funkce automaticky zavolá destruktor.
A to je vše, jak prosté! No zas tak prosté to nebylo. Jen malé upozornění pro rejpaly: až si budete prohlížet funkci FlipHorizontal(), tak mi nepište, že jsem mohl prohazovat celé řádky a ne pixel po pixelu. Při tomto postupu totiž nepotřebujeme dynamicky přidělit paměť pro celý řádek, ale používáme statické pole o deseti prvcích a budeme předpokládat velikost pixelů menší než deset bytů (samozřejmě je to ošetřené ifem).
napsal: Radomír Vrána <rvalien (zavináč) c-box.cz>
Tento článek byl napsán pro web http://nehe.ceske-hry.cz/. Pokud ho chcete umístit i na své stránky, zeptejte autora.