A monitorok, legyenek azok buborék (képcsöves) vagy lapos (lcd) fajtájúak, (de még a legmodernebb OLED kijelzők is) a vörös, zöld és kék szín összekeveréséből állítják elő a színeket. Minden képpont helyén igazából három van, amelyek eltérő színű fénnyel világítanak. Ez nagyítóval nézve látszik is, érdemes megnézni.
Mivel a monitornak ilyen komponensekből kell összeállítaniuk a színeket, ezért azok a videókártya memóriájába is ilyen formátumban kerülnek. A három színösszetevőt az RGB betűkkel szokás jelölni: red, green, blue. Az egyes színösszetevők erősségét a hardver általában egy 0..255 skálán, vagyis 8 biten tárolja. 0 a minimális fényerő (fekete), 255 a maximális. Így három bájton 256×256×256=28×28×28=224-féle szín keverhető ki, összesen 16 777 216 darab. Ez nagyjából annyi, mint amennyit az emberi szem meg tud különböztetni.
Ha spórolni kell a memóriával vagy a számításokkal, akkor kevesebb biten, pontatlanabbul is tárolhatóak az egyes értékek. Az elterjedt variációk:
- 24 bit: a fent említett.
- 32 bit: igazából ez ugyanaz, mint a 3×8=24 bites, csak minden bájthármas után egy negyedik üres
bájtot helyeznek el; azért, hogy egy képpont memóriacíme néggyel való szorzással legyen
számítható. Ez egyszerűbb, mint a 3-mal való szorzás, mivel bitléptetéssel is megoldható.
A négy bájt egyszerre írható és olvasható, egy 32 bites egész típussal (SDL-ben az
Uint32
direkt erre a célra van). - 15 bit: minden komponensre 5 bit jut, vagyis 25=32-es skálán adhatóak meg.
- 16 bit: egy bájt általában 8 bites, ezért a 15 bites tárolásnál egy bit szabadon maradt. Ezzel a bittel a zöld komponens pontosságát érdemes megnövelni, mert az emberi szem arra a legérzékenyebb. Vagyis: R=5 bit, G=6 bit, B=5 bit.
- 8 bit: ezt komponensekre felosztva már túl kevés bit jutna az egyes színekre. A 8 bites tárolásnál egy palettát használnak: 256 szín tömbje, amelyben a képponthoz tartozó 8 bites szám az index.
Az SDL az összes fent említett formátumot támogatja, viszont ehhez be kell mutatni egy újabb rajzolási technikát.
Az eddig bemutatott rajzolási technikák arra épültek, hogy az ablakhoz tartozó (SDL_Window
)
renderelő / rajzoló kontextus (SDL_Renderer
) segítségével az ablak felületére rajzoltunk, az
SDL_gfxPrimitives könyvtár függvényei segítségével. Felmerül a kérdés viszont, hogy hogyan érhetjük el esetleg
az eddigi rajzainkat, vagy állítgathatjuk az egyes pixelek színét hatékonyabban, mint az egyesével való függvényhívás.
Az SDL erre biztosít lehetőséget, viszont nem a rajzoló kontextuson keresztül (mint elsőre
hinné az ember), hanem az ablak segítségével.
Van egy SDL_GetWindowSurface(window)
függvény, ami visszaad egy SDL_Surface*
típusú pointert,
ami a rajzoló felület szimbolizálja (tartalmazza annak méretét, az egyes pixelek színeit, stb...). Ennek a struktúrának
a segítségével sokkal szabadabb irányítást kapunk a pixelek fölött. Igény szerint akár lekérhető egy pixelek tömbjére mutató
pointer, amiben direktbe manipulálhatók a színek. Továbbá itt történik meg a varázslat, a különböző formátumok támogatására.
A kulcs az SDL_BlitSurface()
függvény, ami egy felület megadott pixeleit másolja egy másik felületre.
Továbbá, ha két különböző formátumban lévő képünk van, akkor is másolható egyik a másikra; a szükséges átalakítást
az SDL_BlitSurface()
függvény elvégzi, még ha lassú is lesz tőle a program. Néha azonban szükségünk van arra,
hogy mi magunk manipuláljuk az egyes képpontokat.
Minden SDL_Surface
tartalmaz egy pixels
nevű pointert, ez mutat a memóriában
a képpontokat tároló adatterületre. Tartalmaz továbbá egy SDL_PixelFormat
struktúrát is,
amelyik a kép tárolási módjáról tartalmaz információkat. Ennek a lényeges adattagjai a következők:
typedef struct {
...
Uint8 BitsPerPixel;
Uint8 BytesPerPixel;
Uint8 Rloss, Gloss, Bloss, Aloss;
Uint8 Rshift, Gshift, Bshift, Ashift;
Uint32 Rmask, Gmask, Bmask, Amask;
...
} SDL_PixelFormat;
A BitsPerPixel
mező a fent említett formátumokra utal, értéke
lehet pl. 8, 16 vagy 24. A BytesPerPixel
arra használható, hogy egy
képpont memóriacímét egy soron belül könnyen ki tudjuk számolni, mert azt mondja
meg, hány bájt (sizeof(char)
) egy képpont. A képponthoz tartozó
bájtok manipulációja a többi mezőkben tárolt számok segítségével végezhető el.
Egy példán a legegyszerűbb látni az egészet. Tegyük fel, hogy az R=0xEB, G=0x68, B=0x3E színt (amelyik ez a szín ) szeretnénk tárolni 16 biten. Az R komponensre 5, a G-re 6, a B-re 5 bitet szánunk. Ha ezeket ilyen sorrendben is tároljuk el a 16 biten, vagyis két bájton belül, akkor a következő formátum adódik.
A vörös (R) komponens 8 bit helyett csak 5 biten tárolódik. Ezért az alsó 3 bitet el kell
dobni. Az SDL_PixelFormat
struktúrában az Rloss
mező az R komponensből
eldobott bitek számát tárolja. A kapott értéket Rshift
bitnyivel feljebb toljuk;
mert nem a 16 bit legalsó, hanem a legfelső helyiértékű bitjein tárolódik az R komponens.
Ugyanígy működik ez a többi komponensnél is; a zöldet 6 biten tároljuk, ezért Gloss
csak 2. A kék komponens pedig a végleges számban a legalsó helyiértékű bitekre kerül, ezért
Bshift
0 lesz — legalábbis ebben a példában, mert elképzelhető lenne az is, hogy
nem R, G, B sorrendben tároljuk a komponenseket, hanem B, G, R sorrendben. Az SDL úgy hozza létre
a képeket, hogy mindig beállítja ezeket a mezőket a megfelelő értékre, megkönnyítve ezzel a
dolgunkat. A fentiek alapján, ha egy adott r,g,b színhez keressük azt a (több bájtos) számot,
amelyet a memóriába kell írnunk:
adat = (r >> Rloss << Rshift) | (g >> Gloss << Gshift) | (b >> Bloss << Bshift);
Az Rmask
, Gmask
és Bmask
mezők bitmaszkokat
tartalmaznak. Az Rmask
például egy olyan számot, amelyben ott vannak egyesek, ahol a
képpont adatában az R komponenshez tartozó rész található. Ezt bináris ÉS kapcsolatba hozva a
képpont adattal ki tudjuk vágni belőle az R komponenshez tartozó részt. Ezt a helyére tolva
visszakapjuk az eredeti r értéket. Vagyis a fenti számítás visszafelé így végezhető el:
r = (adat & Rmask) >> Rshift << Rloss;
g = (adat & Gmask) >> Gshift << Gloss;
b = (adat & Bmask) >> Bshift << Bloss;
Az Aloss
, Ashift
és Amask
mezők a kép átlátszóságára vonatkozó információk kezelését teszik
lehetővé, ugyanilyen módon.
Lényegében egyébként ez a rajzolás egyik részfeladata, amitől igazából az SDL_gfx megszabadít minket.
Hozzunk létre egy képet!
A Perlin-zaj nevű rekurzív algoritmus a következő módon működik. Egy kép négy sarkában helyezzünk el színeket. Ez a négy pont, a kép négy sarka, egy téglalapot határoz meg. A téglalap oldalait felezzük meg; az oldalfelező pontokban állítsunk be olyan színeket, amelyek a sarkok színei közötti átmenetek. Például a fenti középső pont legyen a két fenti sarokpont színe között félúton lévő szín. A téglalap közepén lévő pontot is színezzük ki, mégpedig úgy, hogy annak a színe a négy sarok színe közötti átmenet legyen; plusz/mínusz egy véletlenszám. A véletlenszám nagysága legyen arányos a téglalap nagyságával, vagyis minél nagyobb a téglalap, annál nagyobb legyen a véletlen eltérés is. Ezekkel a pontokkal a téglalapot négy kisebb részre osztottuk; csináljuk meg a négy kisebb részre ugyanezt (rekurzívan).
A lenti programban ezt a procedúra háromszor fut le. Három kép van; az egyik
lesz a végül kirajzolt kép R, a másik a G, a harmadik pedig a B komponense.
Létrehozunk egy SDL képet a memóriában az SDL_CreateRGBSurface()
függvénnyel, 32 bites színtérrel. A sok nulla a függvény paraméterlistájának a
végén az Rmask
, Gmask
stb. értékek. Nulla esetén az
alapértéket állítja be ezekhez az SDL. Mivel 32 bites képet kértünk,
egy Uint32
-t tudok írni egyszerre a memóriába: az egy képpont. A tömbbe írandó
értékek a fent részletezett módon számolódnak ki.
A képpont megcímzése trükkös. A képhez tartozó pixels
mező egy void *
típusú mutató, amely a képpontok tömbjének első elemére mutat. A pitch
mező
pedig egy egész szám, amely azt mutatja, hogy hány bájtonként kezdődik egy sor. Ezzel meg lehet
határozni a kép egy sorának címét a memóriában; az eredményt egy Uint32 *
típusú
mutatóba kell tenni, amelynek segítségével a kép adott során belül egy adott pixel egyszerű
indexeléssel előállítható. Ha 16 bites képem lenne, akkor Uint16
-ot, ha 8 bites,
akkor pedig Uint8
-at kellene használni. Ezek egyébként az SDL által definiált
típusok; Uint32
egy 32 bites, előjel nélküli egész szám.
Az első kép a program kimenetét mutatja. A második pedig azt, hogy hogyan nézne ki a kép, ha generálás közben megállítanánk. Ezen látszik a rekurzió. A színek az utóbbin úgy vannak beállítva, hogy kék és fehér közötti átmenetek jelenjenek meg; a program így felhőket rajzol.
#include <SDL.h>
#include <stdlib.h>
#include <time.h>
void sdl_init(int szeles, int magas, SDL_Window **pwindow, SDL_Renderer **prenderer) {
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
SDL_Log("Nem indithato az SDL: %s", SDL_GetError());
exit(1);
}
SDL_Window *window = SDL_CreateWindow("SDL peldaprogram", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, szeles, magas, 0);
if (window == NULL) {
SDL_Log("Nem hozhato letre az ablak: %s", SDL_GetError());
exit(1);
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
if (renderer == NULL) {
SDL_Log("Nem hozhato letre a megjelenito: %s", SDL_GetError());
exit(1);
}
SDL_RenderClear(renderer);
*pwindow = window;
*prenderer = renderer;
}
enum { MERETX = 640, MERETY = 480 };
typedef unsigned char Kep[MERETY][MERETX];
void plasma(Kep kep, int x1, int y1, int x2, int y2) {
if (x2-1 > x1) {
kep[y1][(x1+x2)/2] = (kep[y1][x1]+kep[y1][x2])/2;
kep[y2][(x1+x2)/2] = (kep[y2][x1]+kep[y2][x2])/2;
}
if (y2-1 > y1) {
kep[(y1+y2)/2][x1] = (kep[y1][x1]+kep[y2][x1])/2;
kep[(y1+y2)/2][x2] = (kep[y1][x2]+kep[y2][x2])/2;
}
if (x2-1 > x1) {
int c = (kep[y1][x1]+kep[y1][x2]+kep[y2][x1]+kep[y2][x2])/4
+ rand()%((x2-x1)*2)-(x2-x1);
c = c>255?255:(c<0?0:c);
kep[(y1+y2)/2][(x1+x2)/2] = c;
plasma(kep, x1, y1, (x1+x2)/2, (y1+y2)/2);
plasma(kep, (x1+x2)/2, y1, x2, (y1+y2)/2);
plasma(kep, x1, (y1+y2)/2, (x1+x2)/2, y2);
plasma(kep, (x1+x2)/2, (y1+y2)/2, x2, y2);
}
}
int main(int argc, char *argv[]) {
SDL_Window *window;
SDL_Renderer *renderer;
sdl_init(MERETX, MERETY, &window, &renderer);
srand(time(NULL));
Kep komponensek[3];
for (int x = 0; x < 3; x++) {
komponensek[x][0][0] = rand()%256;
komponensek[x][0][MERETX-1] = rand()%256;
komponensek[x][MERETY-1][0] = rand()%256;
komponensek[x][MERETY-1][MERETX-1] = rand()%256;
plasma(komponensek[x], 0, 0, MERETX-1, MERETY-1);
}
/* kerek egy 32 bit/komponens kepet */
/* osszeallitom a kepet az SDL surface-ben */
SDL_Surface *bmp = SDL_CreateRGBSurface(0, MERETX, MERETY, 32, 0, 0, 0, 0);
for (int y = 0; y < MERETY; y++) {
Uint32 *sor = (Uint32*) ((char*) bmp->pixels + y*bmp->pitch);
for (int x = 0; x < MERETX; x++) {
Uint32 szam; /* 32 bites a kep, ez lesz egy pixel */
szam = (komponensek[0][y][x] >> bmp->format->Rloss << bmp->format->Rshift)
| (komponensek[1][y][x] >> bmp->format->Gloss << bmp->format->Gshift)
| (komponensek[2][y][x] >> bmp->format->Bloss << bmp->format->Bshift);
sor[x] = szam;
}
}
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, bmp);
SDL_FreeSurface(bmp); /* mar nem kell */
/* megjelenites */
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
SDL_Event ev;
while (SDL_WaitEvent(&ev) && (ev.type != SDL_QUIT && ev.type != SDL_KEYDOWN)) {
/* csak var */
}
SDL_Quit();
return 0;
}
Egy kis érdekesség. Felmerülhet a kérdés, hogy miért van szükség a renderelő kontextusra (SDL_Renderer
), ha a
felületeken (SDL_Surface
) keresztül is kirajzolható minden. Az SDL_gfxPrimitives
könyvtár miért nem így
lett megvalósítva. Az érdekes válasz az, hogy az SDL régi verziója (1.2) még így működött. Nem volt benne se
SDL_Window
, se SDL_Renderer
, ez a kettő egyben volt az SDL_Surface
, és minden rajzolás ezen
keresztül zajlott.
Mi változott? Az évek alatt elhaladt az SDL1.2 felett az idő. Megjelent az igény a grafikus gyorsítás támogatására, hogy
kihasználhatók legyenek a videókártyák is (ugyanis, ha tömbökben manipuláljuk a pixelek színeit, azt a processzor végzi). A
grafikus kártyák támogatásának bevezetése miatt kellett megjelennie az SDL_Renderer
-nek, ugyanis ez az, ami megteremti
a kapcsolatot a grafikus kártyák nagyon furcsa (és éppen ezért itt tovább nem részletezett) programozási felülete, és egy egyszerű
C program között. Hogy ez mennyire speciális, mutatja talán az is, hogy ha valaki dereferálni próbál egy SDL_Renderer*
pointert, azt a hibaüzenetet kapja, hogy ez egy INCOMPLETE TYPE
, azaz nem ismert a belső szerkezete. Na de tényleg
elég ennyi, a többit majd Számítógépes grafika tárgyon.