Komprimované textury a SDL_Image

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.

Zdrojové kódy