OpenGL:n perusteet - Osa 1: Ikkunan luominen
OpenGL on laajassa käytössä oleva käyttöjärjestelmäriippumaton rajapinta 2D- ja 3D-grafiikan piirtoon. Tämä artikkelisarja opettaa sinulle 3D-grafiikan perusteet OpenGL:ää käyttäen. Esimerkeissä käytetään C\C++ kieltä. Tämä on artikkelisarjan ensimmäinen osa.
28.5.2004 julkaistun artikkelin on kirjoittanut markus.
1. Johdanto #
Ennen vanhaan grafiikkaa piirrettiin kirjoittamalla se suoraan näytönohjaimen muistiin. Kun tiettyyn osoitteeseen näytönohjaimen muistissa kirjoitettiin tavu, syttyi vastaavaan kohtaan näyttöä vastaavan värinen pikseli. Ja kun yksi pikseli kerran osattiin piirtää, niin monesta pikselistähän sai aikaan mitä tahansa kuvia. Kaikki kuitenkin muuttui 3D-kiihdytinten myötä. Enää grafiikkaa ei piirrettykään tietokoneen prosessorin avulla, vaan näytönohjaimen prosessorin avulla, joka on varta vasten suunniteltu grafiikan piirtämiseen ja suoriutuu tehtävästä näin ollen jopa 300 kertaa nopeammin vastaavan kellotaajuiseen keskussuorittimeen verrattuna.
Ongelmaksi muodostuu kuitenkin se, että jokainen näytönohjain on erilainen ja eri grafiikkakoodin kirjoittaminen kaikille mahdollisille näytönohjaimille olisi mahdotonta. Tämän takia asia hoidetaan niin sanottujen rajapintojen kautta. Eli näytönohjaimesi ajurit tarjoavat sinulle standardoituja funktioita, jotka toimivat samoin kaikilla näytönohjaimilla. Toinen ilmiselvä hyöty on, että ohjelmoijan ei itse tarvitse kasata kuviota yksittäisistä pikseleistä vaan yksinkertaisten primitiivien (kuten viivojen ja kolmioiden) piirtoon on valmiit funktiot.
3D-korttien alkuaikoina tällaisia rajapintoja oli kolme: Glide, Direct3D ja OpenGL. Glide:ä ylläpitäneen 3Dfx:n hävittyä myös Glide kuoli pois. Niinpä nykyään on olemassa kaksi rajapintaa: Microsoftin DirectX-paketin osana oleva Direct3D ja SGI:n alun perin kehittämä, nykyään ARB:n (suurimpien näytönohjainvalmistajien yhteenliittymä) ylläpitämä OpenGL. Molemmat ovat yhtä nopeita ja molemmilla voi tehdä samat asiat. Ainut ero on, että siinä missä Direct3D toimii vain Windows-käyttöjärjestelmässä (ja sen 8-version muunnos Xbox-pelikonsolissa), OpenGL toimii käytännössä kaikissa käyttöjärjestelmissä ja jopa kämmenmikroissa ja matkapuhelimissa (OpenGL ES). Tämä artikkeli käsittelee OpenGL-rajapintaa, jonka aloituskynnys on Direct3D:tä hieman pienempikin. Käytämme tässä artikkelisarjassa Windows-käyttöjärjestelmää, mutta ikkunan luontia lukuunottamatta OpenGL:n käyttö on kaikilla käyttöjärjestelmillä samanlaista.
OpenGL:n virallisen speksin voit imuroida täältä: http://www.opengl.org/documentation/spec...
OpenGL:n opetteleminen spesifikaatiota tutkimalla on kuitenkin vaivalloista, joten aloittakaamme tutoriaali.
2. Tarvittavat kirjastot #
OpenGL:n otsikkotiedostot tulevat kaikkien yleisimpien kääntäjien mukana. Ne ovat nimeltään opengl.h ja glu.h . Lisäksi, koska tässä artikkelissa teemme Windows-ohjelmia, pitää mukaan liittää windows.h. Ohjelma pitää myös linkittää kirjastojen opengl32.lib ja glu32.lib kanssa. Katso kääntäjäsi ohjeista kuinka tämä tehdään. Esim. Dev-Cpp:lla tämä tehdään kirjoittamalla projektin asetuksista löytyvään linker-kenttään "-lopengl32 -lglu32". Myöhemmin tulet tarvitsemaan vielä tiedostoa glext.h . Kyseinen tiedosto päivittyy vähän väliä, joten imuroi itsellesi uusin versio osoitteesta: http://oss.sgi.com/projects/ogl-sample/A...
OpenGL:ää käyttävän C/C++-kielisen tiedoston alku näyttäisi siis tyypillisesti tältä:
#include <windows.h> #include <GL/gl.h> #include <GL/glu.h> #include <GL/glext.h>
3. Ikkunan luominen #
Ennen kuin voit piirtää yhtään mitään on sinun luotava ikkuna. Ikkunalla ei varsinaisesti ole mitään tekemistä OpenGL:n kanssa. Se on vain välttämätön paha, joka on tehtävä ennen kuin pääsemme asiaan. Vaikka OpenGL onkin käyttöjärjestelmäriippumaton on ikkunan luominen jokaisella käyttöjärjestelmällä aina erilainen prosessi. Tämä artikkeli käsittelee Windows-ohjelmointia, joten näytän kuinka ikkuna luodaan Windowssissa. Ikkunan luonnissa on periaatteessa 3 eri vaihetta:
1. Rekisteröi ikkunaluokka
2. Luo ikkuna
3. Luo ikkunaan renderöintikonteksti
Ensimmäinen vaihe on helppo. Täytetään WNDCLASS-tyyppinen rakenne ja rekisteröidään se RegisterClass()-funktiolla. Kummatkin on määritelty windows.h:ssa. Sinun ei siis itse tarvitse toteuttaa kumpaakaan!
WNDCLASS-rakenteen määrittely näyttää tältä:
typedef struct _WNDCLASS
{
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;
ja RegisterClass()-funktion prototyyppi tältä:
ATOM RegisterClass( CONST WNDCLASS *lpWndClass // address of structure with class data );
Luodaan nyt yksi WNDCLASS-rakenne, täytetään se asianmukaisesti ja rekisteröidään se.
WNDCLASS wc; memset(&wc, 0, sizeof(WNDCLASS)); wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.hCursor= LoadCursor(NULL, IDC_ARROW); wc.lpfnWndProc = (WNDPROC) WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = "OpenGLtutoriaali"; if (!RegisterClass(&wc)) return 0;
Huomattavaa tässä olivat kentät lpfnWndProc ja lpszClassName. lpszClassName:en pitää keksiä jokin uniikki nimi tälle ikkunaluokalle. Tätä nimeä tarvitaan myöhemmin. Jokainen ikkuna tarvitsee toimiakseen viestinkäsittelijäfunktion. lpfnWndProc sisältää tämän funktion. Palaamme viestinkäsittelijöihin myöhemmin. Loput kentät sisältävät erilaisia asetuksia kuten käytettävän hiiren kursorin jne. Anna niiden olla sellaisena kuin ne ovat yllä, sillä niiden muuttaminen saattaa tehdä ikkunasta OpenGL-yhteensopimattoman.
Seuraava vaihe on vähintään yhtä helppo. Luodaan ikkuna CreateWindowEx()-funktiolla. Sen prototyyppi näyttää tältä:
HWND CreateWindowEx( DWORD dwExStyle, // extended window style LPCTSTR lpClassName, // pointer to registered class name LPCTSTR lpWindowName, // pointer to window name DWORD dwStyle, // window style int x, // horizontal position of window int y, // vertical position of window int nWidth, // window width int nHeight, // window height HWND hWndParent, // handle to parent or owner window HMENU hMenu, // handle to menu, or child-window identifier HINSTANCE hInstance, // handle to application instance LPVOID lpParam // pointer to window-creation data );
Luodaan nyt ikkuna käyttäen kyseistä funktiota.
HWND hwnd; hwnd=CreateWindowEx(WS_EX_APPWINDOW, "OpenGLtutoriaali", " OpenGL:n perusteet - Osa 1: Ikkunan luominen", WS_CLIPSIBLINGS |WS_CLIPCHILDREN| WS_OVERLAPPEDWINDOW, 0, 0, 800, 600, NULL, NULL, GetModuleHandle(NULL), NULL);
Jälleen ainoat huomionarvoiset parametrit ovat lpClassName, johon siis pitää antaa äsken rekisteröimämme ikkunaluokan nimi, lpWindowName, joka sisältää ikkunan otsikkorivillä näkyvän tekstin ja nWidth ja nHeight, jotka sisältävät ikkunan koon. x ja y ovat ikkunan sijainti suhteessa näytön vasempaan ylänurkkaan. Funktio palauttaa kahvan luotuun ikkunaan. Ota se talteen, sillä sitä tarvitaan myöhemmin. Loput parametrit ovat jälleen erilaisia asetuksia, jotka vaikuttavat ikkunan ulkonäköön ja käyttäytymiseen. Jos esim. haluat, että ikkunan kokoa ei voi muuttaa, lisää dwStyle-parametrin perään vielä liput "& ~WS_MAXIMIZEBOX & ~WS_SIZEBOX" ja jos haluat, että ikkunalla ei ole reunoja eikä otsikkoriviä vaihda dwStyle-parametrin WS_OVERLAPPEDWINDOW-lippu WS_POPUP-lippuun. Erilaisia asetuksia on siis tuhottomasti.
Width ja Height parametrien ilmaisema ikkunan koko on siis koko ikkunan koko. Osa ikkunasta kuitenkin jää otsikkopalkin ja reunojen alle, joten haluamme ehkä mieluummin määrittää sen alueen koon, jolle voi piirtää, eli ns. asiakasalueen koon. Tähän voimme käyttää apuna AdjustWindowRectEx()-funktiota, joka laskee koko ikkunan koon asiakasalueen koosta. Se käyttää apunaan RECT-rakennetta. Prototyypit ovat tämän näköiset:
BOOL AdjustWindowRectEx(
LPRECT lpRect, // pointer to client-rectangle structure
DWORD dwStyle, // window styles
BOOL bMenu, // menu-present flag
DWORD dwExStyle // extended style
);
typedef struct _RECT { // rc
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
RECT-rakenne sisältää ikkunan vasemman ylänurkan kooridinaatit (left ja top) ja oikean alanurkan koordinaatit (right ja bottom). Se pitää esitäyttää asiakasalueen koolla. Tämän jälkeen kutsumme AdjustWindowRectEx()-funktiota, joka muuttaa RECT-rakenteen vastaamaan koko ikkunan kokoa. Seuraavassa paranneltu ikkunan luonti, joka vielä keskittää ikkunan utelemalla näytön resoluution GetSystemMetrics()-funktiolla. (Luvut 800 ja 600 ovat esimerkki tyypillisestä ikkunan koosta)
RECT r;
r.left=GetSystemMetrics(SM_CXSCREEN)/2-800/2;
r.top=GetSystemMetrics(SM_CYSCREEN)/2-600/2;
r.right=r.left+800;
r.bottom=r.top+600;
AdjustWindowRectEx(&r,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW,
FALSE, WS_EX_APPWINDOW);
HWND hwnd;
hwnd=CreateWindowEx(WS_EX_APPWINDOW,
"OpenGLtutoriaali", " OpenGL:n perusteet - Osa 1: Ikkunan luominen",
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW,
r.left, r.top, r.right-r.left, r.bottom-r.top,
NULL, NULL, GetModuleHandle(NULL), NULL);
Viimeinen vaihe on hankalin. Meidän pitää luoda ikkunaan renderöintikonteksti. Tässäkin on kolme vaihetta: ensin tehdään laitekonteksti, sitten valitaan pikseliformaatti ja lopuksi vasta luodaan renderöintikonteksti.
Laitekonteksti luodaan GetDC()-funktiolla, jolle annetaan parametrinä ikkunan kahva.
HDC hdc; hdc=GetDC(hwnd); if (!hdc) return 0;
Luodaksemme pikseliformaatin meidän täytyy täyttää PIXELFORMATDESCRIPTOR-rakenne. Sen määrittely näyttää tältä:
typedef struct tagPIXELFORMATDESCRIPTOR { // pfd
WORD nSize;
WORD nVersion;
DWORD dwFlags;
BYTE iPixelType;
BYTE cColorBits;
BYTE cRedBits;
BYTE cRedShift;
BYTE cGreenBits;
BYTE cGreenShift;
BYTE cBlueBits;
BYTE cBlueShift;
BYTE cAlphaBits;
BYTE cAlphaShift;
BYTE cAccumBits;
BYTE cAccumRedBits;
BYTE cAccumGreenBits;
BYTE cAccumBlueBits;
BYTE cAccumAlphaBits;
BYTE cDepthBits;
BYTE cStencilBits;
BYTE cAuxBuffers;
BYTE iLayerType;
BYTE bReserved;
DWORD dwLayerMask;
DWORD dwVisibleMask;
DWORD dwDamageMask;
} PIXELFORMATDESCRIPTOR;
Täytetään nyt kyseinen rakenne asianmukaisesti, jonka jälkeen valitsemme ja asetamme pikseliformaatin funktioilla ChoosePixelFormat() ja SetPixelFormat(). Ideana on, että täytämme PIXELFORMATDESCRIPTOR-rakenteeseen tiedot siitä millaisen pikseliformaatin haluamme. Valitsemme sitten lähimmän vastaavan ChoosePixelFormat()-funktiolla ja asetamme sen palauttaman pikseliformaatin varsinaiseksi pikseliformaatiksi.
PIXELFORMATDESCRIPTOR pfd; memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR)); pfd.nSize=sizeof(PIXELFORMATDESCRIPTOR); pfd.nVersion=1; pfd.dwFlags=PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER; pfd.iPixelType=PFD_TYPE_RGBA; pfd.cRedBits=8; pfd.cGreenBits=8; pfd.cBlueBits=8; pfd.cAlphaBits=8; pfd.cStencilBits=8; pfd.cDepthBits=16; pfd.iLayerType=PFD_MAIN_PLANE; int pixelFormat; pixelFormat=ChoosePixelFormat(hdc, &pfd); if (!pixelFormat) return 0; if (!SetPixelFormat(hdc, pixelFormat, &pfd)) return 0;
Lopuksi luomme renderöintikontekstin. Tämä tehdään ns. wigle-funktioilla wglCreateContext() ja wglMakeCurrent().
HGLRC hrc; hrc=wglCreateContext(hdc); if (!hrc) return 0; if (!wglMakeCurrent(hdc, hrc)) return 0;
Lopuksi ikkuna tehdään vielä näkyväksi ShowWindow()-funktiolla ja tuodaan etualalle funktioilla SetForegroundWindow() ja SetFocus().
ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); SetFocus(hwnd);
4. Viestinkäsittelijä #
Koska Windowsissa pyörii useampi ohjelma yhtä aikaa täytyy niillä olla jokin tapa kommunikoida toistensa ja Windowssin kanssa. Tämä tapa on ns. viestinkäsittelijäfunktio, jollainen jokaisella ikkunalla täytyy olla. Aina kun Windowssilla on jokin viesti jollekkin ohjelmalle se kutsuu kyseisen ohjelman viestinkäsittelijää. Sinun on siis itse toteutettava ohjelmallesi viestinkäsittelyfunktio ja sen prototyypin on aina näytettävä seuraavalta:
LRESULT CALLBACK WindowProc( HWND hwnd, // handle of window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter );
Funktiolla on siis neljä parametria, joista meitä kiinnostaa erityisesti uMsg. Se sisältää itse viestin. Sillä on tuhottomasti erilaisia mahdollisia arvoja esim: WM_ACTIVE, WM_CLOSE ja WM_PAINT. Sinun ei kuitenkaan tarvitse reagoida niihin kaikkiin vaan ainoastaan niihin joihin haluat. Ainut "pakollinen" käsiteltävä on WM_CLOSE, johon reagoidaan kutsumalla PostQuitMessage()-funktiota parametrillä 0. Ikkuna nimittäin saa kyseisen viestin, kun käyttäjä yrittää sulkea sen esim. painamalla sulkemispainiketta. Muita tärkeitä viestejä ovat WM_SIZE, jonka ohjelma saa aina kun sen ikkunan kokoa muutetaan ja WM_PAINT, jonka ohjelma saa aina kun ikkuna täytyy piirtää uudestaan. Näin voi käydä esim. kun ikkunan edessä ollut toinen ikkuna on siirretty syrjään. WM_PAINT-viestin käsittely alkaa aina BeginPaint()-funktiolla ja loppuu EndPaint()-funktioon. Jos käsittelet jonkin viestin palauta 0 ja jos taas et, lähetä se DefWindowProc()-funktiolle ja palauta sen palauttama arvo. Viestin käsittely lienee helpointa toteuttaa switch-rakenteella. Seuraavassa yksinkertainen esimerkkitoteutus:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
// Ikkuna yritetään sulkea kutsu PostQuitMessage()-funktiota.
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}
}
// Viestiä ei käsitelty kutsu DefWindowProc()-funktiota.
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
5. Pääfunktio #
DOS:issa ja Linuxsissa pääfunktio on yleensä muotoa:
int main(int argc, char* argv[]);
Windowsissa asia on hieman monimutkaisempi. Pääfunktio on muotoa:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow);
Paljon mutkikkaita parametrejä, mutta eipä hätää, niistä ei tarvitse välittää. Lisäksi pääfunktiosta täytyy löytyä standardi viestinkäsittelysilmukka:
MSG msg;
while(1)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// Lisää oma suoritettava ohjelmakoodisi tähän
// Tyypillisesti tässä välissä suoritetaan
// pelilogiikkaa ja renderöidään yksi "frame".
}
}
Eli kyseessä on loputon silmukka, joka purkaa jonosta Windowssin ohjelmalle lähettämiä viestejä ja käsittelee ne. Ainoastaan silloin, kun ei ole yhtään viestiä käsiteltävänä voidaan suorittaa ohjelman omaa koodia. Silmukasta saa poistua vasta kun ohjelma saa sulkemisviestin WM_QUIT. Jonka jälkeen ohjelman tulee sammua.
6. Grafiikan piirtäminen #
Kun ikkuna on luotu päästään itse asiaan eli grafiikan piirtoon. Itseasiassa varsinainen OpenGL osio alkaa vasta tästä.
Ensin on määriteltävä viewport eli se alue ikkunasta, jolle piirretään. Tämä tapahtuu glViewport()-funktiolla. Sen prototyyppi näyttää tältä:
void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
x ja y ovat viewportin vasemman alareunan koordinaatit suhteessa ikkunan vasemman alareunan koordinaatteihin ja width ja height viewportin leveys ja korkeus (siis pikseleissä ilmaistuna). Esim. jos meillä on 800x600 ikkuna ja haluamme koko sen alan piirtämistä varten käyttäisimme kutsua glViewport(0, 0, 800, 600);. Viewport voi myös periaatteessa mennä ikkunan reunojen yli, jolloin ikkunan ulkopuolisia alueita ei yksinkertaisesti piirretä, mutta bugisimmilla näytönohjaimenajureilla tämä saattaa aiheuttaa ongelmia.
Kun viewport on määritelty täytyy vielä määritellä koordinaatisto (tai pidemminkin projektiomatriisi, mutta siitä enemmän joskus toiste). OpenGL:ssä on kaksi erilaista koordinaatistoa: 2D-koordinaatisto 2D-grafiikkaa varten ja 3D-koordinaatisto 3D-grafiikkaa varten. Käytämme tässä esimerkissä hieman helpompaa 2D-koordinaatistoa. Se luodaan funktiolla gluOrtho2D(), jonka prototyyppi näyttää tältä:
void gluOrtho2D(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top);
OpenGL:ssä X-akseli kulkee vaakasuorassa ja Y-akseli pystysuorassa. left-parametri kertoo X-akselin arvon viewportin vasemmassa reunassa ja right X-akselin arvon viewportin oikeassa reunassa. Vastaavasti bottom ja top Y-akselille. Esim. jos haluaisimme määritellä koordinaatiston niin, että origo olisi viewportin keskellä ja sekä Y- että X-akseli ulottuisivat 10 yksikköä joka suuntaan käyttäisimme kutsua gluOrtho2D(-10, 10, -10, 10);. Jos taas meillä olisi kokoa 800x600 oleva viewport ja haluaisimme koordinaatiston, jossa origo on vasemmassa alanurkassa ja yksi pikseli vastaa aina yhtä yksikköä käyttäisimme kutsua gluOrtho2D(0, 800, 0, 600);.
Vihdoin ja viimein pääsemme piirtämään itse grafiikkaa. OpenGL:ssä on monta tapaa piirtää, mutta helpoin niistä on varmasti glBegin() glEnd() parin, eli ns. välitysmoodin käyttäminen. Ensin kutsutaan glBegin()-funktiota, jonka prototyyppi näyttää tältä:
void glBegin(GLenum mode);
Sillä on siis vain yksi parametri mode, joka kertoo mitä piirretään. Sen mahdolliset arvot ovat: GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_QUADS, GL_QUAD_STRIP ja GL_POLYGON. Näistä tärkeimmät ovat GL_POINTS, joka piirtää pisteitä, GL_LINES, joka piirtää viivoja ja GL_TRIANGLES, joka piirtää kolmioita.
glBegin()-kutsun jälkeen kutsumme glVertex2f()-funktiota toistuvasti. Sille annetaan piirrettävän primitiivin kontrollipisteen koordinaatit. Sen prototyyppi näyttää tältä.
void glVertex2f(GLfloat x, GLfloat y);
Eli X ja Y koordinaatit annetaan parametrina. Se missä kohtaa ikkunaa nämä pisteet sijaitsevat määräyttyy luodun koordinaatiston ja viewportin mukaan.
GL_POINTS tapauksessa jokaiseen glVertex2f()-funktion määräämiin koordinaatteihin piirretään piste. GL_LINES tapauksessa aina kahden glVertex2f()-kutsun määräämien koordinaattien välille piirretään viiva ja GL_TRIANGLES tapauksessa aina kolmen kutsun välille kolmio. Käytimme tässä siis glVertex2f()-funktiota koska meillä on 2D koordinaatisto. Myöhemmin 3D-koordinaatistossa käytämme glVertex3f()-funktiota.
Lopuksi kun kaikki tarvittavat glVertex2f() kutsut on tehty kutsutaan glEnd()-funktiota. Se ei ota yhtään parametriä.
Seuraava esimerkki piirtää viivan pisteestä (0, 0) pisteeseen (200, 100).
glBegin(GL_LINES); glVertex2f(0, 0); glVertex2f(200, 100); glEnd();
Piirrettävälle primitiiville voidaan myös asettaa väri glColor3f()-funktiolla. Prototyyppi näyttää tältä:
void glColor3f(GLfloat red, GLfloat green, GLfloat blue);.
Se ottaa parametrinaan värin red, green ja blue arvot. Nämä arvot ovat väliltä 0-1 ja niitä sekoittamalla voidaan muodostaa kaikki mahdolliset värit. Väri voidaan asettaa halutessa jokaiselle primitiivin kontrollipisteelle erikseen. Jos primitiivin kontrollipisteet ovat eri väriset niiden väliin jäävien pikselien värit interpoloidaan. Eli jos viivan toinen pää on musta ja toinen valkea, on viiva keskeltä harmaa. Seuraava esimerkki piirtää kolmion, jonka yksi nurkka on punainen, yksi sininen ja yksi vihreä.
glBegin(GL_TRIANGLES); glColor3f(1, 0, 0); glVertex2f(0, 5); glColor3f(0, 1, 0); glVertex2f(-5, -5); glColor3f(0, 0, 1); glVertex2f(5, -5); glEnd();
Vielä pari juttua ennen kuin kaikki on täydellistä. Nimittäin ennen piirtoa ikkuna on tyhjennettävä kaikesta mahdollisesta muusta grafiikasta, tämä tapahtuu glClear()-funktiolla, jolle annetaan parametriksi GL_COLOR_BUFFER_BIT.
Se toinen juttu on sitten niin sanottu kaksoispuskurointi. OpenGL:ssä on kaksi piirtopintaa (tosin vain silloin kun pikseliformaaatti luotiin PFD_DOUBLEBUFFER parametrillä), joista toinen on aina näyttövuorossa ja toinen aina piirtovuorossa. Niinpä aina kun piirrät jotain se ei ilmesty näytölle vaan sille toiselle piilossa olevalle pinnalle. Jotta grafiikka saataisiin näkyväksi täytyy nämä pinnat piirtämisen jälkeen vaihtaa keskenään SwapBuffers()-funktiolla, jonka prototyyppi näyttää tältä:
BOOL SwapBuffers(
HDC hdc //Device context whose buffers get swapped
);
Eli se ottaa parametrinään ikkunan luonnin yhteydessä saadun laitekontekstin. Kahta piirtopintaa käytetään, jotta käyttäjä ei näe kuvan valmistumista piste kerrallaan (joskin hyvin nopeasti), vaan näkee sen vasta kun koko kuva on valmis.
7. Esimerkkiohjelma #
Lopuksi täydellinen esimerkkiohjelma, joka luo ikkunan ja piirtää sen keskelle kolmion. Huomaa kuinka itse ikkunan luominen vie suurimman osan koodista, kun taas varsinainen grafiikan piirto vie vain muutaman rivin. Tämän takia Internet on pullollaan erilaisia "kehys"-kirjastoja, joilla voit luoda ikkunan nopeasti parilla funktion kutsulla. Näistä kuuluisimpia ovat (nyt jo hieman vanhentunut) GLUT ( http://www.xmission.com/~nate/glut.html ) ja GLFW ( http://glfw.sourceforge.net/ ). Myös erittäin suoritulla SDL-kirjastolla ( http://www.libsdl.org ) voi luoda OpenGL-yhteensopivan ikkunan.
Vielä mainittakoon, että jos haluat saada käyttäjältä syötettä, niin näppäimen tila voidaan lukea Windowsissa GetAsyncKeyState()-funktiolla ja hiiren tila GetCursorPos()-funktiolla. Lisää tietoa näiden käytöstä löytyy msdn:stä ( http://msdn.microsoft.com/ ). Jos käytät GLUT:a, GLFW:tä tai SDL:ää, niin näistä kyllä löytyy taas omat funktionsa näppäinten lukuun.
Voit imuroida oheisen lähdekoodin ja valmiiksi käännetyn version tästä: http://www.suomipelit.com/files/artikkel... .
#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <math.h>
//#include <gl\glext.h> // Ei tarvita tässä ohjelmassa
// Määrittele laitekonteksti globaaliksi sitä nimittäin tarvitaan myös pääfunktiossa.
HDC hdc;
// Viestinkäsittelijä
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
// Koska piirrämme ikkunan sisällön pääsilmukassa jatkuvasti uudelleen
// reakoimme WM_PAINT-viestiin vain tyhjentämällä ikkunan mustaksi.
case WM_PAINT:
{
PAINTSTRUCT p;
BeginPaint(hwnd, &p);
glClear(GL_COLOR_BUFFER_BIT);
SwapBuffers(hdc);
EndPaint(hwnd, &p);
return 0;
}
// Ikkuna yritetään sulkea kutsu PostQuitMessage()-funktiota.
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}
// Käsittele myös WM_SIZE se lähetetään ikkunalle aina kun sen kokoa muutetaan.
// Tämä on oiva tilaisuus muuttaa viewport
// oikean kokoiseksi peittämään koko ikkuna.
case WM_SIZE:
{
// Ikkunan uusi koko saadaan lParam parametrista LOWORD ja HIWORD makroilla.
glViewport(0, 0, LOWORD(lParam), HIWORD(lParam));
return 0;
}
}
// Viestiä ei käsitelty kutsu DefWindowProc()-funktiota.
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int luoIkkuna(unsigned int leveys, unsigned int korkeus,
char *otsikko)
{
// Rekisteröi ikkunaluokka
WNDCLASS wc;
memset(&wc, 0, sizeof(WNDCLASS));
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.hCursor= LoadCursor(NULL, IDC_ARROW);
wc.lpfnWndProc = (WNDPROC) WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = "OpenGLtutoriaali";
if (!RegisterClass(&wc)) return 0;
// Luo ikkuna
RECT r;
r.left=GetSystemMetrics(SM_CXSCREEN)/2-leveys/2;
r.top=GetSystemMetrics(SM_CYSCREEN)/2-korkeus/2;
r.right=r.left+leveys;
r.bottom=r.top+korkeus;
AdjustWindowRectEx(&r,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW,
FALSE, WS_EX_APPWINDOW);
HWND hwnd;
hwnd=CreateWindowEx(WS_EX_APPWINDOW,
"OpenGLtutoriaali", otsikko,
WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_OVERLAPPEDWINDOW,
r.left, r.top, r.right-r.left, r.bottom-r.top,
NULL, NULL, GetModuleHandle(NULL), NULL);
// Luo laitekonteksti
hdc=GetDC(hwnd);
if (!hdc) return 0;
// Valitse pikseliformaatti
PIXELFORMATDESCRIPTOR pfd;
memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd.nSize=sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion=1;
pfd.dwFlags=PFD_DRAW_TO_WINDOW|PFD_SUPPORT_OPENGL|PFD_DOUBLEBUFFER;
pfd.iPixelType=PFD_TYPE_RGBA;
pfd.cRedBits=8;
pfd.cGreenBits=8;
pfd.cBlueBits=8;
pfd.cAlphaBits=8;
pfd.cStencilBits=8;
pfd.cDepthBits=16;
pfd.iLayerType=PFD_MAIN_PLANE;
int pixelFormat;
pixelFormat=ChoosePixelFormat(hdc, &pfd);
if (!pixelFormat) return 0;
if (!SetPixelFormat(hdc, pixelFormat, &pfd)) return 0;
// Luo renderöintikonteksti
HGLRC hrc;
hrc=wglCreateContext(hdc);
if (!hrc) return 0;
if (!wglMakeCurrent(hdc, hrc)) return 0;
// Tuo ikkuna näkyviin
ShowWindow(hwnd, SW_SHOW);
SetForegroundWindow(hwnd);
SetFocus(hwnd);
// Palauta onnistuminen
return 1;
}
// Pääfunktio
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
// Luo ikkuna
if (!luoIkkuna(800, 600, "OpenGL:n perusteet - osa 1: Ikkunan luominen")) return 0;
// Määrittele viewport ja koordinaatisto koko ikkunan kokoiseksi
glViewport(0, 0, 800, 600);
glMatrixMode(GL_PROJECTION);
gluOrtho2D(-13, 13, -10, 10);
// Viestinkäsittelysilmukka
MSG msg;
while(1)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// Tyhjennä puskuri
glClear(GL_COLOR_BUFFER_BIT);
// Piirrä kolmio
glBegin(GL_TRIANGLES);
glColor3f(1, 0, 0);
glVertex2f(0, 5);
glColor3f(0, 1, 0);
glVertex2f(-5, -5);
glColor3f(0, 0, 1);
glVertex2f(5, -5);
glEnd();
// Vaihda puskuri näytölle.
SwapBuffers(hdc);
}
}
return 0;
}
8. Loppusanat #
Tässä artikkelissa opit luomaan OpenGL-yhteensopivan ikkunan Windows-käyttöjärjestelmässä. Huomaa, että jos asiat olisi tehty aivan oikeaoppisesti, niin luotu ikkuna olisi pitänyt vielä tuhota ohjelman sammuessa, mutta luotin tässä sokeasti siihen, että käyttöjärjestelmä vapauttaa automaattisesti tarpeettomat resurssit.
Seuraavassa tutoriaalissa siirrymme varsinaisen 3D-grafiikan piirtoon. Raportoithan kaikki tästä artikkelista löytämäsi virheet (niin kirjoitus-, kuin asiavirheetkin) osoitteeseen markus.ilmola@pp.inet.fi , niin korjaan ne mahdollisimman nopeasti. Myös kaikki kommentit ja kysymykset ovat tervetulleita.

