Artikkelit

Artikkelit olivat Suomipelit.comissa sivuston sulkeuduttua. Näin ollen niihin saattaa sisältyä rikkinäisiä linkkejä tai epämääräisiä viitteitä asioihin, joita ei enää ole olemassa, ja joskus jokin saattaa näkyä väärin.

Palaa artikkelilistaan

OpenGL:n perusteet - Osa 2: 3D grafiikka

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 toinen osa.

9.6.2004 julkaistun artikkelin on kirjoittanut markus.

  1. 1 3-ulotteisen kappaleen esittäminen tietokoneella
  2. 2 3D-koordinaatisto
  3. 3 Renderöinti
  4. 4 Näkymättömien pintojen poisto
  5. 5 Päällekkäisyysongelma
  6. 6 Matriisit
  7. 7 Esimerkkiohjelma
  8. 8 Loppusanat

1 3-ulotteisen kappaleen esittäminen tietokoneella #

Tapoja kuvata 3-ulotteinen kappale tietokoneella on monta, mutta se tapa, jota lähes kaikki pelit käyttävät, on aproksimoida eli arvioida kappaletta monitahokkaalla (englanniksi polyhedron). Monitahokas on 3-ulotteinen kappale, jota rajoittaa monikulmioista eli polygoneista koostuva suljettu pinta. Nämä polygonit ovat monitahokkaan tahoja. Tahkojen leikkausviivoja kutsutaan särmiksi ja särmien leikkauspisteitä kärjiksi eli vertekseiksi. Alla oleva kuva esittää monitahokasta, jossa verteksit on merkitty sinisellä, särmät punaisella ja polygonit harmaalla.


Monitahokkaat voidaan jakaa kahteen ryhmään: kuperiin ja koveriin. Monitahokas on kupera, jos mitkä tahansa kaksi sen vertekseistä voidaan yhdistää viivalla niin, että tämä viiva ei käy monitahokkaan ulkopuolella. Muussa tapauksessa monitahokas on kovera.

Monitahokkaan särmät ja verteksit muodostavat verkon, josta käytetään yleensä englanninkielistä termiä mesh. Jos monitahokkaasta piirretään pelkät särmät kutsutaan kuvaa rautalankamalliksi (englanniksi wire-frame).

Kun monitahokkaalla halutaan aproksimoida jotain kappaletta rakennetaan monitahokas, jonka pinta mukailee mahdollisimman tarkasti alkuperäisen kappaleen pintaa. Mitä enemmän polygoneja monitahokkaassa on, sitä tarkemmin se jäljittelee alkuperäistä kappaletta. Se ei voi kuitenkaan koskaan mallintaa kappaletta täydellisen tarkasti (ellei sitten alkuperäinen kappale ollut monitahokas itsekkin). Kuitenkin hyvin suurilla polygonimäärillä ihmissilmä ei enää huomaa eroa.

Monitahokkaan tallentaminen tietokoneen muistiin on helppoa. Meidän pitää vain tallentaa jokaisen verteksin koordinaatit, sekä tieto siitä mitkä verteksit aina muodostavat tahon. Tämä voidaan toteuttaa vaikka indeksoimalla verteksejä. Vaikka monitahokkaan tahot voivat olla mitä tahansa monikulmioita, voidaan mikä tahansa monikulmio muodostaa kolmioista. Tämän takia riittää, että voimme tallentaa ainoastaan kolmioita.

Esim. Kuutio, jonka keskipiste sijaitsee origossa ja ”säde” on yksi, voitaisiin tallentaa seuraavasti:
float vertex[8][3]={
{-1,-1,-1},{1,-1,-1},{-1,1,-1},{1,1,-1},
{-1,-1,1}, {1,-1, 1},{-1,1, 1},{1,1, 1}};
int index[6*4]={ 0,2,3,1,  4,5,7,6,  5,1,3,7,  4,6,2,0,  7,3,2,6,  4,0,1,5 };	

Eli tallennetaan kuution jokaisen 8 verteksin sijainti, sekä indeksit, jotka ilmoittavat mitkä 4 verteksiä aina muodostavat tahon.

2 3D-koordinaatisto #

Tämän artikkelisarjan ensimmäisessä osassa loimme 2D-koordinaatiston gluOrtho2D()-funktiolla. 3D-grafiikan tapauksessa tarvitsemme 3D-koordinaatiston. Tälläinen koordinaatisto luodaan OpenGL:ssä funktiolla gluPerspeksive(), jonka prototyyppi näyttää tältä:

void gluPerspective(
    GLdouble fovy, 	
    GLdouble aspect, 	
    GLdouble zNear, 	
    GLdouble zFar	
   );

Koordinaatisto on helpoin käsittää jos ajattelet, että jossain päin kyberavaruutta kelluu kamera. Origo sijaitsee tämän kameran linssin keskellä. X-akseli kulkee vaakatasossa ja sen arvot kasvavat oikealle mentäessä. Y-akseli kulkee pystysuuntaan ja sen arvot kasvavat ylöspäin mentäessä. Kamera katsoo kohti negatiivista Z-akselia. Kameralle on myös määritelty näkökentän leveys (englanniksi field of view eli FOV), joka määrää kuinka monen asteen levyisen kaistaleen kamera näkee. Näin ollen kameran kerralla näkemä alue muodostaa kartion. Koska kappaleita, jotka ovat hyvin kaukana kamerasta tai hyvin lähellä sitä, ei ole järkevää piirtää, rajoitetaan kameran katselukartiota vielä kahdella tasolla ns. lähi- ja kaukoleikkaustasolla. Kaikki kaukotason takana olevat kappaleet jätetään piirtämättä samoin kaikki lähitason edessä olevat. Näin ollen näkyväksi alueeksi jää enää katkaistu kartio eli frustum.

gluPerspective()-funktion fovy-parametri kertoo kameran näkökentän leveyden. Tämän arvon on oltava suurempi kuin 0 ja pienempi kuin 180. Suurilla arvoilla saadaan laajakulmanäkymä ja pienillä arvoilla putkinäkömäinen efekti. Vaikka ihmissilmän näkökentän leveys on lähes 180 astetta astetta (tarkan näön alue noin 30 astetta) peittää monitori vain pienen osan näkökentästä, joten realistinen arvo on noin 45-60 astetta. Near ja far-parametrit kertovat lähi- ja kaukotason etäisyydet kamerasta. Huomaa, että lähitäson etäisyys pitää olla suurempi kuin 0 ja kaukotason on aina oltava kauempana kuin lähitason. Lähitaso kannattaa pitää mahdollisimman kaukana kamerasta ja kaukotaso mahdollisimman lähellä kameraa, jotta näkyvä alue pysyy optimaalisen pienenä. Aspect-parametri kertoo kuvasuhteen. Sen on oltava viewportin_leveys/viewportin_korkeus tai muuten kuva vääristyy.

gluOrtho2D() ja gluPerspektive()-funktioista on olemassa hieman monipuolisemmat versiot glOrtho() ja glFrustum(), mutta en käsittele niitä tässä artikkelissa.

3 Renderöinti #

OpenGL:ssä monitahokas piirretään yksinkertaisesti polygoni kerrallaan. Piirtäminen aloitetaan glBegin()-funktiolla, jolle annetaan parametriksi GL_TRIANGLES, jos halutaan piirtää kolmioita tai GL_QUADS, jos halutaan piirtää nelikulmioita. Tämän jälkeen annetaan verteksien sijainnit glVertex3f()-funktiolla. Jos glBegin()-funktion parametri oli GL_TRIANGLES, piirretään jokaisen kolmen annetun verteksin välille aina kolmio. Tai jos se oli GL_QUADS piirretään jokaisen neljän verteksin välille aina nelikulmio. Lopuksi kutsutaan glEnd()-funktioita. Jokaiselle verteksille voidaan myös määrätä väri kutsumalla glColor3f()-funktiota ennen jokaista glVertex3f()-funktion kutsua. Esim. kappaleessa 1 määritelty kuutio voitaisiin piirtää esimerkiksi seuraavalla tavalla:

glBegin(GL_QUADS);
int i;
for (i=0; i<6*4; i++)
{
  glVertex3f(vertex[index[i]][0], vertex[index[i]][1], vertex[index[i]][2]);
}
glEnd();

Eli kutsutaan glVertex3f()-funktiota neljä kertaa jokaista kuution kuutta tahoa kohti.

Kun polygonien määrä kasvaa suureksi käy glBegin()-,lEnd()-parin käyttäminen tehottomaksi, sillä jokaista kolmiota kohden tarvitaan aina kolme glVertex3f()-funktion kutsua. Esim. jos meillä olisi 10000 kolmekulmaista polygonia tarvittaisiin 30000 glVertex3f()-funktion kutsua. Ei hyvä! Tämän takia OpenGL:ssä on kolme muutakin tapaa piirtää polygoneja. Ne ovat ”display list”, ”vertex array” ja ”vertex buffer object”.

3.1 Display listit

Display list:in ideana on ”nauhoittaa” funktion kutsuja, jonka jälkeen kaikki nauhoitetut kutsut voidaan ”toistaa” yhdellä funktion kutsulla. Ensin display list pitää luoda glGenLists()-funktiolla. Funktio palauttaa luodun display list:n tunnuksen. Nauhoitus aloitetaan glNewList()-funktiolla, jolle annetaan parametrina glGenLists()-funktiolta saatu tunnus ja symboli GL_COMPILE. Tämän jälkeen voidaan kutsua vapaasti lähes mitä tahansa OpenGL:n funktioita ja ne nauhoittuvat. Kun display list on valmis kutsutaan glEndList()-funktioita. Myöhemmin nauhoitus voidaan toistaa kuinka monta kertaa tahansa glCallList()-funktiolla, jolle annetaan parametrina display list:in tunnus. Huomaa, että display list:in sisältöä ei voi muuttaa. Display list voidaan tuhota glDeleteList()-funktiolla. Esim. äsken piirretty kuutio voitaisiin nauhoittaa display list:iin seuraavasti:
int tunnus;
tunnus=glGenLists(1);
glNewList(tunnus, GL_COMPILE);
glBegin(GL_QUADS);
int i;
for (i=0; i<6*4; i++)
{
  glVertex3f(vertex[index[i]][0], vertex[index[i]][1], vertex[index[i]][2]);
}
glEnd();
glEndList();

Vastaavasti se voitaisiin tämän jälkeen piirtää koska tahansa kutsulla glCallList(tunnus);.

3.2 Vertex array

Vertex array:n ideana on antaa OpenGL:lle osoitin verteksidataan, jonka jälkeen kaikki polygonit voidaan piirtää yhdellä funktion kutsulla. Ennen kuin vertex array:ta voidaan käyttää pitää se laittaa päälle glEnableClientState()-funktiolla, jolle annetaan parametrina GL_VERTEX_ARRAY. Tämän jälkeen annetaan osoitin verteksidataan glVertexPointer()-funktiolla, jonka prototyyppi näyttää tältä:
void glVertexPointer(
    GLint size, 	
    GLenum type, 	
    GLsizei stride, 	
    const GLvoid *pointer 	
   );

Ensimmäinen parametri kertoo komponenttien määrän per verteksi (yleensä 3). Seuraava datan tyypin (yleensä GL_FLOAT). Kolmas verteksien etäisyyden toisistaan muistissa (yleensä sizeof(verteksi)). Ja viimeinen on osoitin verteksidataan. Kun osoitin on annettu voidaan varsinainen renderöinti tehdä yhdellä glDrawElements()-funktion kutsulla. Sen prototyyppi näyttää tältä:
void glDrawElements(
    GLenum mode,	 
    GLsizei count,	 
    GLenum type,	 
    const GLvoid *indices	 
   );

Ensimmäisellä parametrilla on sama merkitys kuin glBegin()-funktionkin parametrilla. Viimeinen on osoitin taulukkoon, joka indeksoi glVertexPointer()-funktiolla annettuja verteksejä. Toinen parametri kertoo alkioiden määrän tässä taulukossa. Toiseksi viimeinen kertoo tämän taulukon alkioden tyypin (yleensä GL_UNSIGNED_INT). Seuraava esimerkki piirtää kappaleessa yksi määritellyn kuution käyttäen vertex array:ta.
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(float[3]), vertex);
glDrawElements(GL_QUADS, 6*4, GL_UNSIGNED_INT, index);


3.3 Vertex buffer object

Se viimeinen tapa on sitten vertex buffer object eli VBO. Se on muuten sama kuin vertex array, mutta siinä verteksidata siirretään keskusmuistista nopeampaan näytönohjaimen muistiin. Koska VBO:n edut tulevat esiin vasta valtavan suurilla polygonimäärillä ja tämä on vain OpenGL:n perusteet-artikkeli, en käsittele sitä.

4 Näkymättömien pintojen poisto #

Koska monitahokkaat ovat määritelmän mukaan suljettuja, emme voi koskaan nähdä tahojen takapuolia, ainoastaan niiden etupuolet. Tämän takia olisi tehokkuuden kannalta järkevää, jos polygoni jätettäisiin piirtämättä silloin kun sen takapuoli on kameraan päin. Nyt ilmestyykin ongelma. Mistä oikein tietää kumpi polygonin puolista on sen takapuoli ja kumpi etupuoli? Eivätkös ne ole ihan samanlaisia kummatkin? OpenGL:ssä polygonin etupuoli määritellään niin, että jos numeroimme sen verteksit järjestyksessä, on etupuoli se, jolta katsottuna verteksien numerot kasvavat vastapäivään.


Näkymättömien pintojen poisto kytketään päälle glEnable()-functiolla, jolle annetaan parametrina GL_CULL_FACE. Vastaavasti sen saa pois päältä glDisable()-funktiolla samalla parametrilla. Voit myös halutessasi päättää jätetäänkö piirtämättä taka-, vai etupuolet. Tämä tehdään glCullFace()-funktiolla, jolle annetaan parametrina, joko GL_FRONT tai GL_BACK. Oletusarvo on GL_BACK. Sinun ei tietenkään tarvitse itse numeroida verteksejä, vaan OpenGL numeroi ne siinä järjestyksessä, kun se saa ne esim. glVertex3f()-funktiolta.

5 Päällekkäisyysongelma #

Aina kun piirrät OpenGL:ssä jotain, se peittää alleen kaiken ennen sitä piirretyn. Tästä seuraa se, että jos monitahokkaan takenpana olevat polygonit piirretään etummaisten jälkeen, peittävät ne etummaiset alleen ja saatu kuva on näin ollen virheellinen. Jos näkymättömien pintojen poisto laitetaan päälle, ei tätä ongelmaa esiinny, jos monitahokas on kupera, sillä siinä mitkään kaksi kameraan päin olevaa polygonia ei peitä toisiaan. Sen sijaan koveran monitahokkaan tapauksessa ei pelkkä näkymättömien pintojen poisto auta, vaan tarvitaan avuksi jotain toista algoritmia. Kaksi kuuluisinta ovat: maalarin algoritmi ja syvyyspuskurialgoritmi.

5.1 Maalarin algoritmi.

Maalarin algoritmin idea on yksinkertainen. Polygonit lajitellaan ja piirretään tämän jälkeen järjestyksessä takimmaisesta etummaiseen, samoin kuin maalari tekee maalatessaan taulua. On kuitenkin muutama tapaus, joissa maalarin algoritmi ei toimi, eli kuva on aina virheellinen riippumatta siitä missä järjestyksessä polygonit piirretään. Alla pari esimerkkiä.


Tämän takia maalarin algoritmia ei kannatakkaan käyttää kuin joissakin erikoistapauksissa.

5.2 Syvyyspuskurialgoritmi

Syvyyspuskurialgoritmin ideana on tallentaa pikselin värin lisäksi myös sen etäisyys kamerasta. Tämä syvyysarvo tallennetaan ns. syvyyspuskuriin eli Z-puskuriin. Aina ennen pikselin piirtoa lasketaan sen syvyysarvo. Tarkistetaan puskurista, onko kyseisellä paikalla olevan pikselin syvyysarvo jo pienempi kuin uuden pikselin syvyys. Jos on, niin pikseli jätetään piirtämättä. Jos taas ei, niin pikseli piirretään ja syvyyspuskuriin päivitetään uusi arvo.

OpenGL:ssä on syvyyspuskuri sisäänrakennettuna. Se tarvitsee vain kytkeä päälle glEnable()-funktiolla, jolle annetaan parametrina GL_DEPTH_TEST. Syvyyspuskuri on tietenkin tyhjennettävä ennen piirtämistä. Tämä tehdään glClear()-funktiolla, jolle annetaan parametrina GL_DEPTH_BUFFER_BIT.

6 Matriisit #

Voidaksesi käyttää OpenGL:ää sinun ei tarvitse ymmärtää matriisien syvintä olemusta. Riittää, että tiedät niiden perusperiaatteen. Matriisi on taulukko (OpenGL:n tapauksessa 4x4 taulukko), jonka jokaisessa solussa on jokin mielivaltainen luku. Matriisilla voidaan kertoa vektori, jolloin vektori muuntuu toiseksi vektoriksi. Millaiseksi, se riippuu siitä, mitä lukuja matriisi sisälsi. Valitsemalla matriisin alkiot sopivasti saadaan vektori vaikka pyörimään jonkin akselin ympäri jonkin kulman verran.

Jos matriisin alkiot valitaan niin, että vinorivillä on ykkösiä ja kaikkialla muualla nollia, saadaan ns. yksikkömatriisi (englanniksi identity matrix), jolla kertomalla vektori ei muutu miksikään.

Myös matriiseja voidaan kertoa keskenään, jolloin matriisien ominaisuudet yhdistyvät. Esim. jos meillä on matriisi, joka pyörittää 30 astetta Z-akselin ympäri ja matriisi, joka pyörittää 50 astetta Z-akselin ympäri saadaan niiden tulona matriisi, joka pyörittää 80 astetta Z-akselin ympäri. Jos haluat tarkempia tietoja matriiseista lue artikkeli: Matriisimatematiikkaa peliohjelmoijille
http://www.suomipelit.com/index.php?c=na...


OpenGL:ssä on sisäänrakennettuna useitakin matriiseja. Tässä artikkelissa olemme kiinnostuneita modelview-matriisista ja projektiomatriisista. Aina kun annat OpenGL:lle verteksin glVertex-sarjan funktiolla kerrotaan kyseinen verteksi ensin modelview-matriisilla ja sitten projektiomatriisilla. Kummatkin nämä matriisit ovat oletuksena yksikkömatriiseja eli verteksi ei muutu miksikään.

OpenGL sisältää kasan funktioita, joilla sen sisäisiä matriiseja voidaan muokata. Ennen kuin voimme käyttää niitä meidän on kuitenkin päätettävä mitä matriisia haluamme muokata. Tämä tehdään glMatrixMode()-funktiolla, joka saa parametrikseen, joko GL_MODELVIEW tai GL_PROJECTION riippuen siitä kumpaa matriiseista haluamme muokata. Myös muita vaihtoehtoja on, mutta emme käsittele niitä tässä artikkelissa.

Seuraavassa tärkeimmät matriiseja muokkaavista funktioista:

glLoadIdentity() - korvaa nykyisen matriisin yksikkömatriisilla.

glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z) - kertoo nykyisen matriisin matriisilla, joka pyörittää vektoria angle astetta akselin x, y, z ympäri.

glTranslatef(GLfloat x, GLfloat y, GLfloat z ) - siirtää vektoria parametrien x, y ja z verran.

Seuraava esimerkki siirtää ensin -5 yksikköä z-akselin suuntaan ja pyörittää sitten 60 astetta x-akselin ympäri.

glLoadIdentity();
glTranslatef(0, 0, -5);
glRotatef(60, 1, 0, 0);


Eli jos nyt annat OpenGL:lle yhdenkin verteksin, siirretetään sitä ensin 5 yksikköä negatiivisen z-akselin suuntaan ja pyöritetään sitten 60 astetta x-akselin ympäri ennen piirtämistä.

Myös koordinaatistot tallennetaan matriisiin. Niille on varattu varta vasten projektiomatriisi. gluOrtho2D() ja gluPerspektive() ovatkin itse asiassa matriiseja muokkaavia funktioita, jotka kertovat nykyisen matriisin luomallaan koordinaatistomatriisilla. Tämän takia projektiomatriisi on ensin valittava glMatrixMode()-funktiolla ennen kummankaan kutsua.

Nykyinen matriisi voidaan halutessa tallentaa varastoon pinoon kutsulla glPushMatrix();. Pinon päällimmäinen matriisi taas voidaan ladata takaisin kutsulla glPopMatrix();. Pinoon mahtuu maksimissaan 32 modelview-matriisia ja 2-projektiomatriisia (kummallakin on oma pino).

7 Esimerkkiohjelma #

Seuraava ohjelma avaa ikkunan ja piirtää siihen pyörivän värikkään kuution. Koska se kuinka monta kertaa sekunnissa tietokone piirtää kuvan riippuu sen tehosta, pyörisi kuutio eri nopeudella eri kokeilla. Tämän takia käytämme GetTickCount()-funktiota, joka palauttaa ajan millisekunteina, mittaamaan kuvan piirtämiseen kuluvan ajan. Näin voimme ajastaa pyörimisnopeuden samaksi kaikilla koneilla. 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 <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
    // reagoimme 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)
{
  // Data piirrettävää kuutiota varten
  float vertex[8][3]={{-1,-1,-1},{1,-1,-1},{-1,1,-1},{1,1,-1},
                      {-1,-1,1}, {1,-1, 1},{-1,1, 1},{1,1, 1}};
  float color[8][3]={{0,0,0},{1,0,0},{0,1,0},{1,1,0},
                      {0,0,1}, {1,0, 1},{0,1, 1},{1,1, 1}};
  int index[6*4]={ 0,2,3,1,  4,5,7,6,  5,1,3,7,  4,6,2,0,  7,3,2,6,  4,0,1,5 };
  float angle=0;

  unsigned int aika;
  unsigned int piirtoaika;
  unsigned int alkuaika;

  // Luo ikkuna
  if (!luoIkkuna(800, 600, "OpenGL:n perusteet - Osa 2: 3D grafiikka")) return 0;

  // Määrittele viewport koko ikkunan kokoiseksi
  glViewport(0, 0, 800, 600);

  // Koska koordinaatisto on itseasiassa matriisi täytyy meidän ottaa
  // projektiomatriisi käsiteltäväksi ennen gluPerspective-kutsua.
  glMatrixMode(GL_PROJECTION);
  gluPerspective(60, 800.0/600.0, 1, 100);

  // Kaikki matriisia muuttavat käskyt vaikuttavat tämän jälkeen modelview-matriisiin
  glMatrixMode(GL_MODELVIEW);

  // Laita näkymättömien pintojen poisto ja sysyyspuskurialgoritmi päälle.
  glEnable(GL_CULL_FACE);
  glEnable(GL_DEPTH_TEST);

  // Viestinkäsittelysilmukka
  alkuaika=GetTickCount();
  MSG msg;
  while(1)
  {
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
      if (msg.message==WM_QUIT) break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    else
    {
      // Käytämme GetTickCount()-funktiota, joka palauttaa ajan millisekunneissa,
      // laskemaan kuvan piirtämiseen kuluneen ajan.
      // Näin voimme ajastaa kuution pyörimään samalla nopeudella kaikilla kokeilla.
      aika=GetTickCount();
      piirtoaika=aika-alkuaika;

      if (piirtoaika>0)
      {
        alkuaika=aika;

        // Kasvata pyörityskulmaa hieman
        // Kuutio pyörii nyt 0.06 astetta millisekunnissa
        // eli 1 kierroksen 6 sekunnissa
        angle+=0.06*piirtoaika;

        // Tyhjennä väripuskuri ja syvyyspuskuri
        glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

        // Aseta modelview-matriisi
        glLoadIdentity();           // "Resetoi" matriisi yksikkömatriisiksi
        glTranslatef(0, 0, -5);     // Siirrä kuutiota hieman kauemmaksi kamerasta
        glRotatef(angle, 1, 0, 0);  // Pyöritä kuutiota hieman joka akselin ympäri
        glRotatef(angle, 0, 1, 0);
        glRotatef(angle, 0, 0, 1);

        // Piirrä kuutio
        glBegin(GL_QUADS);
        int i;
        for (i=0; i<6*4; i++)
        {
          glColor3f(color[index[i]][0], color[index[i]][1], color[index[i]][2]);
          glVertex3f(vertex[index[i]][0], vertex[index[i]][1], vertex[index[i]][2]);
        }
        glEnd();

        // Toinen tapa piirtää kuutio vertex array:lla.
        /*
        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_COLOR_ARRAY);
        glVertexPointer(3, GL_FLOAT, sizeof(float[3]), vertex);
        glColorPointer(3, GL_FLOAT, sizeof(float[3]), color);
        glDrawElements(GL_QUADS, 4*6, GL_UNSIGNED_INT, index);
        */

        // Vaihda puskuri näytölle.
        SwapBuffers(hdc);
      }
    }
  }

  return 0;
}

8 Loppusanat #

Tässä artikkelissa opit piirtämään kolmeulotteisia kappaleita OpenGL:llä. Seuraavassa osassa puhumme tekstuureista. 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.