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. 1 Valaistuksen matematiikkaa 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 pintaa. Tätä kutsutaan termillä "photon mapping" ja jotkin 3Dmallinnusohjelmat itseasiassa käyttävät tätä menetelmää photorealisten kuvien renderöintiin. Miljardien valonsäteiden radan laskeminen ei kuitenkaan sovi realiaikasovellukselle. Ajutus 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. Vasemmalla ilman valoja. Keskellä valojen kanssa ja oikealla myös varjot mukana. Huomaa lisääntynyt realismi. Koska emme voi laskea valon vaikutusta täydellisen tarkasti, täytyy meidän löytää jokin keino arvioida sitä. Uskottavan valaistuksen aikaansaamiseksi voimme jakaa pinnasta verkkokalvolle saapuvan valon karkeasti neljään eri tyyppiin: ympäristövaloihin (ambient light), hajavaloihin (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 C d (niin kuin color ja decal) ja valon värillä, jota merkitsemme symbolilla C l (niin kuin color ja light). Niinpä saamme hajavalon lopulliseksi yhtälöksi: C d * C l * ( 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: C l * 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 C l * 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ä: C l * 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: C d * 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 = C d * A + C d * C l * ( N L ) + C l * G * ( N H ) ^ M + C d * 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/(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 vaimennus termiä 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/(k*d^2) * ( C d * A + C d * C l * ( N L ) + C l * G * ( N H ) ^ M + C d * 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ä? Helpposti, 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 C d saamme yhtälön: I = 1/(k*d^2) * ( A + C l * ( N L ) + C l * 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 = C d * ( 1/(k*d^2) * ( A + C l * ( N L ) + C l * 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);. GL_EXT_separate_specular_color saattaa olla tuettuna vaikka GL_EXT_secondary_color ei olisikaan. 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). 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 Rodeon 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. 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). 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 laajennuksen tätä varten. Tämän laajennuksen nimi on GL_ARB_shadow. Jätän tähän laajennukseen tutustumisen lukijan omalle vastuulle 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 on 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. Oikealla 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ää ns. Carmack s reverse -algoritmia, jossa katvetila piirretään 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 tähän algoritmiin 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/artikkelit/testi4.zip. #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; // 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; // 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 MSG msg; while(1) if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) if (msg.message==wm_quit) break; TranslateMessage(&msg); DispatchMessage(&msg); else // 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 // Kasvata pyörityskulmaa hieman angle+=0.05; // Siirrä valoa lightpos[0]=5*sin(angle*-0.2); lightpos[1]=5+sin(angle*0.5); lightpos[2]=5*cos(angle*-0.2); 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 keskustelufoorumit löytyvät täältä. OpenGL:n virallinen kotisivu. Uutisia, viralliset speksit ja http://nehe.gamedev.net NeHe Productions. Netin ylivoimaisesti suosituimmat opengl tutoriaalit. http://www.gamedev.net/download/redbook.pdf - 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.
http://oss.sgi.com/projects/ogl-sample/registry/ - SGI:n ylläpitämä OpenGL:n laajennusrekisteri. Sama löytyy myös kaikkien näytönohjainvalmistajien kotisivuilta. http://www.delphi3d.net/hardware/index.php - 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/technical/index.html - OpenGL faq. Usein kysytyt kysymykset OpenGL:stä. http://www.gametutorials.com/tutorials/opengl/opengl_pg1.htm - Game tutorials. 50 aloittelijoille tarkoitettua OpenGL-tutoriaalia. http://www.ultimategameprogramming.com/tutorial.php?category=opengl&page=1 - Ultima game programming. Lähes 100 hieman edistyneimmille tarkoitettua OpenGLtutoriaalia. 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