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 4: Valot ja varjot

OpenGL on käyttöjärjestelmäriippumaton kirjasto 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 viimeinen osa.

27.7.2004 julkaistun artikkelin on kirjoittanut markus.

  1. 1. Valaistuksen matematiikkaa
  2. 2. Käytännön toteutus
  3. 3. OpenGL:n oma valojärjestelmä
  4. 4. Pikselin tarkka valaistus
  5. 5. Verteksi- ja pikselivarjostinohjelmat
  6. 6. Sapluunapuskuri
  7. 7. Varjot
  8. 8. Esimerkkiohjelma
  9. 9. Loppusanat

1. Valaistuksen matematiikkaa #

Vasemmalla ilman valoja. Keskellä valojen kanssa ja oikealla myös varjot mukana. Huomaa lisääntynyt realismi.Vasemmalla ilman valoja. Keskellä valojen kanssa ja oikealla myös varjot mukana. Huomaa lisääntynyt realismi.

Oikeassa elämässä näkemämme kuva syntyy, kun jostakin valonlähteestä lähtevä valo heijastuu jostakin pinnasta silmämme verkkokalvolle. Voisimme siis teoriassa laskea virtuaalimaailmassa olevan kappaleen valaistuksen sinkoamalla virtuaalisesta valonlähteestä miljardeittain valonsäteitä ja laskemalla mitkä niistä osuvat renderöimäämme pintaan.

Miljardien valonsäteiden radan laskeminen ei kuitenkaan sovi reaaliaikasovellukselle. Ajatus voidaan tietenkin kääntää päälaelleen. Jäljitetään pinnan jokaisesta pikselistä valonsäteen rataa nurinkurisesti ja katsotaan johtaako se valonlähteeseen. Tätä menetelmää kutsutaan termillä "ray tracing". Tekniikka on huomattavasti kevyempi ja sitä käyttäviä demoja on muutamia, mutta myös tämä tekniikka on vielä liian raskas varsinkin peleille.

Koska emme voi laskea valon vaikutusta täydellisen tarkasti, täytyy meidän löytää jokin keino arvioida sitä. Arviointitekniikoita on useita, joista tässä artikkelissa esittelen kaksi samankaltaista nimeltään: Blinn ja Phong.

Näissä tekniikoissa pinnasta verkkokalvolle saapuva valo jaetaan karkeasti neljään eri tyyppiin: ympäristövaloon (ambient light), hajavaloon (diffuse light), peiliheijastukseen (specular light) ja itsesäteiltyyn valoon (emissive light).

1.1 Hajavalo

Hajavalo on valoa, joka tulee suoraan jostakin valonlähteestä pintaan. Pinta absorboi osan tästä valosta ja heijastaa loput eteenpäin silmän verkkokalvolle. Merkitsemme hajavaloa tässä artikkelissa symbolilla D (niin kuin diffuse). Hajavalon määrä on riippuvainen siitä kulmasta, jossa valonsäde saapuu pintaan. Jos L (niin kuin light) on vektori kohti valonlähdettä ja N (niin kuin normal) pinnan normaalivektori ja kummankin näiden vektorien pituus on yksi, niin hajavalon määrä saadaan näiden pistetulona eli N · L. Pinta ei kuitenkaan heijasta valon kaikkia aallonpituuksia tasapuolisesti, vaan se absorboi osaa paremmin ja osaa huonommin. Tästa muodostuu pinnan väri. Esim. jos pinta absorboi kaiken muun paitsi punaisen valon näyttää pinta punaiselta. Kaiken lisäksi pintaan saapuva valo ei välttämättä ole valkoista eli sisällä kaikkia mahdollisia aallonpituuksia. Meidän tarvitsee siis kertoa hajavalo vielä pinnan värillä, jota merkitsemme symbolilla Cd (niin kuin color ja decal) ja valon värillä, jota merkitsemme symbolilla Cl (niin kuin color ja light). Niinpä saamme hajavalon lopulliseksi yhtälöksi: Cd * Cl * ( N · L ) . Tässä * ei siis tarkoita pistetuloa vaan värien kertomista komponenteittain.

1.2 Ympäristövalo

Ympäristövalo on valoa, jonka ei voida sanoa tulevan mistään tietystä suunnasta ja se vaikuttaa kaikkiin pintoihin samalla tavalla, olkoot ne sitten missä asennossa tahansa suhteessa valonlähteeseen. Tämä johtuu siitä, että valonsäteet kimpoilevat pinnasta toiseen sekoittuen lopulta yhteinäiseksi tasaiseksi valoksi. Merkitsemme tässä artikkelissa ympäristövaloa symbolilla A (niin kuin ambient). Ympäristävalon määrä on koko ajan vakio ja sitä ei oikein voi laskea mitenkään, vaan se sen määrä on arvioitava. Jos minimi on 0 ja maksimi 1, niin realistinen kuva saadaan yleensä hyvin pienillä arvoilla esim 0.1. Koska pinta heijastaa myös ympäristövalosta vain tietyt aallonpituudet, täytyy se kertoa pinnan värillä, joilloin saamme tälle komponentille yhtälön: Cl * A.

1.3 Peiliheijastus

Kun valo osuu pintaan tietyssä kulmassa, ei pinta absorboikkaan yhtään valoa vaan heijastaa sen sellaisenaan eteenpäin. Kutsumme tällaista valoa peiliheijastuneeksi ja merkitsemme sitä symbolilla S (niin kuin specular). Peiliheijastuksen kulma riippuu pinnan materiaalista, kutsumme tätä ominaisuutta pinnan kiiltävyydeksi ja merkitsemme sitä symbolilla G (niin kuin gloss). Lisäksi tähän vaikuttaa pinnan tasaisuus, jota merkitsemme symbolilla M, realistinen M:n arvo on yleensä välillä 8 - 16. Peiliheijastus riippuu valonlähteen sijainnin lisäksi myös siitä mistä kulmasta pintaa katsellaan. Meidän tarvitsee tietää siis vielä vektori, joka osoittaa kohti kameraa. Merkitsemme tätä vektoria symbolilla E (niin kuin eye). Peiliheijastuksen laskemiseeen on kaksi tapaa: ns phong- ja blinn-valaistusmallit.

Phong-mallissa meidän tarvitsee tietää vektori R (niin kuin reflection), joka saadaan kun vektori E peilataan vektorin N suhteen. Tämä tehdään kaavalla: R = 2 * (N · L) * N - L. Kun R on tiedossa saadaan peiliheijastus kaavasta Cl * G * (L · R)^M. Eli valon väri kertaa G kertaa L:n ja R:n pistetulo potenssiin M. Pinnan väri ei siis vaikuta peiliheijastukseen.

Vektorin R laskeminen on kuitenkin hieman turhan monimutkainen. Tämän takia on olemassa yksinkertaisempi malli Blinn. Blinn-mallissa vektoria R ei tarvita, vaan lasketaan vektori H, joka puolittaa vektoreiden L ja E välisen kulman. Tähän jälkeen peiliheijastus lasketaan yhtälöstä: Cl * G * (N · H)^M.

Potenssilasku ^M kuitenkin muodostaa pienen ongelman. Potenssiin korotus nimittäin on kaikesta nykyajan laskentatehosta huolimatta varsin hidas operaatio varsinkin kun se joudutaan laskemaan, jopa tuhansia kertoja per frame. Tämän takia saattaa joskus olla järkevää lukita M arvoon 16 ja aproksimoida funktiota x^16, jollakin toisella yksinkertaisemmalla funktiolla. Tällaisia ovat mm: max( 0, 4*(x-0.75) ) ja max( 0, 4*(x*x-0.75) ).

1.4 Itsesäteilty valo

Viimeinen muoto on itsesäteilty valo. Tämä on valoa, jota pinta itse tuottaa. Tyypillinen itsevalaiseva pinta on fosfori. Merkitsemme tätä valoa symbolilla E (niin kuin emissive). Tätäkään valoa ei voida mitenkään laskea vaan se on arvioitava. Tavallisilla pinnoilla tämä se on yleensä 0. Jos oletamme, että pinta säteilee itsensä väristä valoa saamme tämän komponentin kaavaksi: Cd * E.

1.5 Lopullinen valoyhtälö

Nyt voimme muodostaa yhtälön, joka antaa meille hyvin realistisen valaistuksen. Laskemme vain eri muodot yhteen eli kokonaisvalo, jota merkitsemme symbolilla I (niin kuin illumination) on A+D+S+E. Jos vielä puramme yhtälön auki blinn-mallin mukaisesti saamme I = Cd * A + Cd * Cl * ( N · L ) + Cl * G * ( N · H ) ^ M + Cd * E . Tämä ei ota vielä huomioon sitä tosiasiaa, että valon kirkkaus vaimenee mitä kauempana valonlähteestä ollaan. Yhtälö pitää siis kertoa vaimennustermillä, joka saadaan jakamalla 1 jollakin sopivalla vakiolla k kerrottuna etäisyyden (käytämme symbolia d) neliöllä eli 1/(1+k*d^2). Tämä olisi siis fysikaalisesti oikein, mutta ei välttämättä hyvän näköinen. Niinpä usein käytetään jotain muuta vaimennustermiä esim: max( 0, 1 - ( d / r )^2 ). Tällöin valon kirkkaus on maksimissaan keskellä valonlähdettä ja hiipuu nollaan saavutettaessä etäisyys r ja on nolla kaikkialla tätä kauempana.

Lopullinen (blinn-mallin mukainen) yhtälö on siis: I = 1/(1+k*d^2) * ( Cd * A + Cd * Cl * ( N · L ) + Cl * G * ( N · H ) ^ M + Cd * E ).

Jos valonlähteitä on useampia pitää valaistus laskea kaikille valoille erikseen ja sitten summata tulokset.

2. Käytännön toteutus #

Nyt siis tiedämme valaistukseen tarvittavan yhtälön. Mutta kuinka kappale sitten oikein valaistaan sillä? Helposti, lasketaan yhtälö kappaleen jokaiselle verteksille ja annetaan tulos värinä OpenGL:lle glColor3f()-funktiolla.

Tehdään seuraavaksi funktio, joka laskee tämän valoyhtälön syötteenään saamalle verteksille. Se saa syötteenään verteksin sijainnin (Pv), valon sijainnin (Pl), pinnan värin (Cd), valon värin (Cl) ja pinnan normaalin (N). Funktio laskee verteksille valon ja antaa sen OpenGL:lle värinä. Yksinkertaisuuden vuoksi se jättää vaimenemisen huomiotta ja olettaa itsesäteillyn valon olevan nolla.

void valo(float Pv[3], float Pl[3], float Cd[3], float Cl[3], float N[3])
{
  const float A=0.1;
  const float G=0.9;
  const float M=16;

  float L[3], E[3], H[3], I[3];
  float temp;

  // Laske vektori L, joka osoittaa kohti valoa.
  L[0]=Pl[0]-Pv[0];
  L[1]=Pl[1]-Pv[1];
  L[2]=Pl[2]-Pv[2];
  temp=sqrt(L[0]*L[0]+L[1]*L[1]+L[2]*L[2]);
  L[0]/=temp;
  L[1]/=temp;
  L[2]/=temp;

  // Laske vektori E, joka osoittaa kohti kameraa.
  E[0]=-Pv[0];
  E[1]=-Pv[1];
  E[2]=-Pv[2];
  temp=sqrt(E[0]*E[0]+E[1]*E[1]+E[2]*E[2]);
  E[0]/=temp;
  E[1]/=temp;
  E[2]/=temp;

  // Laske vektori H, joka puolittaa vektorien E ja L välisen kulman.
  H[0]=L[0]+E[0];
  H[1]=L[1]+E[1];
  H[2]=L[2]+E[2];
  temp=sqrt(H[0]*H[0]+H[1]*H[1]+H[2]*H[2]);
  H[0]/=temp;
  H[1]/=temp;
  H[2]/=temp;

  // Laske ympäristövalo + hajavalo + peiliheijastus
  I[0]= A*Cd[0] + Cd[0]*Cl[0]*pisteTulo(N, L) + G*Cl[0]*pow(pisteTulo(N, H), M);
  I[1]= A*Cd[1] + Cd[1]*Cl[1]*pisteTulo(N, L) + G*Cl[1]*pow(pisteTulo(N, H), M);
  I[2]= A*Cd[2] + Cd[2]*Cl[2]*pisteTulo(N, L) + G*Cl[2]*pow(pisteTulo(N, H), M);

  // Anna tulos OpenGL:lle.
  glColor3f(I[0], I[1], I[2]);
}

Tämä johtaa kuitenkin ongelmaan teksturoinnin yhteydessä. Pinnan värihän saadaan tällöin tekstuurista, joten se voi olla eri jokaisella pikselillä. Miten siis tekstuuri suhtautuu valaistukseen? Jos luit sarjan edellisen osan muistat varmaan, että värin ja tekstuurin yhdistämisestä huolehtii texture environment, joka oletuksena kertoo värin ja tekstuurin keskenään (GL_MODULATE). Jos jätemme valoyhtälöstä pois komponentin Cd saamme yhtälön: I = 1/(1+k*d^2) * ( A + Cl * ( N · L ) + Cl * G * ( N · H ) ^ M + E ). Jos nyt laskemme valaistuksen tämän yhtälön mukaisesti ja annamme texture environmentin kertoa värin tektuurilla saamme lopulliseksi väriksi: I = Cd * ( 1/(1+k*d^2) * ( A + Cl * ( N · L ) + Cl * G * ( N · H ) ^ M + E ) ), joka on muuten sama kuin alkuperäinen yhtälömmekin paitsi, että myös peiliheijastus tulee kerrottua pinnan värillä. Tämä ei ole suuri katastrofi, jos peiliheijastuksen arvo on mitättömän pieni ( G on noin 0 ), mutta pilaa koko vaikutelman, jos peiliheijastuksen osuus on merkittävä. Tämän ongelman voi korjata toissijaisella värillä, josta puhumme seuraavaksi.

2.1 Toissijainen väri

glColor3f()-funktiolla annettua väriä kutsutaan ensisijaiseksi väriksi. On kuitenkin olemassa laajennus GL_EXT_secondary_color, joka sallii toissijaisen värin määrittämisen. Tämä laajennus tuo mukanaan mm. funktion glSecondaryColor3fEXT(Glfloat red, Glfloat green, Glfloat blue), jolla toissijainen väri annetaan. Toissijainen väri ei osallistu texture environmenttiin, vaan se lisätään (siis lasketaan yhteen) pikselin väriin vasta sen jälkeen kun teksture environment on tehnyt tehtävänsä. Tämä kuitenkin tapahtuu vain, jos GL_COLOR_SUM_EXT on päällä. Se saadaan päälle kutsulla glEnable(GL_COLOR_SUM_EXT).

Korjataan nyt edellisen kappaleen funktiota niin, että se olettaa pinnan värin tulevan tekstuurista ja erottaa peiliheijastuksen toissijaiseen väriin. Funktio olettaa, että GL_EXT_secondary_color-laajennos on tuettu ja että GL_COLOR_SUM_EXT ja GL_TEXTURE_2D ovat päällä ja texture environment on GL_MODULATE-moodissa.
void valo2(float Pv[3], float Pl[3], float Cl[3], float N[3])
{
  const float A=0.1;
  const float G=0.9;
  const float M=16;

  float L[3], E[3], H[3], I[3], S[3];
  float temp;

  // Laske vektori L, joka osoittaa kohti valoa.
  L[0]=Pl[0]-Pv[0];
  L[1]=Pl[1]-Pv[1];
  L[2]=Pl[2]-Pv[2];
  temp=sqrt(L[0]*L[0]+L[1]*L[1]+L[2]*L[2]);
  L[0]/=temp;
  L[1]/=temp;
  L[2]/=temp;

  // Laske vektori E, joka osoittaa kohti kameraa.
  E[0]=-Pv[0];
  E[1]=-Pv[1];
  E[2]=-Pv[2];
  temp=sqrt(E[0]*E[0]+E[1]*E[1]+E[2]*E[2]);
  E[0]/=temp;
  E[1]/=temp;
  E[2]/=temp;

  // Laske vektori H, joka puolittaa vektorien E ja L välisen kulman.
  H[0]=L[0]+E[0];
  H[1]=L[1]+E[1];
  H[2]=L[2]+E[2];
  temp=sqrt(H[0]*H[0]+H[1]*H[1]+H[2]*H[2]);
  H[0]/=temp;
  H[1]/=temp;
  H[2]/=temp;

  // Laske ympäristövalo + hajavalo
  I[0]= A     +     Cl[0]*pisteTulo(N, L);
  I[1]= A     +     Cl[1]*pisteTulo(N, L);
  I[2]= A     +     Cl[2]*pisteTulo(N, L);
  //  Laske  peiliheijastus
  S[0]= G*Cl[0]*pow(pisteTulo(N, H), M);
  S[1]= G*Cl[1]*pow(pisteTulo(N, H), M);
  S[2]= G*Cl[2]*pow(pisteTulo(N, H), M);

  // Anna tulos OpenGL:lle.
  glColor3f(I[0], I[1], I[2]);
  glSecondaryColor3fEXT(S[0], S[1], S[2]);
}

3. OpenGL:n oma valojärjestelmä #

Ei kuitenkaan ole pakko kirjoittaa omaa funktiota, joka laskee valaistuksen, sillä OpenGL sisältää myös oman valaistusjärjestelmän. Valaistus saadaan päälle kutsulla glEnable(GL_LIGHTING);. Kun valaistus on päällä laskee OpenGL valot automaattisesti ja korvaa värit saamillaan tuloksilla. glColor3f()-funktiolla ei siis ole mitään vaikutusta silloin kun valaistus on päällä! Valonlähteitä on maksimissaan kahdeksan ja niitä voidaan laittaa päälle ja pois yksitellen glEnable() ja glDisable()-funktioilla, joille annetaan parametrina GL_LIGHTx, jos x on valon numero väliltä 0-7.

Valon attribuutteja (sijaintia, väriä jne...) voidaan muuttaa glLightfv()-funktiolla, jonka prototyyppi näyttää tältä:

void glLightfv(GLenum light, GLenum pname, const GLfloat *params);

Ensimmäinen parametri kertoo valon, jonka attribuutteja muutetaan (siis GL_LIGHTx). Toinen on muuttava attribuutti. Tärkeimmät ovat GL_AMBIENT (ympäristovalon eli A:n arvo), GL_DIFFUSE ja GL_SPECULAR (valon väri eli Cl, OpenGL siis sallii eri valon värit hajavalolle ja peiliheijastukselle), GL_QUADRATIC_ATTENUATION (vaimenemisen vakio k) ja GL_POSITION (valon sijainti). Viimeinen parametri on osoitin 4-komponenttiseen taulukkoon, joka sisältää attribuutin uuden arvon. 3 ensimmäistä kenttää ovat attribuutin uusi arvo ja viimeisen kentän on oltava 1. Paitsi GL_QUADRATIC_ATTENUATION tapauksessa, jossa taulukko sisältää vain yhden arvon.

Valaistavan pinnan ominaisuuksia voidaan asettaa glMaterialfv()-funktiolla. Prototyyppi näyttää tältä:

void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params);

Ensimmäisen parametrin on oltava GL_FRONT_AND_BACK. Toisen parametrin mahdolliset arvot ovat: GL_AMBIENT_AND_DIFFUSE (pinnan väri), GL_SPECULAR (pinnan kiiltävyys eli G:n arvo), GL_EMISSION (itsesäteillyn valon määrä eli E:n arvo) ja GL_SHININESS (vakion M arvo). Viimeinen parametri on jälleen kerran osoitin 4 komponenttiseen taulukkoon, joka sisältää uuden arvon. Paitsi GL_SHININESS tapauksessa, jossa taulukko sisältää vain yhden arvon.

Koska pinnan normaalia tarvitaan valaistuksen laskemiseen täytyy se antaa OpenGL:lle. Tämä tehdään funktiolla glNormal3f(), jonka prototyyppi näyttää tältä:

void glNormal3f(GLfloat nx, GLfloat ny, GLfloat nz);

Se siis ottaa normaalivektorin x, y ja z komponentit parametrinaan. Huomaa, että tämän vektorin pituuden tulee olla 1. Tätä funktiota kutsutaan glBegin() ja glEnd()-funktioiden välissä ja jokaiselle verteksille erikseen.

On olemassa GL_EXT_separate_specular_color-laajennus, joka saa OpenGL erottamaan laskemansa valon peiliheijastuskomponentin toissijaiseksi väriksi. Tämä tehdään kutsulla glLightModel(GL_LIGHT_MODEL_COLOR_CONTROL_EXT, GL_ SEPARATE_SPECULAR_COLOR_EXT);

Oikealla ilman peiliheijastusta ja muissa sen kanssa, mutta keskellä GL_SEPARATE_SPECULAR_COLOR_EXT ei ole päällä kun taas vasemmalla se on.Oikealla ilman peiliheijastusta ja muissa sen kanssa, mutta keskellä GL_SEPARATE_SPECULAR_COLOR_EXT ei ole päällä kun taas vasemmalla se on.

4. Pikselin tarkka valaistus #

Huomattavaa on, että OpenGL:n sisäinen valojärjestelmä laskee valot vain vertekseille. Ei siis jokaiselle pikselille erikseen. Tämän jälkeen jokaiselle verteksille lasketut valoarvot (värit), interpoloidaan pikseleille. Kaikki näyttää ihan hyvältä niin kauan kuin polygonien koko on tarpeeksi pieni ja valon lähde kaukana niistä. Jos näin ei ole alkavat virheet näkyä. Parempi olisikin, jos voisimme laskea valoarvot jokaiselle pikselille erikseen jolloin tuloksena olisi huomattavasti realistisempi kuva.

Vasemmalla kuvatun 8 kolmiosta koostuvan pinnan valaistus laskettu vertekseille (keskellä) ja pikseleille (oikealla).<br />
Vasemmalla kuvatun 8 kolmiosta koostuvan pinnan valaistus laskettu vertekseille (keskellä) ja pikseleille (oikealla).

glColor3f()-funktiolla ei kuitenkaan voi antaa väriä erikseen jokaiselle pikselille. Ainoastaan vertekseille. Seuraavassa kappaleessa esittelen pikselivarjostimiksi kutsutun tekniikan, jolla tämä ongelma saadaan pois päiväjärjestyksestä, mutta sitä ennen esittelen muutaman kiertotien.

Jos sekä valo että piirrettävä polygoni ovat staattisia eli ne eivät liiku voidaan valon vaikutus piirtää etukäteen tekstuuriin esim. jollakin kuvankäsittelyohjelmalla. Tällöin säästetään myös tehoa, kun mitään valaistukseen liittyvää ei tarvitse laskea ajon aikana. Tätä tekniikkaa kutsutaan termillä "lightmapping" ja sitä käyttävät mm. pelit Quake 2 ja 3 sekä half-life.

Ajatus voidaan viedä vieläkin pidemmälle. Miksei valoefektejä lasketa tekstuureihin ajon aikana ja muuttuneita tekstuureja sitten ladata uudestaan näytönohjaimen muistiin glTexImage2D()-funktiolla. Jos tekstuurit ovat tarpeeksi matalaresoluutioisia, tämä on ihan toimiva vaihtoehto. Jos tekstuurit kuitenkin ovat kovin korkearesoluutioisia kuluu tekstuurien päivittämiseen liikaa aikaa ja menetelmä ei toimi. Tämän tekniikan nimi on ”dynamic lightmapping”. Mm. pelit Alien vs Predator 1 ja 2 käyttävät tätä tekniikkaa.

Texture environment osaa laskea erilaisia funktioita jokaiselle pikselille, yhdistäen eri tekstuureja. On itse asiassa mahdollista laskea texture environment:in avulla valoyhtälöt jokaiselle pikselille. Tähän tarvitaan laajennukset GL_ARB_texture_env_combine, GL_ARB_texture_env_crossbar (tai GL_NV_texture_env_combine4) ja GL_ARB_texture_env_dot3 ja vähintään 4 teksturointiyksikköä. Lisäksi peiliheijastuksessa tarvittava potenssiinkorotus ei ole mahdollinen, joten sitä on arvioitava jollakin yksinkertaisemmalla funktiolla.

5. Verteksi- ja pikselivarjostinohjelmat #

Verteksi- tai pikselivarjostin on ohjelma tai pidemminkin funktio, jonka OpenGL suorittaa jokaista verteksiä/pikseliä kohden. Parasta tässä on, että voit kirjoittaa tämän funktion itse. Näitä funktioita kutsutaan varjostimiksi, koska niitä käytetään usein nimenomaan valaistuksen laskentaan. Varjostinohjelmat saavat syötteenään verteksin/pikselin kaikki arvot, kuten värin ja sijainnin ja tuottavat tuloksenaan uudet arvot. Huomaa, että koska pikselivarjostinohjelmat suoritetaan kerran jokaista pikseliä kohden, ja kuvassa voi helposti olla miljoonia pikseleitä kasvaa tarvittavan tehon määrä valtavaksi. Tämän takia pikselivarjostimet toimivat vain kaikkein uusimmissa näytönohjaimissa, kuten GeForceFX ja Ati Radeon 9500. Nämä varjostimet ovat saatavilla laajennusten GL_ARB_vertex_program, GL_ARB_fragment_program, GL_ARB_vertex_shader ja GL_ARB_fragment_shader muodossa.

Verteksivarjostimet eivät tuo varsinaisesti mitään uutta. Ne saavat syötteenään verteksin sijainnin, värin jne. Suorittavat näillä jotain laskentaa ja korvaavat arvot uusilla. Olisit voinut tietenkin laskea nämä uudet arvot omassa ohjelmassasi ja antaa ne OpenGL:lle alkuarvoina, joilloin koko verteksivarjostinta ei olisi tarvittu. Etu on siinä, että verteksivarjostimen koodi suoritetaan näytönohjaimen prosessorilla, joka on nimenomaan erikoistunut grafiikan piirtämiseen ja suorittaa verteksivarjostinohjelman yleensä (joskaan ei aina) nopeammin kuin tietokoneen keskusyksikkö. Näin ollen verteksivarjostimet nostavat ohjelman suorituskykyä, ei sen graafista laatua.

Pikselivarjostimet ovat sen sijaan toinen juttu. Ei nimittäin ole muuta keinoa tehdä pikselikohtaisia laskutoimituksia (texture environment:illa kikkailua lukuunottamatta). Niiden avulla on mahdollista laskea blinnin tai phongin valoyhtälöt jokaiselle pikselille erikseen. Mm. Doom 3:n hieno grafiikka perustuu tähän. Valitettavasti pikselivarjostimilla on katastrofaalinen vaikutus suorituskykyyn. Esim. 800x600 resoluutioisessa ikkunassa on 480000 pikseliä (olettaen että yhtään pikseliä ei piirretä kahdesti). Jos pikselivarjostin, jossa on vain 10 rivia koodia, suoritetaan jokaiselle pikselille tekee se lähes 5 miljoonaa suoritettavaa koodiriviä. On siinä näytönohjaimella laskemista.

Varjostinohjelmat voidaan kirjoittaa, joko konekielellä (laajennukset GL_ARB_vertex_program ja GL_ARB_fragment_program) tai korkeamman tason glSlang-kielellä (laajennukset GL_ARB_vertex_shader ja GL_ARB_fragment_shader) (myös Direct3D:llä on vastaavanlainen korkeamman tason kieli nimeltä HLSL). Lisäksi on olemassa NVidian kehittämä Cg. Se on siitä vekkuli, että se toimii sekä OpenGL:ssä, että Direct3D:ssä. Jotta voisit käyttää varjostinohjelmia sinun täytyy opetella jokin näistä kielistä. En kuitenkaan aijo esitellä niitä sen enempää, sillä niistä saisi vaikka oman artikkelinsa. Varjostinohjelmat ovat kuitenkin tulevaisuutta, joten niitä kannattaa joskus edes vilkaista.

6. Sapluunapuskuri #

Ennen kuin voimme mennä eteenpäin pitää minun esitellä sinulle yksi kätevä työkalu nimeltä sapluunapuskuri (englanniksi stencil buffer). Sapluunapuskuri toimii nimensä mukaan sapluunana. Ensin sapluunapuskuriin piirretään jotain, jonka jälkeen sapluunatestaus asetetaan päälle. Tämän jälkeen pikselit piirtyvät vain niihin kohtiin, joissa sapluunapuskuriin on piirretty jotain (tai halutessa toisin päin). Ennen kuin sapluunapuskuria voidaan käyttää pitää se laittaa päälle. Tämä tehdään kutsulla glEnable(GL_STENCIL_TEST).

Sapluunapuskuriin ei kuitenkaan voida tallentaa syvyysarvoja (kuten syvyyspuskuriin) tai väriarvoja kuten väripuskuriin, vaan lukuja väliltä 0 – 255 (olettaen, että sapluunapuskurille varattujen bittien määrä per pikseli on 8). Se mitä sapluunapuskurissa olevalle luvulle tapahtuu sinne piirrettäessä valitaan glStencilOp()-funktiolla. Prototyyppi näyttää tältä:

void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass);

Kolme eri tapausta. Eli mitä tehdään kun pikseli ei läpäise sapluunatestiä (fail), mitä tehdään, kun pikseli läpäisi sapluunatestin, mutta ei syvyystestiä (zfail) ja mitä tehdään, kun pikseli läpäisi molemmat (zpass). Kaikkien parametrien mahdolliset arvot ovat: GL_KEEP (ei muutosta), GL_ZERO (korvataan nollalla), GL_REPLACE (korvataan jollakin vakioarvolla, joka annetaan myöhemmin esiteltävällä glStencilFunc()
-funktion ref-parametrina), GL_INCR (kasvattaa lukua yhdellä), GL_DECR (vähentää lukua yhdellä) ja GL_INVERT (kääntää bitit).

Käytettävä sapluunatesti voidaan valita funktiolla glStencilFunc(). Sen prototyyppi on tämän näköinen:

void glStencilFunc(GLenum func, GLint ref, GLuint mask);

Parametri func kertoo käytettävän funktion. Mahdollisia vaihtoehtoja ovat GL_NEVER (pikseliä ei koskaan piirretä), GL_ALWAYS (pikseli läpäisee testin aina, eli sapluunatesti on pois päältä), GL_EQUAL (pikseli piirretään vain jos sen kohdalla oleva arvo sapluunapuskurissa on sama kuin ref-parametrin arvo), GL_LESS ja GL_GREATER. Viimeinen parametri on maski. Sen arvo on yleensä ~0 eli binääriluku 11111111.

Ennen käyttöä sapluunapuskuri on tietenkin tyhjennettävä. Se tapahtuu kutsulla glClear(GL_STENCIL_BUFFER_BIT). Lisäksi on huomattava, että kun kirjoitamme sapluunapuskuriin emme varmaan halua samalla piirtää mitään väripuskuriin. Tämä voidaan estää kutsulla glColorMask(0,0,0,0); ja äskeisen vaikutus saadaan kumottua kutsulla glColorMask(1,1,1,1);.

7. Varjot #

OpenGL ei sisällä valmista glEnable(GL_SHADOWS)-mekanismia, jolla varjot voisi loihtia helposti. Tämä sen takia, että piirtäessään yhtä polygonia OpenGL ei tiedä mitä kaikkea se tulee vielä piirtämään ennen kuin kuva on valmis. On kuitenkin paljon algoritmeja, joilla varjoja voi tehdä. Helpointa olisi tietenkin käyttää ns. feikkivarjoja eli piirtää kuvaan jotain mikä näyttää hieman varjolta esim. musta ympyrä kappaleen alle. Kun halutaan fysikaalisesti realistisia varjoja tarvitaan hieman enemmän työtä. Kaksi kuuluisinta varjoalgoritmia ovat "shadow map"-algoritmi ja ”stencil shadow volumes”-algoritmi. Kumpikaan algoritmi ei perustu siihen, että ne piirtäisivät varjon, vaan pidemminkin piirtävät kaiken muun paitsi varjon, jolloin varjon kohdat jäävät mustiksi. Itse pidän enemmän "stencil shadow volumes"-algoritmista, jota käytämme myös esimerkkiohjelmassamme, mutta esittelen ensin hieman hankalamman "shadow map"-algoritmin perusidean ja jätän yksityiskohdat lukijan oman tutkisen varaan.

7.1 "Shadow map"-algoritmi

"Shadow map"-algoritmin idea on seuraavanlainen. Renderoidaan kuva ensin valonlähteen näkökulmasta. Valonlähteen on siis oltava suunnattu, eikä "shadow map"-algoritmi toimi pistemäisille valonlähteille (yksi syy miksi en pidä siitä). Kopioidaan tämän kuvan syvyyspuskuri tekstuuriin. Kutsukaamme tätä tekstuuria nimellä shadow map.

Vasemmalla näkymä valonlähteen perspektiivistä katsottuna. Oikealla tämän kuvan syvyyspuskuri esitettynä niin, että vaaleat kohteet ovat lähellä ja tummat kaukana. Huomaa kuinka valo ei näe omia varjojaan.Vasemmalla näkymä valonlähteen perspektiivistä katsottuna. Oikealla tämän kuvan syvyyspuskuri esitettynä niin, että vaaleat kohteet ovat lähellä ja tummat kaukana. Huomaa kuinka valo ei näe omia varjojaan.

Teksturoidaan tällä tekstuurilla kaikki kappaleet valiten tekstuurikoordinaatit niin, että tekstuuri tulee projisoitua kappaleiden päälle valonlähteestä katsottuna. Tämän jälkeen renderoidaan kuva normaalisti samalla testaten jokaiselle pikselille, onko sen etäisyys valonlähteestä suurempi kuin sen kohdalla shadow map:issa oleva arvo. Jos on, pikseli on varjossa ja se voidaan jättää piirtämättä, muuten ei.
Vasemmalla näkymä ilman varjoja. Keskellä näkymä teksturoituna "shadow map"-tekstuurilla, joka on projisoitu näkymän päälle. Oikealla näkymä, jossa sellaiset pikselit joiden etäisyys valosta on suurempi kuin arvo sen "shadow map"-tekstuurissa on jätetty piirtämättä (mustiksi).Vasemmalla näkymä ilman varjoja. Keskellä näkymä teksturoituna "shadow map"-tekstuurilla, joka on projisoitu näkymän päälle. Oikealla näkymä, jossa sellaiset pikselit joiden etäisyys valosta on suurempi kuin arvo sen "shadow map"-tekstuurissa on jätetty piirtämättä (mustiksi).

Itse kuvan renderointi valonlähteestä nähtynä ei ole hankalaa. Syvyyspuskurin kopiointi tekstuuriin taas voidaan tehdä glCopyTexSubImage2D()-funktiolla. Sen prototyyppi näyttää tältä.

void glCopyTexSubImage2D(
GLenum target,
GLint level,
GLint xoffset,
GLint yoffset,
GLint x,
GLint y,
GLsizei width,
GLsizei height
);

Ensimmäisen parametrin on oltava GL_TEX_IMAGE_2D ja toisen 0. Offset-parametrit kertovat kohdan tekstuurista, johon kuva kopioidaan (yleensä 0,0) ja x, y kohdan ruudulta, josta data kopioidaan (yleensä 0,0). Width ja height ovat kopioitavan alueen koko. Huomaa, että tektuurin, johon data tällä funktiolla kopioidaan pitää olla jo valmiiksi olemassa eli se on täytetty jollakin datalla käyttäen glTexImage2D()-funktioita. Lisäksi tämän tekstuurin tulee olla ns. "depth texture", tai muuten glCopyTexSubImage2D() kopioi väridatan eikä syvyysdataa.. Tämä uusi tekstuurimuoto tulee GL_ARB_depth_texture-laajennoksen mukana ja tekstuuri on tätä muotoa, kun glTexImage2D()-funktion components-parametri asetetaan symboliin GL_DEPTH_TEXTURE_ARB.

Tekstuurikoordinaattien laskeminen eli tekstuurin projisointi kappaleiden päälle on aika matemaattinen juttu, jonka jätän käsittelemättä. Mainittakoon kuitenkin, että OpenGL sisältää automaattisen tekstuurikoordinaattien generoinnin, jolla tämä voidaan hoitaa.

Kun shadow map on luotu ja kappaleet teksturoitu sillä jää enää jäljelle kysymys: Kuinka oikein testaamme onko pikseli kauempana valosta kuin sen shadow map arvo? Voisimme tietenkin suorittaa tämän testin pikselivarjostimessa, mutta OpenGL sisältää ihan valmiin laajennoksen tätä varten. Tämän laajennoksen nimi on GL_ARB_shadow. Jätän tähän laajennokseen tutustumisen lukijan oman mielenkiinnon varaan ja siirryn "stencil shadow volumes"-algoritmiin.

7.2 ”Stencil shadow volumes”-algoritmi

Shadow volume eli "katvetila" (jos tiedät paremman suomennoksen niin kerro toki minullekkin) on monitahokas, joka sulkee sisäänsä kaikki ne pisteet, jotka jäävät varjoon ja vastaavasti mikään katvetilan ulkopuolella oleva piste ei ole varjossa. Algoritmin idea on muodostaa monitahokkaalle valonlähteestä päin katsottu silhuetti. Venyttää tätä silhuettia valonlähteestä poispäin joilloin muodostuu monitahokkaan katvetila valonlähteen suhteen.

Monitahokkaan silhuetin löytäminen saattaa aluksi tuntua hankalalta tehtävältä, mutta se on itse asiassa aika helppoa. Seuraavassa yksi algoritmi. Parempiakin varmasti löytyy, mutta uskon tämän olevan aika helppo ymmärtää.
for (jokaiselle monitahokaan taholle)
{
  if (tämä taho osoittaa kohti valoa)
  {
    for (jokaiselle tämän tahon vierustaholle)
    {
      if (tämä vierustaho EI osoita kohti valoa)
      {
        Tahojen välinen särmä kuuluu silhuettiin.
      }
    }
  }
}

Silhuetin löytymisen jälkeen se täytyy venyttää katvetilaksi. Tämä on yksinkertaista. Projisoidaan jokaisesta särmästä kopio poispäin valonlähteestä ja yhdistetään tämä särmä alkuperäisen kanssa nelikulmioksi, joka muodostaa yhden katvetilan tahoista. Näin saadaan päistä avoin katvetila. Tämä katvetila voidaan vielä tarvittaessa sulkea käyttämällä alkuperäisen monitahokkaan omia polygoneja.

Kun katvetila on muodostettu herää enää kysymys: Kuinka testataan mitkä pikselit ovat sen sisässä ja mitkä sen ulkopuolella? Kikka on seuraavanlainen. Ensin kuva renderöidään normaalisti, mutta tummemmalla värillä (esim. käyttäen pelkkää ympäristövaloa) täyttäen samalla syvyyspuskuri. Tämän jälkeen syvyyspuskurin päivitys laitetaan pois päältä ( glDepthMask(GL_FALSE) ), mutta säilyttäen syvyystestaus päällä. Tämän jälkeen katvetila piirretään sapluunapuskuriin ja saplaanaoperaatio asetetaan sellaiseksi, että sapluunapuskurin bitit, käännetään aina, kun sinne piirretään jotain ( glStencilOp(GL_KEEP, GL_KEEP, GL_INVERT) ). Näin katvetilan sisään jäävät pikselit saadaan arvoon 1 ja sen ulkopuoliset arvoon 0. Tämä sen takia, että syvyystestauksesta johtuen katvetilan ulkopuoliset bitit käännetään parillinen määrä kertoja, kun taas sisällä olevat pariton määrä kertoja. Tämän jälkeen kuva piirretään uudestaan oikealla värillä, mutta tällä kertaa saplaanatestaus päällä niin, että piirto tapahtuu vain niihin kohtiin missä saplaanapuskurin arvo on 0 ( glStencilFunc(GL_EQUAL, 0, ~0) ). Näin ollen varjostetulle alueelle ei piirry mitään ja niihin jää tumma varjon väri.
Vasemmalla kaksi valaistua keilaa. Keskellä pystyssä olevan keilan katvetila havainnollistettu. Oikealla keilat on piirretty niin, että kaikki katvetilan sisään jäävät pikselit on jätetty piirtämättä (jäävät siis mustiksi).Vasemmalla kaksi valaistua keilaa. Keskellä pystyssä olevan keilan katvetila havainnollistettu. Oikealla keilat on piirretty niin, että kaikki katvetilan sisään jäävät pikselit on jätetty piirtämättä (jäävät siis mustiksi).

Valitettavasti koverien monitahokkaiden tapauksessa antamani algoritmi tuottaa silhuetteja, jotka leikkaavat itsensä. Tällöin bittien kääntäminen sapluunapuskurissa ei anna oikeaa tulosta (kuten käy myös silloin kun kamera on katvetilan sisässä). Bittien kääntämisen sijaan voidaan käyttää jotain muuta algoritmia kuten "z-pass" tai ”Carmack’s reverse”. Näiden idea on piirtää katvetila kaksi kertaa. Ensimmäisellä kerralla sen etupuoli sapluunapuskurin arvoa kasvattaen ja toisella sen takapuoli sapluunapuskurin arvoa vähentäen, jolloin saadaan taas katvetilan ulkopuolella olevat bitit arvoon 0 ja muut johonkin nollasta poikkeavaan arvoon. Jätän kuitenkin näihin algoritmeihin tutustumisen lukijan omalle vastuulle.

8. Esimerkkiohjelma #

Esimerkkiohjelma piirtää tason ja sen päälle kuution ja yhden valonlähteen. Sekä taso, että kuutio valaistaan OpenGL:n omilla valoilla ja varjot tehdään käyttäen ”stencil shadow volumes”-algoritmia. Tämä on tähän astisista esimerkkiohjelmista monimutkaisin, mutta ei mahdoton ymmärtää. 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 <stdio.h>
//#include <gl\glext.h>  // Ei tarvita tässä ohjelmassa

// Määrittele laitekonteksti globaaliksi sitä nimittäin tarvitaan myös pääfunktiossa.
HDC hdc;

// Valon sijainti
float lightPos[4]={ 0, 5, 0, 1 };

// 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;
}

// Laskee kahden vektorin pistetulon
float pistetulo(float v1[3], float v2[3])
{
  return v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2];
}

// Taso piirretään käyttäen useita pieniä nelikulmioita
// paremman valaistuksen saavuttamiseksi
void piirraTaso(void)
{
  int x, z;

  glBegin(GL_QUADS);
  glNormal3f(0,1,0);
  for (z=0; z<22; z+=2)
  {
    for (x=0; x<22; x+=2)
    {
      glVertex3f(-10+x, 0, -10+z);
      glVertex3f(-10+x, 0, -10+z+2);
      glVertex3f(-10+x+2, 0, -10+z+2);
      glVertex3f(-10+x+2, 0, -10+z);
    }
  }
  glEnd();
}

// Piirtää kuution tai sen katvetilan, jos katvetila-parametri on TRUE
void piirraKuutio(BOOL katvetila)
{
  // Data piirrettävää kuutiota varten
  static float vertex[8][3]={{-1,0,-1},{1,0,-1},{-1,2,-1},{1,2,-1},
                             {-1,0,1}, {1,0, 1},{-1,2, 1},{1,2, 1}};
  static 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} };

  // Tahojen normaalit
  static float normal[6][3]={{0,0,-1},{0,0,1},{1,0,0},{-1,0,0},{0,1,0},{0,-1,0}};

  // Katvetilan muodostusta varten jokaisen tahon on tiedettävä naapurinsa.
  static int naapuri[6][4]={{3,4,2,5},{5,2,4,3},{5,0,4,1},
                            {1,4,0,5},{2,0,3,1},{3,0,2,1}};

  // Jokaiselle verteksille valoa kohti osoittava vektori.
  static float L[8][3];

  if (!katvetila)
  {
    // Piirrä kuutio
    glBegin(GL_QUADS);
    int i, j;
    for (i=0; i<6; i++)
    {
      glNormal3f(normal[i][0], normal[i][1], normal[i][2]);
      for (j=0; j<4; j++)
      {
        glVertex3f(vertex[ index[i][j] ][0],
                   vertex[ index[i][j] ][1],
                   vertex[ index[i][j] ][2]);
      }
    }
    glEnd();
  }
  else
  {
    int i,j;

    // Laske valoa kohti osoittavat vektorit.
    for (i=0; i<8; i++)
    {
      L[i][0]=lightPos[0]-vertex[i][0];
      L[i][1]=lightPos[1]-vertex[i][1];
      L[i][2]=lightPos[2]-vertex[i][2];
    }

    // Piirrä katvetila.
    // Tässä tulee sairaan paljon indeksointia, joka olisi voitu välttää
    // jonkinlaisen verteksi-structuren ja osoittimien käytöllä.
    glBegin(GL_QUADS);
    // Jokaiselle taholle
    for (i=0; i<6; i++)
    {
      // Jos tämä taho osoittaa kohti valoa
      if (pistetulo( normal[i], L[ index[i][0] ] )>=0)
      {
        // Jokaiselle vierustaholle
        for (j=0; j<4; j++)
        {
          // Jos tämä vierustaho EI osoita kohti valoa
          if (pistetulo(normal[ naapuri[i][j] ], L[ index[ naapuri[i][j] ][0] ])<0)
          {
            // Tahojen välinen särmä kuuluu silhuettiin
            // venytä se nelikulmioksi poispäin valosta.
            glVertex3f(vertex[ index[i][j] ][0],
                       vertex[ index[i][j] ][1],
                       vertex[ index[i][j] ][2]);
            glVertex3f(vertex[ index[i][(j+1)%4] ][0],
                       vertex[ index[i][(j+1)%4] ][1],
                       vertex[ index[i][(j+1)%4] ][2]);
            glVertex3f(vertex[ index[i][(j+1)%4] ][0]-100*L[ index[i][(j+1)%4] ][0],
                       vertex[ index[i][(j+1)%4] ][1]-100*L[ index[i][(j+1)%4] ][1],
                       vertex[ index[i][(j+1)%4] ][2]-100*L[ index[i][(j+1)%4] ][2]);
            glVertex3f(vertex[ index[i][j] ][0]-100*L[ index[i][j] ][0],
                       vertex[ index[i][j] ][1]-100*L[ index[i][j] ][1],
                       vertex[ index[i][j] ][2]-100*L[ index[i][j] ][2]);
          }
        }
      }
    }
    glEnd();
  }
}


// Pääfunktio
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
  float angle=0;

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

  // Luo ikkuna
  if (!luoIkkuna(800, 600, "OpenGL:n perusteet - Osa 4: Valot ja varjot")) 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);
  // Valitse syvyystestausfunktio "<=" oletuksena olevan "<" tilalle.
  glDepthFunc(GL_LEQUAL);
  glEnable(GL_DEPTH_TEST);

  // Aseta valo nro. 0 päälle
  float Cl[4]={0.8,0.8,0.8,1};
  float A[4]={0.2,0.2,0.2,1};
  float Cd[4]={1,1,1,1};
  glLightfv(GL_LIGHT0, GL_DIFFUSE, Cl);  // Väri
  glLightfv(GL_LIGHT0, GL_AMBIENT, A);   // Ympätisrövalon määrä
  glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, Cd); // Pinnan väri
  glEnable(GL_LIGHT0);

  // OpenGL lisää valaistukseen vielä yhden valonlähteistä
  // riippumattoman ympäristövalon, josta haluamme päästä eroon.
  float nolla[4]={0,0,0,1};
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, nolla);

  // 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 valon liikkumaan samalla nopeudella kaikilla kokeilla.
      aika=GetTickCount();
      piirtoaika=aika-alkuaika;

      if (piirtoaika>0)
      {
        alkuaika=aika;

        // Kasvata pyörityskulmaa hieman seuraavaa framea varten.
        angle+=0.03*piirtoaika;

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

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

        // Siirrä valoa
        lightPos[0]=5*sin(angle*-0.04);
        lightPos[1]=5+sin(angle*0.1);
        lightPos[2]=5*cos(angle*-0.04);
        glLightfv(GL_LIGHT0, GL_POSITION, lightPos);

        // Ensimmäinen vaihe.
        // täytetään syvyyspuskuri ja piirretään kuva käyttäen pelkkää ympäristövaloa
        glDisable(GL_LIGHTING);
        glDisable(GL_STENCIL_TEST);
        glColor3f(A[0], A[1], A[2]);
        piirraTaso();
        piirraKuutio(FALSE);

        // Toinen vaihe
        // Piirrä katvetila sapluunapuskuriin
        // Jos haluat päästä eroon varjoista kommentoi tämä toinen vaihe pois
        glDisable(GL_CULL_FACE); // Katvetilasta pitää piirtää kaikki osat
        glEnable(GL_STENCIL_TEST); // Sapluunapuskuri päälle
        glColorMask(0, 0, 0, 0);   // Emme halua päivittää väripuskuria
        glDepthMask(0);            // Emmekä syvyyspuskuria
        glStencilFunc(GL_ALWAYS, 0, 0);
        glStencilOp(GL_KEEP, GL_KEEP, GL_INVERT); // Käännä bitit piirtäessä
        piirraKuutio(TRUE);
        glEnable(GL_CULL_FACE);
        glColorMask(1, 1, 1, 1);
        glDepthMask(1);

        // Viimeinen vaihe
        // Piirrä lopullinen kuva kohtiin, jossa sapluunapuskurin arvo on 0
        glEnable(GL_LIGHTING);
        glStencilFunc(GL_EQUAL, 0, ~0);
        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
        piirraTaso();
        piirraKuutio(FALSE);

        // Piirrä vielä valonlähde
        glDisable(GL_LIGHTING);
        glDisable(GL_STENCIL_TEST);
        glPointSize(5);
        glBegin(GL_POINTS);
        glColor3f(1,1,0);
        glVertex3f(lightPos[0], lightPos[1], lightPos[2]);
        glEnd();

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

  return 0;
}

9. Loppusanat #

Tässä artikkelissa opit valoista ja varjoista. Tässä artikkelisarjassa olemme nyt käyneet läpi pintapuolisesti kaikki 3d grafiiikan perusasiat. Lopuksi listaan vielä joitakin linkkejä, joista löydät rutkasti lisää luottevaa OpenGL:stä ja 3D grafiikasta yleensäkkin.

http://www.opengl.org – OpenGL:n virallinen kotisivu. Uutisia, viralliset speksit ja keskustelufoorumit löytyvät täältä.

http://nehe.gamedev.net – NeHe Productions. Netin ylivoimaisesti suosituimmat openGL tutoriaalit.

http://www.gamedev.net/download/redbook.... - The Red Book. Virallinen OpenGL:n opaskirja. Tämä netistä löytyvä ilmaisversio on aika hemmetin vanha, mutta ajaa asiansa. Uusin versio on saatavilla vain kirjakaupoista, mutta siitä on netissä yksi näyteluku: http://www.opengl.org/documentation/red_... .

http://oss.sgi.com/projects/ogl-sample/r... - SGI:n ylläpitämä OpenGL:n laajennusrekisteri. Sama löytyy myös kaikkien näytönohjainvalmistajien kotisivuilta.

http://www.delphi3d.net/hardware/index.p... - OpenGL Hardware Registry. Rekisteri, jossa on lueteltu valtavasti näytönohjaimia ja kerrottu mitä laajennuksia ne tukevat.

http://www.mesa3d.org/ - Mesa 3D Graphics Library. Software OpenGL "ajurit". Voit kokeilla näiden "ajurien" avulla laajennuksia, joita oma näytönohjaimesi ei tue.

http://www.opengl.org/resources/faq/tech... - OpenGL faq. Usein kysytyt kysymykset OpenGL:stä.

http://www.gametutorials.com/gtstore/c-1... - Game tutorials. 50 aloittelijoille tarkoitettua OpenGL-tutoriaalia.

http://www.ultimategameprogramming.com - Ultimate game programming. Lähes 100 hieman edistyneimmille tarkoitettua OpenGL-tutoriaalia.

http://www.codesampler.com/oglsrc.htm - CodeSampler. Paljon OpenGL tutoriaaleja aina aloittelijoille tarkoitetuista edistyneimpiin.

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