Senčilniki, scenski graf in fizika
V prejšnji številki smo spoznali naprednejše modeliranje grafičnih objektov. Tokrat se bomo še zadnjič poglobili v drobovje računalniške grafike in si ogledali senčilnike, ki nam omogočajo naprednejše učinke na izrisanih predmetih. Povzeli bomo tudi uporabo senčilnikov v namene hitrejšega procesiranja, ki je postalo eden izmed dejavnikov nadaljnjega razvoja strojne opreme. Razumevanje delovanja senčilnikov nam bo pomagalo izdelati lastne 3D igre. Ogledali si bomo tudi uporabo naprednejših podatkovnih struktur, obdelovanje večjih svetov in simulacijo fizikalnih zakonov. Vsebina članka je skupaj s programsko kodo dostopna na android.monitor.si.
Tokrat si bomo ogledali delo s senčilniki, ki bodo našo igro polepšali ali dodali učinke, ki ustvarijo bolj realističen grafični prikaz. V sodobnih igrah grafična dodelanost šteje za nekakšen sinonim za dobro igranost, čeprav sta pomembna tudi sam scenarij in izvedba. Temu v prid govorijo igre, ki grafično dodelanost kot kriterij za igralnost zavračajo in z abstrakcijo grafičnega prikaza premikajo kazalec igralnosti v prid scenariju. Tak zgled je dobro znana igra Minecraft, v kateri igralec ustvarja lasten abstrakten svet iz kock. Drugi priljubljeni naslovi na PC in konzolnih platformah pa kljub temu prisegajo na učinke, ki jih prinašajo nove igre. Naprednejši izris grafike omogočajo prav senčilniki, ki se izvajajo na grafični kartici. Takšne transformacije bi lahko izvedli tudi v okviru programa ali aplikacije, ki se izvaja na centralni procesni enoti. Zaradi splošne namembnosti slednje pa takšno početje ni primerno zaradi počasnosti izvajanja specializiranih grafičnih ukazov. Z namenskim vezjem lahko dosežemo hitrejše izvajanje določenih funkcionalnosti, ki so predpisane s posameznim standardom grafičnih knjižnic. Najbolj znani knjižnici sta OpenGL in Microsoftov DirectX. Medtem ko je DirectX omejen na uporabo pri platformi Windows in Xbox, je OpenGL namenjen številnim platformam, med drugim je implementiran tudi v sistemu Android.
S povečevanjem funkcionalnosti in hitrosti grafičnih kartic se je razširil tudi osnovni namen kartic, ki ni omejen le na večji užitek pri igranju iger, temveč tudi za naprednejše izračune v znanstvenih vodah. Glavni razlog je zopet hitrost izvajanja matematičnih ukazov. Čeprav je bila še nedolgo tega takšna uporaba konjiček navdušencev, je uporaba procesorske moči grafične kartice v druge namene postala resen dejavnik tudi pri samem razvoju strojne opreme.
Zgodovina uporabe senčilnikov
Senčilniki so zaradi enostavne rabe in visoke hitrosti nadvse priročni za izdelavo vizualnih učinkov. Kot glavna ovira se je v preteklosti pojavljal problem različnih standardov, podprtih s strani izdelovalcev grafičnih kartic in razvijalcev iger. Funkcionalnosti, ki so sprva temeljile na izbiri izdelovalcev kartic, so se združevale v knjižnice naprednejših metod. Leta 1992 je skupina Khronos predstavila knjižnico OpenGL, ki je predstavljala vmesni nivo med programsko in strojno opremo. Uporaba naprednejših zmožnosti grafičnih kartic je bila pred tem že mogoča, a je vsaka strojna oprema potrebovala prirejeno programsko opremo. Leta 1995 je Microsoft predstavil DirectX (zmogljivosti prve različice knjižnice so vidne na sliki 1) kot grafično knjižnico, namenjeno razvoju za operacijski sistem Windows 95.
Slika 1: Prikaz zmožnosti knjižnice DirectX 1.0
Nasledila je pred tem uporabljani API WinG, ki v knjižnico združeval nekatere pogostejše zmožnosti grafičnih kartic za izris grafičnih aplikacij okolju Windows 3.x. Tako OpenGL kot DirectX sta vsebovali podporo statičnemu cevovodu, ki je dovoljeval določene grafične učinke. Šele na začetku tega desetletja smo dobili grafične kartice, ki so vsebovale programabilni cevovod, ki ga razvijalci lahko nadzorujejo s senčilniki. To je odprlo vrata novim zmožnostim manipulacije izrisa na zaslon, hiter razvoj in široka vgradnja knjižnic v grafične kartice pa je prinesla enostavnejši in hitrejši razvoj iger. S prenosom knjižnice DirectX na platformo Xbox je Microsoft želel uporabo knjižnice še bolj okrepiti, enako je OpenGL dobil svoje mesto v novejših generacijah grafičnih procesorjev pametnih telefonov.
Slika 2: Prikaz zmogljivosti najnovejših knjižnic DirectX 11 (zgoraj) in OpenGL 4.3 (spodaj)
Zabavna industrija je z vedno višjimi zahtevami tako vzpodbudila razvoj grafičnih kartic, da jih danes obravnavamo kot najmočnejši del strojne opreme. Raziskovalci so rešitev svojih potreb po izračunih kompleksnih problemov videli v prenosu dela programske kode iz centralne procesne enote na grafično kartico. Problemi, ki jih rešujemo s superračunalniki, so navadno računsko zahtevni, obenem pa jih je mogoče paralelizirati (o paralelizaciji smo že govorili v preteklih člankih). Grafično kartico lahko vidimo kot skupek desetin majhnih namenskih procesnih enot. Najtežje računske operacije lahko v obliki programa prenesemo na grafično kartico in nanjo pošiljamo vhodne parametre in pridobivamo rezultate izračunov. V današnjem času se je dobro razvilo splošno namensko računanje na grafičnih procesnih enotah (GPGPU - angl. General Purpose computation on GPUs). V namen znanstvenih raziskav so nekatere grafične kartice posebej prirejene, na primer dve NVidiini seriji Tesla in Quaddro ter AMDjeva serija FirePro.
Senčilniki
Senčilniki, ki jih predstavljamo v tem članku, so programi, napisani v namenskem jeziku GLSL (nekoliko podobnem programskemu jeziku C) z omejenim naborom ukazov, specializiranih za preračunavanje lastnosti pri računalniški grafiki. S senčilniki opisujemo želene spremembe lastnosti osnovnih gradnikov – vozlišč ali slikovnih pik. Lastnosti, ki jih želimo modelirati – položaj v prostoru, barva, teksture in druge – lahko opišemo v obliki spremembe vrednosti za posamezno vozlišče ali slikovno piko. Ker modeliramo dve različni vrsti gradnikov, ločujemo na senčilnike za vozlišča (angl. vertex shaders) in senčilnike za fragmente slike (angl. fragment shaders). Včasih se ti senčilniki izvajajo tudi za posamezne slikovne pike. V tem primeru jih imenujemo senčilniki slikovnih pik (angl. pixel shaders). Pred kratkim so se pojavili tudi t. i. mozaični senčilniki (angl. tessellation shaders), namenjeni dodajanju podrobnosti na ploskve geometrije. Novejša različica senčilnikov ponuja podporo generiranju geometrije (angl. geometry shaders), s katerimi lahko preoblikujemo ali generiramo novo geometrijo kar na grafični kartici. Prav tako se senčilnike za geometrijo aktivno uporablja pri raziskovalnih izračunih.
Glavni razlog za uporabo senčilnikov je značilno visoka stopnja možnosti paralelizacije problemov, ki jih modeliramo. Pri naključnem obarvanju dima, ki se vali izza avtomobila v sivinskem odtenku, vrednost za posamezen delec ni odvisna od preostale množice. Takšno modifikacijo lahko procesiramo vzporedno. Za to nalogo imajo grafične kartice množico enostavnih procesorjev, namenjenih procesiranju senčilnikov.
Oglejmo si pot naših geometrijskih likov in teles od nalaganja posameznih vozlišč v medpomnilnik do izrisa modelov na zaslonu.
Na sliki 3 je orisano zaporedje delovanja senčilnikov. Za nas posebej pomembna sta prvi in zadnji, ki si ju bomo tudi podrobneje ogledali. Sprva uporabimo senčilnik vozlišč za izračun položaja vozlišč predmeta v 2D projekcijski ravnini. Na koncu uporabimo senčilnik slikovnih pik za obarvanje posameznih slikovnih pik.
Slika 3: Shematski prikaz zaporedja izvedbe senčilnikov
Senčilnik je z vidika programerja sam zase program, napisan v programskem jeziku GLSL. Poleg slednjega omenimo še uporabljana jezika HLSL in Cg, ki sta prav tako visokonivojska jezika za programiranje senčilnikov. Jezik GLSL močno spominja na programski jezik C, na katerem temelji. Izvajanje senčilnika se prične v metodi main, kjer v programu globalno definiranim spremenljivkam določimo želene novo izračunane vrednosti. Izvedba senčilnika poteka z nalaganjem programa v obliki programske kode na grafično kartico. Vsak izdelovalec v gonilnik grafične kartice vgradi lasten prevajalnik, to ima več pozitivnih posledic: koda je prenosljiva tako med različnimi platformami kot tudi med različno strojno opremo; prevedba programa je s specifičnim gonilnikom lahko optimizirana za posamezno grafično kartico glede na njene lastnosti.
Oglejmo si zgled enostavnega senčilnika za vozlišča.
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;
void main() {
gl_Position = vPosition * uMVPMatrix;
}
Programska koda tega zgleda pravzaprav glede na prejšnje stanje ne izvede ničesar posebnega za posamezno slikovno piko. Slednjo transformira s projekcijsko matriko in vrednost shrani v spremenljivko gl_Position.
Ko želimo natančneje modelirati dele med posameznimi vozlišči, uporabimo senčilnik za fragmente, ki omogoča modeliranje prehodov med vozlišči. Senčilnik fragmentov se izvaja nad posameznimi slikovnimi pikani in z njim ne vplivamo na vozlišča, kot smo lahko storili pri senčilniku vozlišč. V senčilniku fragmentov lahko pridobimo barvne vrednosti posamezne slikovne pike in z njimi manipuliramo. Senčilnik fragmentov tako poskrbi za končni videz na zaslon izrisane slike.
V prejšnjih člankih smo za barvanje površin uporabljali medpomnilnik za barve, ki smo jih določili za posamezna vozlišča. Oglejmo si enostaven senčilnik fragmentov, ki bo enako obarval površino, nato pa ga razširimo za izrisovanje prelivov po odtenkih barve.
void main(void) {
gl_FragColor = vec4(1,1,1,1);
}
Zgornji zgled prikazuje določanje bele barve vsaki posamezni slikovni piki. Seveda lahko uporabimo tudi informacijo o mestu izrisa slikovne pike in tako prilagodimo obarvanje v prelive. Oglejmo si zgled naprednejšega senčilnika, ki izrablja informacijo o lokaciji izrisa slikovne pike za izračun njene barve.
precision mediump float;
uniform vec4 vColor;
void main(void) {
float val = sin(gl_FragCoord.x / 50.0);
val = (val / 2.0) + 0.5;
val *= 0.5;
gl_FragColor = vec4(0.2*(1.0-val),val,0.2*val,1.0);
}
Senčilnik v spremenljivko gl_FragColor shrani novo izračunani štiridimenzionalni vektor, ki ponazarja barvo v treh komponentah (vrednosti barve so med nič in ena) in prosojnost. Komponente izračunamo s pomočjo spremenljivke glFragCoord, ki hrani koordinate slikovne pike na zaslonu. Pri tem uporabimo sinusno funkcijo, ki je del knjižnice GLSL.
Modeliranje s senčilniki deluje sila enostavno. Težje je modeliranje naprednejših učinkov, ki zahteva napredno znanje matematike. Oglejmo si vključitev senčilnika v delovanje programa. Zgoraj opisani senčilnik smo uporabili pri izrisu enostavnih predmetov – kvadrata in štiristrane piramide (slika 4). Ker lika barvamo glede na koordinate in s pomočjo funkcije sinus, se barvni odtenki periodično ponavljajo. Če bi piramido pomaknili proti desni, bi se barvi obeh predmetov ujemali po vodoravni osi. Slednje izhaja iz uporabe spremenljivke gl_FragCoord, ki vsebuje koordinate fragmenta.
Slika 4: Piramida in kvadrat imata enake barvne prelive, saj smo jima dodelili isti senčilnik.
V izrisovalnik bomo implementirali metodo loadShader, ki bo poskrbela za prevajanje senčilnika in nalaganje na grafično kartico. Senčilnik je v programski kodi še vedno v neprevedeni obliki, klic prevedbe zato opravimo ročno pred želeno rabo.
public static int loadShader(int type, String shaderCode){
// referenca na senčilnik
int shader = GLES20.glCreateShader(type);
// pripenjanje izvorne kode, ki je
// shranjena v obliki niza
GLES20.glShaderSource(shader, shaderCode);
// sprožimo prevajanje senčilnika
GLES20.glCompileShader(shader);
return shader;
}
Prva vrstica metode ustvari neprazen predmet za senčilnik in vrne referenco na predmet v pomnilniku, ki ga shranimo v spremenljivko shader. Druga vrstica izvede nalaganje izvirne programske kode senčilnika na lokacijo predhodno ustvarjenega predmeta, na koncu pa kodo, naloženo v predmet, prevedemo in vrnemo referenco senčilnika.
V konstruktorju posameznega razreda lika ali drugega grafičnega objekta pokličemo omenjeno metodo. Nato ustvarimo nov prazen program GL, ki rabi kot baza za pripenjanje različnih senčilnikov.
// ustvarimo nov GL program
mProgram = GLES20.glCreateProgram();
// pripnemo senčilnik vozlišč
GLES20.glAttachShader(mProgram, vertexShader);
// pripenemo senčilnik fragmentov
GLES20.glAttachShader(mProgram, fragmentShader);
// GL program povežemo
GLES20.glLinkProgram(mProgram);
Programu smo pripeli senčilnika vozlišč in slikovnih pik, nato pa poklicali povezovalnik in povezali naloženo izvršljivo kodo. Treba je razširiti metodo draw in v njej poskrbeti za podatke, ki jih želimo uporabiti v posameznem senčilniku. Po nastavljenih atributih moramo v pravilnem vrstnem redu izrisati vozlišča, kot smo počeli prej.
Seveda ni nujno, da senčilnik deluje brezhibno. Zato je dobro preverjati rezultat metode glGetError, ki vrača zaporedno številko napake. Iskanje napak v praksi večinoma uporabljamo pri preverjanju delovanja senčilnikov, z začetnimi težavami pa se lahko srečamo s problemi podpore različnih zmogljivosti sistema GL, ki je odvisna od strojne opreme.
Senčilniki so v zadnjem desetletju edinstveno spremenili zabavno industrijo. Razvijalci iger niso edini, ki uporabljajo senčilnike za vizualne učinke. Tudi produkcija risanih filmov uporablja nove tehnologije, zato danes prevladujejo animirani filmi, ki temeljijo na modeliranju 3D predmetov. Še pred desetletji smo spremljali Disneyjeve filme v risani tehniki.
Slika 5: Zgled vizualizacije scenskega grafa, ki prikazuje hierarhično odvisnost predmetov.
3D prizor
V naših enostavnih zgledih smo v 3D prostor predmete postavljali neodvisno glede na druge predmete. Ko pričnemo ustvarjati večje 3D prostore ali celo svetove, se je treba zavedati, da predmeti v prostoru le niso povsem neodvisni med seboj, temveč jih je dobro zaradi optimizacije izrisa združevati v hierarhično strukturo. V takšnih primerih se dostikrat za pomoč pri organizaciji uporablja skupek prodatkovnih struktur, imenovanih scenski graf. Scenski graf predstavlja hierarhično organizirano podatkovno strukturo za hranjenje predmetov v 3D prizoru.
Scenski graf se največkrat uporablja prav za hranjenje položajev posameznih predmetov v prizoru in za določanje povezav med njimi. Definiramo lahko, kateri predmeti pripadajo kateremu prostoru (npr. v določeni sobi so določeni predmeti: omare, mize, posoda …, spet v drugi sobi pa so drugi predmeti: orožje, orodje, seno). S tem lahko preprosto omejimo tudi izrisovanje zgolj tistih predmetov, ki jih v prizoru trenutno vidimo (npr. predmete ene sobe), s čimer poskrbimo, da ne prihaja do izrisa nepotrebnih elementov, in zato bolj optimalno izkoristimo razpoložljive vire. Podobno kot pri samem izrisu lahko procesorski čas prihranimo tudi pri drugih stvareh. Na primer: ni treba, da preračunavamo položaje nasprotnikov, ki so od nas zelo oddaljeni. Prav tako ni treba izvajati simulacij sistemov delcev v oddaljenih delih okolja, ki zaradi oddaljenosti niso vidni. Za oddaljeno okolje tudi ni treba preračunavati fizike.
Najbolj znana in tudi brezplačna programska knjižnica za hranjenje definicije 3D prizora je OpenSceneGraph (www.openscenegraph.org/). Knjižnica je že nekaj časa na voljo tudi za mobilne naprave in je namenjena reševanju opisanega problema. Uporaba napredne knjižnice za definicijo 3D prizora in za definiranje povezav in relacij med posameznimi predmeti v 3D prostoru je pri razvoju kompleksnejše igrice nujna. Večje skupine razvijalcev včasih razvijajo lastne knjižnice s podobnimi funkcionalnostmi, ki so prilagojene za uporabo v njihovih izdelkih. Da bi se izognili dodatnemu strošku, ki ga predstavlja razvoj takšne knjižnice, razvijalci velikokrat uporabijo stvari, ki so že na razpolago in so jih za podobno uporabo razvili in za uporabo objavili drugi razvijalci.
Fizika v igrah
Večina računalniških iger do neke mere upošteva fizikalne zakone, ki vladajo v naravi. Če želimo, da se predmeti igri temu primerno vedejo, je treba fizikalne zakone opisati oz. implementirati v programski kodi. Mnogokrat želimo simulirati fizikalne zakone realnosti. V ta namen so razvijalci razvili t. i. fizikalne pogone. Fizikalni pogon je knjižnica, ki vsebuje implementacijo delovanja fizikalnih zakonov v računalniškem okolju. Z uporabo fizikalnega pogona lahko definiramo, kako naj se določene stvari v okolju obnašajo: kakšna naj bo sila težnosti, kaj naj se zgodi ob trku predmetov, kakšne lastnosti imajo določene površine ipd.
Slika 6: Prikaz delovanja fizikalnega pogona na zgradbah, sestavljenih iz kock. Vsaka kocka ob rušenju realistično interaktira s svetom.
Zgled brezplačne knjižnice, ki je uporabljena tudi v veliko igrah večjih podjetij, je knjižnica BulletPhysics (bulletphysics.org/). Ta je od nedavna na voljo tudi za mobilne platforme, med katerimi je tudi platforma Android. Fizikalni izračuni so velikokrat zelo zahtevni in zahtevajo kar velik del procesorskega časa. Delovanje knjižnice na platformi Android je zaradi izrabe funkcionalnosti paketa Android NDK, ki podpira izvajanje aplikacij v domorodni kodi, dosti hitrejše, to pa pomeni, da je knjižnica dosti bolj uporabna, saj lahko tako večji del procesorskega časa namenimo drugim stvarem ali preračunamo obnašanje večjega števila predmetov.
• • •
Tokrat smo na kratko predstavili več pomembnih prvin pri razvoju 3D računalniških iger. Senčilniki, scenski graf in fizikalni pogoni so se iz njihove osnovne platforme – osebnih računalnikov – že pred časom razširili tudi na igralne konzole, vedno bolj pa njihova uporaba prodira tudi na področje mobilnih naprav, ki je bilo predvsem zaradi manjših zmogljivosti nekoliko zapostavljeno področje rabe. Vedno več uporabnih knjižnic, kot sta OpenSceneGraph in BulletPhysics, je že na voljo tudi za mobilne platforme, kar omogoča razvoj računalniških iger še večjemu krogu razvijalcev. Prav zaradi takih knjižnic se lahko vedno več manjših razvijalskih skupin poda v razvoj mobilnih iger.
V prihodnjem članku se bomo lotili razvoja 3D igre z uporabo višjenivojskega programskega ogrodja. Izpostavili bomo prednosti uporabe in poteka razvoja igre z ogrodjem, ki bo zaradi vgrajenih funkcionalnosti precej lažji od doslej prikazanega pristopa.