KoodiOpenGL-ohjelmointi: grafiikkamoottoriSarjan kolmessa edellisessä osassa olemme esitelleet OpenGL:n tärkeimmät ominaisuudet.Nyt niputamme kaiken yhteen ja rakennamme yksinkertaisen grafiikkamoottorin.Teksti: Mikko Rasa Kuvat: Mikko Rasa, Teija TuhkioAlkuun varoituksen sana:laadukkaan grafiikkamoottorintekeminen on valtava urakka.Älä siis odota tekeväsi seuraavaa UnrealEngineä pelkästään tämän artikkelinohjeilla. Oman grafiikkamoottorintekeminen on silti opettavaista jaauttaa myös ymmärtämään valmiidenmoottorien toimintaa paremmin.Grafiikkamoottori koostuu useista alijärjestelmistä,jotka tekevät yhteistyötäkeskenään. Tärkeimmät niistä ovat resurssienlataus ja hallinta, näkymäverkon(engl. scene graph) hallinta ja renderöinti.Monesti grafiikkamoottorit tarjoavatmyös animointitoimintoja. Laajemmissapelimoottoreissa on myös muita toimintojakuten törmäystarkistus, käyttäjänsyötteen käsittely ja skriptiohjelmat.Tässä artikkelissa keskitymme kuitenkingrafiikkaan.Grafiikkamoottorin voi rakentaa monellatavalla, eikä tässä artikkelissa oletilaa esitellä niitä kaikkia. Jos jokin ratkaisuei miellytä, kokeile rohkeasti toistalähestymistapaa.Artikkelin mukana tuleva esimerkkikoodion toimiva grafiikkamoottori muttaei toteuta kaikkia tässä esiteltyjä ominaisuuksia.Puuttuvien asioiden kohdallaon artikkelissa Puuttuu-merkintä. Lukijavoi halutessaan etsiä näistä aiheista lisätietoainternetistä ja kehittää moottoriaeteenpäin.Yleinen arkkitehtuuriAllekirjoittanut ei useinkaan tee tarkkojasuunnitelmia etukäteen, mutta jonkinlainenyleiskuva on hyvä muodostaa. Kutenaiemmissakin esimerkkiohjelmissa,moottorin toteutuskielenä on C++. Olioohjelmointion myös luonteva valinta.Globaalit muuttujat ja muut globaalittilat voivat tehdä ohjelman kulun seuraamisestaja virheiden etsimisestä vaikeaa,joten niitä on syytä välttää. Tämänhuomaa erityisen hyvin juuri OpenGL:nkanssa, jossa vaikkapa tekstuurin unohtaminensidotuksi jossain päin koodiavoi aiheuttaa virheellistä toimintaa aivanmuualla. Niinpä piilotamme OpenGLtoiminnotkokonaisuudessaan moottorinrajapinnan taakse, jossa tiloja voidaanmuuttaa hallitusti.Koska ohjelmat käyttävät grafiikkamoottoriakirjastona, on moottorin luokatja funktiot syytä nimetä niin, että vältytäänyhteentörmäyksiltä toisten kirjastojenkanssa.Käytön helpottamiseksi teemmemoottorille pääluokan, joka tekee kaikentarvittavan alustuksen. Lisäksi sinne voidaansijoittaa tietyt ylätason toiminnot,kuten kokonaisen ruudun renderöinti.Vältämme kuitenkin muiden luokkien tarpeetontasitomista pääluokkaan, jolloinniiden uudelleenkäytettävyys paranee.C++:n muistinhallinnan väitetäänolevan vaikeaa, mutta sen ei tarvitseolla sitä. Kun heti projektin alussa päättääpelisäännöt muistinkäytölle, välttyyuseimmilta ongelmilta. Skrolli-moottorinesimerkkitoteutuksessa käytämme omistajuusperiaatetta,eli olion luonut luokkaon vastuussa myös sen tuhoamisesta.Käytämme myös pinosta varattuja olioita,aina kun se on järkevää.Virhetilanteiden raportointiin käytämmepoikkeuksia. Muihin virheenkäsittelytapoihinverrattuna niiden etunaon helppokäyttöisyys. Käsittelemättömätpoikkeukset pysäyttävät ohjelman suorituksen,eivätkä virheet jää huomaamatta.Myös poikkeuksen kaappaaminen debuggerissaon helppoa. Moderneilla kääntäjilläpoikkeuksien käyttö ei tee koodistahitaampaa, joten ainoaksi haittapuoleksijää hieman suurempi ohjelman koko.ResurssienhallintaResurssienhallinnan tehtävänä on ladataohjelman tarvitsemia resursseja levyltä japitää kirjaa jo ladatuista resursseista. Jossamaa resurssia pyydetään uudestaan,resurssienhallinta voi antaa valmiiksi ladatunesiintymän.Resursseihin on pystyttävä viittaamaanjotenkin, joten annamme niille nimet.Suoraviivaisinta on käyttää tiedostonimiä.Resursseja on myös helpointakäsitellä työstövaiheessa erillisinä tiedostoina.Myöhemmin resurssienhallintaanvoidaan lisätä vaihtoehtoisia lataustapoja,kuten vaikkapa tuki zip-tiedostoille.Tärkeimmät resurssityypit ovat objekti,shader-ohjelma ja tekstuuri. Lisäksitarvitaan joitakin aputyyppejä, jotka eivätkuvaa mitään OpenGL:n olioita vaanauttavat muiden resurssien hallinnassa.Tekstuurit ovat yksinkertaisesti kuvatiedostoja.Yleisissä kuvaformaateissa eiole mahdollista helposti määritellä lisäominaisuuksiakuten suodatusta tai toistoa,joten käytämme kaikille tekstuureillesamoja asetuksia.Shaderit ladataan GLSL-kielisestälähdekoodista. Koska kokonaiseen sha-48 2014.1
der-ohjelmaan tarvitaan sekä kulmapiste-että pikselishaderi, nämä on erotettavatiedostossa jotenkin. Valitsemmeerottimeksi vähintään kolme yhdysmerkkiä(-) sisältävän rivin, koska se on visuaalisestiselvä erotin eikä sellaista voinormaalisti esiintyä GLSL-koodissa.Objektit ovat kaikkein monimutkaisintapaus. Ei ole olemassa mitään universaaliakaikkien käyttämää formaattia,mutta jotkin formaatit ovat levinneet varsinlaajalle. Mainitsemisen arvoisia ovatainakin Collada, 3DS ja OBJ.Collada on suhteellisen uusi, avoimeenstandardiin perustuva XML-pohjainenformaatti, jonka on julkaissutKhronos Group. Teoriassa se voi sisältäämitä tahansa 3D-grafiikassa tarvittavaadataa. Yleiskäyttöisyytensä vuoksi se onkuitenkin hieman raskas tämän artikkelintarpeisiin.3DS puolestaan on Autodeskin 3DStudion käyttämä formaatti. Siitä ei olejulkaistu virallista määrittelyä, mutta senrakenne on selvitetty perin pohjin. Myös3DS pystyy säilömään monentyyppistädataa, mutta binääriformaatin käsittelyon työlästä ja altista virheille.OBJ on peräisin Wavefront Technologiesinmallinnusohjelmista. Siitäkään eiole kehittäjän julkaisemaa määrittelyä,mutta tekstipohjaisena formaattina seon erittäin helppolukuinen. Yksinkertaisimmillaanyksi tiedosto sisältää yhdenobjektin, joten se soveltuu mainiosti grafiikkamoottorimmeensimmäiseksi objektiformaatiksi.Tilan säästämiseksi en selitä tässäformaattia kokonaisuudessaan. Halukkaatvoivat tutustua Wikipedian kuvaukseen tai esimerkkiohjelman lähdekoodiin.Pari asiaa on kuitenkin syytämainita.OBJ-tiedostossa kulmapisteidensijainnit, normaalit ja tekstuurikoordinaatitmuodostavat kukin omantaulukkonsa. Tasopintojen määrittelytviittaavat taulukoihin erikseen,jolloin esimerkiksi samaanormaalia voi käyttää usean eri kulmapisteenkanssa. Tämä sopii huonostiOpenGL:ään, jossa kunkin kulmapisteenon oltava oma kokonaisuutensa kaikkineominaisuuksineen. Niinpä joudummehieman kikkailemaan saadaksemme datansopivaan muotoon.Toinen huomionarvoinen seikka onobjektin koostuminen yksittäisistä tasopinnoista.Ne voivat olla myös kolmioita,nelikulmioita tai jopa useampikulmaisiakuvioita. Tämä ei olekovinkaan tehokasta, koska indeksejätarvitaan enemmän ja välimuistintehokkuus kärsii. Optimaalisenketjutuksen muodostaminen on varsinmonimutkaista, mutta onneksi yksinkertaisellakinalgoritmilla päästään tyydyttäväänlopputulokseen.3D-mallinnusohjelmissa ei yleensäole mahdollisuuksia shaderien määrittelyyn,eikä OBJ-formaattikaan tue niitä.Sen sijaan materiaalit ovat yleisesti käytettyominaisuus, joten käytämme niitämyös Skrolli-moottorissa. Materiaali sisältäävähintäänkin viittauksen shaderohjelmaanja mahdollisesti myös tekstuuriin.Lisäksi se voi sisältää shaderinkanssa käytettäviä uniform-muuttujienarvoja.Materiaaleille ei ole tarjolla mitäänyleisesti käytettyä ja yksinkertaista tiedostoformaattia,joka täyttäisi kaikki tarpeemme,joten joudumme kehittämäänoman.Resurssienhallinta omistaa lataamansaresurssit ja tuhoaa ne viimeistäänsitten, kun se itsekin tuhoutuu. Julkisenrajapinnan kautta siltä voi pyytää viittauksen tietyn nimiseen resurssiin. Funktiomallineillavoimme myös tarkistaa,että resurssi on odotettua tyyppiä.Paljon dataa sisältävissä peleissä voiolla tarpeen myös poistaa tarpeettomaksikäyneitä resursseja muistista ja tehdätilaa uusille, esimerkiksi siirryttäessäkentästä toiseen. Tällöin on huolehdittavasiitä, ettei mikään toinen resurssienää viittaa muistista poistettuihin resursseihin.(Puuttuu.)NäkymäverkkoNäkymäverkko on tietorakenne, jokakuvaa ruudulle piirrettäviä esineitä janiiden suhteita toisiinsa. Jotkin grafiikkamoottorittuovat sen hyvinvahvasti esiin abstraktionkautta, toisissa se taasmuodostuu hyvinkin huomaamattomasti.Skrolli-moottorissakäytämmesuhteellisenyksinkertaista jaselkeää verkkorakennetta.Verkkokoostuurenderöitävistäasioista, joten luomme abstraktinkantaluokan nimeltä Renderable.Eri operaatioista teemme virtuaalifunktioita.Eräs tärkeä ominaisuus näkymäverkossaon asioiden ryhmittely. Vähintäänkintarvitaan yksi ryhmä, joka sisältääkaikki piirrettävät esineet. Yhtä helppoaon kuitenkin tehdä yleispätevä ryhmittelymalli,jolla voi luoda näkymästä hierarkkisenesityksen. Tämä helpottaa asioidenkäsittelyä, kun kokonaista ryhmäävoi käsitellä yhtenä yksikkönä.Usein on tarpeen sisällyttää näkymäänuseampi kopio samasta asiasta.Esimerkiksi metsän jokaisesta puusta onturha tehdä yksilöllistä, koska ne veisivätliikaa muistia eikä katsoja kuitenkaanhuomaisi kaikkia eroja. Muutaman erilaisenpuumallin monistaminen riittääluomaan vaikutelman elävästä metsästä.Tätä varten teemme Instance-nimisenapuluokan, joka pitää sisällään käytettävänmatriisin sekä viittauksen alkuperäiseenasiaan.RenderöintiEnsimmäinen ja tärkein näkymäverkkoontoteutettava operaatio on näkymänrenderöinti. Siihen tarvitaan jonkinverran globaalia tilaa, kuten kameranja valaistuksen tiedot. Koska haluammepitää moottorin eri luokat mahdollisimmanitsenäisinä, välitämme nämä tiedotrender-funktion parametrina aputietorakenteessa.OpenGL:n tilan muuttaminen vie hiemanresursseja, joten turhia tilamuutoksiaon syytä välttää. Ensimmäinen askeltähän on jättää asetetut tilat voimaankunkin esineen renderöinnin jälkeen.Jos seuraava esine käyttää samojatiloja, ajuri havaitsee, ettei tilamuutostatodellisuudessa tapahdu, ja jättääoperaation suorittamatta. KoskaOpenGL:n tila on moottorin hallinnassa,voimme rakentaa renderöintilogiikansiten, ettei tahatonta virhetoimintaapääse syntymään.Mikäli halutaan piirtää läpikuultaviaesineitä kuten värillistä lasia,on läpikuultavat osat renderöitävämuiden jälkeen ja järjestyksessäkauimmaisesta lähimpään. Muutenvoi käydä niin, että läpikuultavanesineen takana oleva toinenesine ei piirrykään ruudulle, koskasyvyyspuskurissa on jo lähempänäoleva arvo. (Puuttuu.)Virtuaalimaailman kasvaessa laajaksion tavallista, että maailmasta onnäkyvissä kerrallaan vain pieni osa. Kaikkiaesineitä koskevien piirtokomentojen49