Allegro (2) - Práca s grafikou

Allegro (2) - Práca s grafikou
04.08.2008 23:45 | Články | Samuel BWPOW Kupka

Druhá časť seriálu je venovaná práci s grafikou. Vzhľadom na to, že prevažná časť knižnice Allegro je venovaná práve práci s grafikou, nie je v tomto článku možné prebrať všetky aspekty a ponúkané možnosti. Preto ho treba chápať len ako stručný úvod do problematiky.

Článok bude opäť obohatený o príklady. Budeme pokračovať tam, kde sme v predchádzajúcom článku skončili. Čitateľov však nabádam k tomu, aby si všetky príklady vyskúšali naprogramovať samostatne a taktiež skúšali aj funkcie, ktoré nebudú v tomto článku použité. Tréning robí majstra a programovať sa nedá naučiť inak ako samotným programovaním. Na konci článku nájdete odkazy na všetky relevantné manuálové stránky.

Princíp práce s grafikou

Všetky grafické operácie v Allegre, vrátane vstupu a výstupu, pracujú s bitmapovou (alebo rastrovou) grafikou zloženou z pixelov. Pixel je základná jednotka bitmapovej grafiky, pričom každý z nich má jedinú vlastnosť - farbu. Od tejto farby sa dajú odvodiť niektoré dodatočné vlastnosti, ako napríklad priesvitnosť. Počet rôznych farieb (hodnôt), ktoré môže nadobúdať, určuje farebná hĺbka, ktorá je ekvivalentná počtu bitov, ktoré jeden pixel obsadzuje v pamäti. Allegro podporuje nasledovné farebné hĺbky (v bitoch): 8, 15, 16, 24 a 32. Samotná číselná hodnota farby však nemá ešte žiadnu výpovednú hodnotu. Farba v našom ponímaní, ktorú ľudské oko vníma, je totiž zložená z viacerých zložiek. Teória o farbách je už mimo témy tohto článku, ale v prípade záujmu môžete začať svoju púť za hlbším poznaním na wikipedii. Dôležité je spomenúť, že Allegro využíva trojzložkový RGB model, kde každá zložka môže nadobúdať maximálne 256 rôznych hodnôt (8-bit). Plnú možnú informáciu teda nesie pri 24-bitoch (RGB) a pri 32-bitoch (RGBA), kde okrem farebných zložiek nájdeme aj alfa zložku (v Allegre využívanú napríklad na priesvitnosť). Pri 16-bitovom režime sú zložky RGB rozdelené po 5-6-5 bitoch, keďže na zmeny zelenej farby je ľudské oko najcitlivejšie. Pri 15-bitovom je to 5-5-5 a pri 8-bitovom nie sú farebné zložky zakódované priamo vo farbe pixelu, ale používa sa dodatočná tabuľka obsahujúca 256 32-bitových RGB záznamov, nazývaná paleta. V dnešnej dobe je však používanie 8-bitového režimu v útlme a tak sa mu ani nebudeme bližsie venovať. Režimy, ktoré budeme prakticky používať, sú 16, 24 a 32-bit. Je dôležité si pamätať, že farebná hĺbka je globálna premenná, ktorá sa nastavuje volaním funkcie void set_color_depth(int depth); a od momentu nastavenia bude používaná pri volaní všetkých grafických funkcií.

Pixely sú teda jednotky tvoriace obraz. Pristupovať k nim (kvôli zmene alebo čítaniu) je možné zadaním ich súradníc priamo v pixeloch (označuje sa px). Súradnicová sústava (jej orientácia) je možno iná, na akú ste zvyknutí z matematiky. Keďže prvé obrazovky boli určené hlavne na textový výstup a v západnom svete sa číta zľava doprava a zhora dole, súradnicová sústava využívaná pri vykresľovaní má počiatok v ľavom hornom rohu, pričom súradnica X rastie smerom k pravému hornému rohu a súradnica Y smerom k ľavému dolnému rohu.

Súradnicová sústava

Na obrázku je naznačená aj os Z, ktorá vyjadruje "hĺbku". Stretneme sa s ňou až neskôr, pri článkoch o 3D grafike.

V Allegre sa na "uskladnenie" grafiky používa štruktúra BITMAP. Výťah dôležitých premenných:


   int w, h;                     /* width and height in pixels */
   int clip;                     /* flag if clipping is turned on */
   int cl, cr, ct, cb;           /* clip left, right, top and bottom values */
   void *dat;                    /* the memory we allocated for the bitmap */

Táto obsahuje samotné pixely (resp. ich hodnoty) po riadkoch, rozmery a informácie o prípadnom orezaní (regióne, do ktorého je možné kresliť). Po inicializácii grafického režimu sa automaticky nastaví globálna premenná extern BITMAP *screen;, ktorá reprezentuje obrazovku. Je teda možné do nej ihneď začať kresliť. Orezávanie je zapnuté a nastavené na jej aktuálne rozmery a tým pádom sa nič nestane, ak sa pokúsite vykresliť niečo mimo, príkaz sa proste nevykoná. Ak by ste deaktivovali kontrolu orezávania, program by Vám pri pokuse vykresliť niečo mimo obrazovky skoro určite spadol. Prejdime si teda prvé príkazy:

Niektoré z týchto funkcií sú použité v nasledujúcom príklade (celý zdrojový kód).

Príklad 1

    while(!close_button_pressed){
        int farba=makecol(rand()%256,rand()%256,rand()%256);
        putpixel(screen,rand()%screen->w,rand()%screen->h,farba);
        rest(10);
    }

Primitívy

Po vykreslení prvých pixelov prichádzajú na rad primitívy. Allegro ich pozná niekoľko, pričom sa snaží využívať aj možnú akceleráciu grafickej karty (napríklad pri úsečkách). Tu je zoznam niektorých funkcií na ich vykresľovanie:

Zoznam všetkých funkií nájdete v manuáli.

Niektoré z týchto funkcií sú použité v nasledujúcom príklade (celý zdrojový kód).

Príklad 2

    int x=screen->w/2;
    int y=screen->h/2;
    int ax,ay;
    while(!close_button_pressed){
        int farba=makecol(rand()%256,rand()%256,rand()%256);
        line(screen,x,y,ax=rand()%screen->w,ay=rand()%screen->h,farba);
        x=ax;
        y=ay;
        if(rand()&1){
            circle(screen,x,y,5,farba);
        }
        else{
            rect(screen,x-5,y-5,x+5,y+5,farba);
        }

        rest(300);
    }

V príklade je náhodne vybranou farbou nakreslená úsečka z bodu [X,Y] do bodu [AX,AY] a následne je okolo tohto bodu vykreslená kružnica alebo štvorec.

Schopnosť vykresliť bodky (príklad 1) alebo čarbanice (príklad 2) síce poteší, ale teraz je už načase načrieť trochu viac do programátorských a matematických schopností v nasledujúcom príklade (celý zdrojový kód).

Príklad 3

#define DLZKA 512
 
struct XY{
    int x,y;
};

    int x=screen->w/2;
    int y=screen->h/2;
    XY xy[DLZKA],a;
    float uhol=0;
    for(int i=0;i<DLZKA;i++){
        xy[i].x=x;
        xy[i].y=y;
    }
    int pos=0;
    while(!close_button_pressed){
        for(int i=0;i<DLZKA;i++){
            a=xy[(pos+i)%DLZKA];
            if(i+pos>=DLZKA){
                circlefill(screen,a.x,a.y,10,makecol((i+1)*255/DLZKA,0,0));
            }
        }
        a.x+=(int)(cos(uhol)*8);
        a.y+=(int)(sin(uhol)*8);
        uhol+=(float)(rand()%101-50)/100;
        if(a.x<0||a.y<0||a.x>=screen->w||a.y>=screen->h) uhol+=1;
        xy[(pos++)%DLZKA]=a;
        rest(20);
    }

Pred očami sa nám začal chaoticky hadiť červený had Nehad, pričom jeho chvost postupne slabne, až sa stráca úplne. Samotný Nehad je tvorený určitým počtom kruhov, pričom kruh na začiatku hada má sýtu červenú farbu a všetky ďalšie kruhy majú postupne farbu menej sýtu. Začiatok má teda červenú zložku na maxime a ostatné zložky na nule. Budeme to zapisovať ako (255,0,0). Úplne posledný krúžok hada má farbu už úplne čiernu, teda (0,0,0). Krúžky sú vykresľované pri každom pohybe a kreslia sa od chvosta (od najtmavšieho) po hlavu (po najsvetlejší). Všetky sa vykresľujú priamo na obrazovku, čo môže spôsobovať škaredé blikanie, ak sa jednotlivé krúžky navzájom prekrývajú. Ako to len vyriešiť?

Pamäťové bitmapy

Ako sme videli v predchádzajúcom príklade, vykresľovanie priamo na obrazovku nie je to pravé. Preto sa využíva niekoľko techník. Prvou je takzvaný double- alebo triple buffering. Ide o techniku, kedy existuje v pamäti grafickej karty viac obrazovkových bufferov súčasne, pričom viditeľný je vždy len jeden z nich (aktívny), kým do druhého (alebo tretieho) sa kreslí. Keď je kreslenie dokončené, grafickej karte sa prikáže, aby okamžite začala vykreslovať druhý buffer. Kým karta zobrazuje druhý buffer, kreslí sa opäť do prvého a takto stále dookola. Táto technika je už veľmi stará no používa sa až dodnes. V minulosti jej uplatneniu prekážali hlavne grafické karty s nízkym množstvom pamäte (napríklad 640x480 16-bit zaberá 600KiB, na double buffering bolo teda treba až 1,2MiB grafickej pamäte). Dnes už našťastie tento problém neexistuje. Ďalšou prekážkou je pomerne pomalý prístup ku grafickej pamäti cez zbernicu (PCI, AGP alebo najnovšia PciExpress) oproti rýchlemu prístupu do systémovej RAM. Preto je vhodnejšie, v prípade ak sa vykonáva kreslenie na CPU, aby bola celá scéna pripravovaná v systémovej RAM a až po jej dokončení, bola celá naraz presunutá na grafickú kartu. Tento príncíp budeme využívať aj my, keďže väčšinu nášho kreslenia bude vykonávať CPU. Ku kresleniu priamo na grafickej karte sa vrátime v pokračovaní tohto seriálu pri AllegroGL (Allegro + OpenGL).

Ako kresliace plochy (nazývané aj canvas alebo plátno) fungujú v Allegre BITMAPy. Jednu z nich, screen, sme už využili. Môžeme si však vytvoriť aj vlastné. Následne môžeme do nich kresliť alebo ich kopírovať medzi sebou. Tu je zoznam niektorých funkcií na manipuláciu s nimi:

Použijeme ich v nasledujúcom príklade (celý zdrojový kód).

Príklad 4

    BITMAP *actual=create_bitmap(screen->w,screen->h);
    if(actual==NULL) main_koniec(3,"Nepodarilo sa naalokovat pamat pre bitmap!");
    clear_bitmap(actual);
 
    int x=screen->w/2;
    int y=screen->h/2;
    XY xy[DLZKA],a;
    float uhol=0;
    for(int i=0;i<DLZKA;i++){
        xy[i].x=x;
        xy[i].y=y;
    }
    int pos=0;
    while(!close_button_pressed){
        for(int i=0;i<DLZKA;i++){
            a=xy[(pos+i)%DLZKA];
            if(i+pos>=DLZKA){
                circlefill(actual,a.x,a.y,10,makecol((i+1)*255/DLZKA,0,0));
            }
        }
        a.x+=(int)(cos(uhol)*8);
        a.y+=(int)(sin(uhol)*8);
        uhol+=(float)(rand()%101-50)/100;
        if(a.x<0||a.y<0||a.x>=screen->w||a.y>=screen->h) uhol+=1;
        xy[(pos++)%DLZKA]=a;
        blit(actual,screen,0,0,0,0,actual->w,actual->h);
        rest(20);
    }
 
    destroy_bitmap(actual);

Príklad opäť vykresľuje hada Nehada, tentokrát však nie sú kruhy posielané priamo na obrazovku, ale kreslené do pamäťového bufferu, ktorý je následne celý poslaný na obrazovku.

Načítanie zo súboru, sprity

Po tom, ako sme sa naučili používať pamäťové bitmapy, sme len krôčik od načítavania obrázkov zo súboru. Allegro podporuje niekoľko grafických formátov. Dnes, už ide žiaľ, o tie menej rozšírené. Sú to BMP, PCX, TGA a LBM. Prvé tri z nich vie aj ukladať. Slúžia na to funkcie:

Zoznam všetkých funkcií nájdete v manuálových stránkach. Dôležité je taktiež poznamenať, že v prípade, ak má uložený obrázok inú farebnú hĺbku, ako je aktuálne nastavená, obrázok je pri načítaní automaticky skonvertovaný.

S obrázkami (a celkovo s pamäťovými buffermi) sa dajú robiť zaujímavé veci. Okrem kopírovania oblastí (blit) je možné obrázok vykresliť ako takzvaný sprite. Predstavte si, že máte hada, ktorý by ale mal mať nejakú hlavu s očami a ústami. No kresliť oči a ústa pomocou primitív je pomerne náročné. Preto by ste chceli použiť hotový obrázok hlavy. Problém ale je, že hlava by mala byť okrúhla a blitovanie podporuje len celé obdĺžniky. Riešenie je jednoduché. Jedna farba v Allegre (a v mnohých iných knižniciach) je v istých prípadoch použitá ako priehľadná. V našom prípade to je farba fialová, červená a modrá zložka na maximum, zelená na nulu, teda (255,0,255). Stačí použiť túto farbu ako pozadie okolo hlavy a nevykresliť ju pomocou funkcie blit, ale pomocou:

Použijeme ich v nasledujúcom príklade (celý zdrojový kód).

Príklad 5

    BITMAP *actual=create_bitmap(screen->w,screen->h);
    if(actual==NULL) main_koniec(3,"Nepodarilo sa naalokovat pamat pre bitmap!");
    clear_to_color(actual,makecol(255,255,255));
 
    BITMAP *penguin=load_bitmap("penguin.bmp",NULL);
    if(penguin==NULL) main_koniec(3,"Nepodarilo sa nacitat penguin.bmp!");
 
    for(int y=0;y<actual->h;y+=penguin->h){
        for(int x=0;x<actual->w;x+=penguin->w*2){
            blit(penguin,actual,0,0,x+((y/penguin->h)%2)*penguin->w,y,penguin->w,penguin->h);
        }
    }
 
    destroy_bitmap(penguin);
 
    BITMAP *hlava=load_bitmap("hlava.bmp",NULL);
    if(hlava==NULL) main_koniec(3,"Nepodarilo sa nacitat hlava.bmp!");
 
    int x=screen->w/2;
    int y=screen->h/2;
    XY xy[DLZKA],a;
    float uhol=0;
    for(int i=0;i<DLZKA;i++){
        xy[i].x=x;
        xy[i].y=y;
    }
    int pos=0;
    a.x=-100;
    a.y=-100;
    while(!close_button_pressed){
        circlefill(actual,a.x,a.y,16,0);
        for(int i=0;i<DLZKA;i++){
            a=xy[(pos+i)%DLZKA];
            if(i+pos>=DLZKA){
                circlefill(actual,a.x,a.y,10,makecol((i+1)*255/DLZKA,0,0));
            }
        }
        a.x+=(int)(cos(uhol)*8);
        a.y+=(int)(sin(uhol)*8);
        uhol+=(float)(rand()%101-50)/100;
        if(a.x<0||a.y<0||a.x>=screen->w||a.y>=screen->h) uhol+=1;
        xy[(pos++)%DLZKA]=a;
        draw_sprite(actual,hlava,a.x-hlava->w/2,a.y-hlava->h/2);
        blit(actual,screen,0,0,0,0,actual->w,actual->h);
        rest(20);
    }
 
    destroy_bitmap(actual);
    destroy_bitmap(hlava);

Na začiatku načítame obrázok milého tučniačika a šachovnicovo ním vytapetujeme pozadie. Následne samotného tučniačika zničíme, keďže ho už viac nepotrebujeme. Potom načítame hlavu hada Nehada. Samotného hada vykresľujeme rovnako, ako v predchádzajúcom príklade, akurát tesne pred skopírovaním actual na obrazovku, dokreslíme aj hlavu. Ale čo sa to deje? Had nám požiera pekné pozadie s tučniačikmi a zanecháva za sebou skazu. A ani hlava sa správne nenatáča. Takto to určite nemôže ostať!

Priehľadnosť, rotácia

Allegro podporuje rotáciu pri vykresľovaní. Ako ste si určite všimli, na pohyb nášho hada používame uhol v radiánoch, ktorý náhodne meníme. Na slovenských školách sa pri výpočtoch bežne používajú stupne. Allegro nevyužíva ani jeden z týchto systémov určovania veľkosti uhlov a definuje vlastný. Ba čo viac, definuje aj vlastný číselný typ. Ide o typ typedef long fixed (viac tu). Uhly sa teda v Allegre zadávajú ako fixed čísla od 0 po 256. V našom prípade teda musíme spraviť konverziu z radiánov.

Porovnanie veľkostí uhlov

Nasleduje popis niektorých funkcií:

Otáčaciu hlavu máme teda vyriešenú, ale stále sme nevyriešili problém s hadom požierajúcim pozadie. Doteraz sme hada kreslili tak, že sme vykreslili niekoľko kruhov, ktoré sa postupne zosvetľovali. Aby had chodil po pozadí, potrebujeme miesto obyčajného zosvetľovania (rastu červenej zložky) spraviť kruhy bližšie ku koncu hada stále viac priesvitné. Priesvitnosť (a niektoré ďalšie efekty) sú v Allegre riešené veľmi elegantne. V našom prípade potrebujeme priehľadne vykresľovať primitívy. To dosiahneme pomocou nasledujúcich funkcií:

Všetko si predvedieme v nasledujúcom príklade (celý zdrojový kód).

Príklad 6

    BITMAP *actual=create_bitmap(screen->w,screen->h);
    if(actual==NULL) main_koniec(3,"Nepodarilo sa naalokovat pamat pre bitmap!");
 
    BITMAP *back=create_bitmap(screen->w,screen->h);
    if(back==NULL) main_koniec(3,"Nepodarilo sa naalokovat pamat pre bitmap!");
 
    clear_to_color(back,makecol(255,255,255));
 
    BITMAP *penguin=load_bitmap("penguin.bmp",NULL);
    if(penguin==NULL) main_koniec(3,"Nepodarilo sa nacitat penguin.bmp!");
 
    for(int y=0;y<back->h;y+=penguin->h){
        for(int x=0;x<back->w;x+=penguin->w*2){
            int r=rand()&3;
            if(r==0) draw_sprite(back,penguin,x+((y/penguin->h)%2)*penguin->w,y);
            if(r==1) draw_sprite_v_flip(back,penguin,x+((y/penguin->h)%2)*penguin->w,y);
            if(r==2) draw_sprite_h_flip(back,penguin,x+((y/penguin->h)%2)*penguin->w,y);
            if(r==3) draw_sprite_vh_flip(back,penguin,x+((y/penguin->h)%2)*penguin->w,y);
        }
    }
 
    destroy_bitmap(penguin);
 
    BITMAP *hlava=load_bitmap("hlava.bmp",NULL);
    if(hlava==NULL) main_koniec(3,"Nepodarilo sa nacitat hlava.bmp!");
 
    int x=screen->w/2;
    int y=screen->h/2;
    XY xy[DLZKA],a;
    float uhol=0;
    for(int i=0;i<DLZKA;i++){
        xy[i].x=x;
        xy[i].y=y;
    }
    int pos=0;
    a.x=-100;
    a.y=-100;
    drawing_mode(DRAW_MODE_TRANS,actual,0,0);
    while(!close_button_pressed){
        blit(back,actual,0,0,0,0,back->w,back->h);
        for(int i=0;i<DLZKA;i++){
            a=xy[(pos+i)%DLZKA];
            if(i+pos>=DLZKA){
                set_trans_blender(255,0,0,(i+1)*255/DLZKA);
                circlefill(actual,a.x,a.y,10,makecol(255,0,0));
            }
        }
        a.x+=(int)(cos(uhol)*8);
        a.y+=(int)(sin(uhol)*8);
        pivot_sprite(actual,hlava,a.x,a.y,hlava->w/2,hlava->h/2,fmul(ftofix(uhol),radtofix_r));
        uhol+=(float)(rand()%101-50)/100;
        if(a.x<0||a.y<0||a.x>=screen->w||a.y>=screen->h) uhol+=1;
        xy[(pos++)%DLZKA]=a;
        blit(actual,screen,0,0,0,0,actual->w,actual->h);
        rest(20);
    }
 
    destroy_bitmap(actual);
    destroy_bitmap(hlava);

Had Nehad už nemaže pozadie, ale ako na potvoru, vzniká opäť škaredý efekt na jeho chvoste. Kým doteraz sme kruhy celkom prekresľovali, pri priesvitnosti vidno všetky kruhy pod sebou. Aj toto sa dá pomerne jednoducho vyriešiť, ale to ponechám ako cvičenie pre čitateľov.

Určite ste si všimli, že v príklade sú použité niektoré funkcie, ktoré neboli spomenuté v texte. Z názvov je ale pomerne jednoduché zistiť, čo je ich úlohou.

Záver

Allegro poskytuje obrovské možnosti, čo sa týka vykresľovania a manipulácie s grafikou. V tomto článku je spomenutá len veľmi malá časť z nich. Preto povzbudzujem čitateľov k tomu, aby si otvorili manuál a prezreli si zoznam funkcií (odkazy na jednotlivé stránky sú na konci článku). Aj keď mnoho z nich možno nikdy nevyužijete, je dobré o ich existencii vedieť.

V následujúcej časti sa budeme venovať práci s klávesnicou a myšou. Po nej bude nasledovať časť venovaná práci so súbormi a kompresii. Zároveň v nej začneme konečne pracovať na hre Packoban, keďže už budeme mať na to dostatok vedomostí.

Odkazy