Objavljeno: 18.12.2012 | Avtor: Matevž Pesek | Monitor December 2012 | Teme: android, grafične kartice

Android in 3D grafika

V prvi polovici letošnjega leta smo zgradili 2D računalniško igro. Po krajšem poletnem premoru posegamo po kompleksnejšemu razvoju igre v treh dimenzijah.

Tokrat predstavljamo obilico možnosti mobilnih naprav s platformo Android za razvoj iger, ki počasi briše stereotipe o neuporabnosti mobilnikov in tabličnih računalnikov za razvoj in igranje resnejših iger. Predstavili bomo podprte možnosti platforme in zmožnosti današnjih mobilnih naprav. Pričujoči članek je tudi uvod v novo serijo člankov, v katerih bomo predstavili razvoj preproste 3D računalniške igre.

Še ne dolgo tega smo z navdušenjem spremljali razvoj prvih telefonov z grafičnim zaslonom, ki je omogočal več kot le izpis znakov v nekaj vrsticah. Nastale so prve igre, legendarna Nokiina kača nam vsem vzbudi lepe spomine na telefone, katerih namen je bil predvsem dosegljivost in vzdržljivost. Z leti razvoja in večjimi zasloni smo dočakali prve 3D grafične igre, napisane za platformo JavaME, ki pa zaradi svoje razdrobljenosti ni bila pretirano priljubljena med razvijalci. Šele prihod t. i. "pametnih telefonov" je prinesel na trg mobilnih platform širok razmah industrije razvoja iger. Tudi vaša pametna mobilna naprava podpira 3D grafiko, ki z novimi različicami telefonov postaja vedno bolj impresivna. Da se bomo lažje lotili razvoja 3D računalniške igre, je dobro najprej spoznati nekaj teorije o 3D računalniški grafiki in nekaj podrobnosti o tem, kako se lotiti stvari pri razvoju androidnih aplikacij.

Takole smo si včasih krajšali čas z enostavno igro kače, ki je tekla na mobilnih napravah znamke Nokia.

Mobilna naprava postaja v notranjosti precej podobna domačim namiznim računalnikom. Tako kot v slednjih je tudi v mobilnih napravah v središču centralna procesorska enota, katere takt se v povprečni sodobnejši mobilni napravi giblje okoli enega gigaherca in več. Podobno kot pri Intelovi arhitekturi procesorjev je ARM svoje procesorje oblikoval kot večjedrne. To nam omogoča hkratno izvajanje več procesov in niti. Redni bralci si lahko prikličete v spomin članek iz prejšnjega meseca, ki krajše opiše uporabnost takšnega izvajanja pri pošiljanju slike na spletni strežnik. Poleg hitrosti in števila jeder procesorja smo tako ob nakupu novega mobitela kot računalnika pozorni tudi na količino sistemskega pomnilnika. Ena izmed stvari, na katero pri nakupu mobitela doslej večinoma nismo bili pozorni, je grafični procesor.

V spletu

Vsebina članka je skupaj s programsko kodo dosegljiva na android.monitor.si, tam najdete tudi članke iz prejšnje serije o razvoju 2D igre.

Kaj je ARM

Večini današnjih procesorjev, ki so v mobilnih napravah, pravimo na kratko procesorji ARM oz. procesorji, zasnovani na arhitekturi ARM. Gre za svojevrsten fenomen, ki je računalniškim navdušencem bolj malo znan: procesorje ARM namreč izdeluje več deset svetovno znanih izdelovalcev, kot so AMD, Texas Instruments, LG, Atmel, Apple in še veliko drugih. Načrtuje in trži pa jih malo znano podjetje ARM Holdings iz Velike Britanije, ki ne izdeluje samih procesorjev, temveč trži arhitekturo čipov. Tako lahko Apple izda čip ARM pod lastnim imenom A6, NVidia pa prodaja procesorje ARM pod bolj znanim imenom Tegra.

Nekateri izmed procesorjev imajo poleg centralne tudi grafično procesno enoto, drugi pa za grafične operacije izkoriščajo ločen čip. Tudi tu imamo več možnih rešitev, ki bi se lahko kot na osebnih računalnikih spremenile v težave zaradi množičnosti standardov. Glede na uveljavljene standarde pri osebnih računalnikih tako poznamo grafične knjižnice DirectX in OpenGL. Na področju mobilnih naprav se je v ta namen izoblikovala grafična knjižnica OpenGL ES. Gre za različico knjižnice OpenGL, ki je v osnovi okrnjena in namenjena osebnim računalnikom, platforma Android pa jo podpira že od različice 1.0. S časom je tudi knjižnica dobila posodobitve, ki v zadnji obliki na mobilne naprave prinašajo že skorajda polno funkcionalnost trenutne knjižnice OpenGL.

Seveda pa poleg slednje poznamo tudi konkurenčne oblike izkoriščanja zmogljivosti grafičnih kartic. Kot zgled omenimo NVidiin pristop pri uporabi procesorjev Tegra. Gre za visoko zmogljiv sistem, ki v svojem bistvu zmore vsaj toliko ali več kot povprečna grafična enota s podporo OpenGL ES. Zato so v NVidii začeli razvijati igre, izrecno usmerjene v dodatno izkoriščanje procesorjev Tegra. Seznam takih iger si lahko ogledate na www.tegrazone.com. Če imate napravo Tegra, vam priporočamo, da katero izmed brezplačnih iger tudi preizkusite, saj je grafika nadvse impresivna.

Zgled sistema na čipu (angl. system-on-chip) NVidiinega procesorja Tegra 3, ki združuje v sebi tudi grafični čip.

OpenGL ES

Knjižnica OpenGL ES je brezplačna tako za razvijalce kot za izdelovalce strojne opreme. Nadzor in razvoj prevzema konzorcijska skupina Khronos, ki jo med vidnejšimi predstavniki sestavljajo Intel, NVidia in Silicon Graphics, ta poleg prej omenjene knjižnice razvija še kopico drugih prosto dostopnih knjižnic. Prva različica OpenGL ES 1.0 je povzela specifikacije takratne knjižnice OpenGL 1.3 za osebne računalnike. Z manjšimi izboljšavami je kmalu izšla različica OpenGL ES 1.1 (podprta s sistemom Android 1.6), OpenGL ES 2.0 pa je prinesla kopico novosti, med drugim najpomembnejšo - senčilnike (angl. shaders). To različico podpirajo androidni sistemi od različice 2.0 naprej, pripravljena in od poletja naprej tudi objavljena pa je tudi specifikacija najnovejše različice knjižnice - OpenGL ES 3.0.

V seriji, ki jo pričenjamo s tem člankom, se bomo omejili kar na zadnjo podprto različico - OpenGL ES 2.0. Seveda bo večji del uvodne programske kode z manjšimi popravki deloval tudi na starejših različicah sistema Android. Tokrat si oglejmo enostavno izrisovanje objektov na zaslon mobilne naprave.

Novi vodnik za razvoj 3D grafike bomo začeli z novim projektom Android Eclipse, ki bo imel le eno aktivnost, v našem primeru MainActivity. Metodi onCreate() bomo dodali še dve drugi metodi, onPause() in onResume(). Znotraj metode onCreate() so že vam dobro znane vrstice, ki kličejo istoimensko metodo nadrazreda in metodo za nastavitev pogleda - this.setContentView(). Slednja verjetno vsebuje referenco do datoteke XML, ki je bila avtomatsko ustvarjena z novim projektom. Tokrat bomo na tem mestu dodali drug pogled (View). Najprej ustvarimo pogled tipa GLSurfaceView, nato pa nov upodobljevalnik tipa GLSurfaceView.Renderer. Ker je treba razred Renderer sprva implementirati, si za začetek ustvarimo nov razred tipa GLRenderer, ki razširja razred GLSurfaceView.Renderer. Kodo bomo kasneje nadgradili, zaenkrat le dodajmo metode, ki jih potrebuje razširjeni razred GLRenderer. Namig: implementacija metod nadrazreda se najhitreje opravi z desnim klikom rdeče obarvanega imena razreda, ki ga orodje Eclipse označi zaradi napake. Kot hitri popravek (quick fix) se prikaže možnost "add unimplemented methods", kar je rešitev, ki nam v tem trenutku zadošča. Avtomatsko zgenerirane metode bodo prazne, razen prisotnosti potrebnih povratnih klicev v obliki return <privzeta_povratna_vrednost>. V naši osnovni aktivnosti torej dopolnimo metodo onCreate() na naslednji način:

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //klic metode nadrazreda
glSurfaceView = new GLSurfaceView(this);
//udejanjenje nove GL površine
glSurfaceView.setRenderer(new GLRenderer(this));
this.setContentView(glSurfaceView);
}

Za trenutek se ozrimo še po prej omenjenih metodah onPause() in onResume(). Prvo metodo naše aktivnosti kliče sistem ob dogodku, ko se naša aktivnost ne izrisuje več na zaslonu, temveč je pomaknjena v "ozadje", oziroma je prekrita z izrisom druge aktivnosti. V tem primeru želimo začasno zaustaviti izris naše površine. V definiciji druge metode želimo našo izrisovalno površino znova aktivirati oziroma nadaljevati izris. Kot bomo spoznali malce kasneje, bomo s klicem pavziranja izrisa dosegli začasno zamrznitev stanja na zaslonu. V nasprotnem primeru bi se ob vnovičnem priklicu aplikacije na zaslon položaji izrisanih predmetov bistveno spremenili.

// Metoda se kliče, ko se aplikacija
// pomakne v ozadje
@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
}

// Metoda se kliče, ko se aplikacija
// spet pomakne v ospredje
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}

In aplikacija že deluje. Glede na doslej zapisano kodo se ni zgodilo pravzaprav nič, saj se na mobilni napravi prikaže le črn zaslon. Za lažjo predstavo delovanja površine bomo nanjo izrisali dva lika, kasneje pa dodali še geometrijska telesa in jih pomikali po površini.

Liki in geometrijska telesa

Ustvarimo nov razred Trikotnik, v katerem bomo implementirali opis slednjega v obliki skupine oglišč.

private float[] vertices = {
-0.0f, 0.0f, 0.0f, // levo
2.0f, 0.0f, 0.0f, // desno
0.0f, 2.0f, 0.0f, // zgoraj
};

Poleg oglišč je treba določiti tudi zaporedje slednjih v matematično pozitivni smeri (smer, nasprotna urnemu kazalcu), saj so oglišča le tako vidna na zaslonu.

private byte[] indices = { 0, 1, 2 };

Razredu moramo določiti tudi dva medpomnilnika, enega za oglišča in drugega za vrstni red slednjih.

private FloatBuffer vertexBuff;
private ByteBuffer indexBuff;

V prvega bomo naložili poprej definirana oglišča, z drugim pa podali vrstni red slednjih. Oboje bomo implementirali v konstruktorju razreda trikotnik. Oba medpomnilnika bomo uporabili pri izrisu v metodi draw(), ki jo bomo implementirali v tem razredu.

ByteBuffer byteBuff = ByteBuffer.allocateDirect(vertices.length * 4); //4 bajti na število
vertexBuff = byteBuff.asFloatBuffer(); //zapisali bomo tip float
vertexBuff.put(vertices); //zapis
vertexBuff.position(0); //premik kazalca v medpomnilniku na začetek

//zapis indeksov
indexBuff = ByteBuffer.allocateDirect(indices.length);
indexBuff.put(indices);
indexBuff.position(0);

Zapišimo še metodo draw(), ki bo na vmesnik zapisala definirane lastnosti lika.

public void draw(GL10 gl) {
// definiranje medpomnilnika oglišč
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuff);

// izris primitivnih površin - trikotnikov
gl.glDrawElements(GL10.GL_TRIANGLES, indices.length,
GL10.GL_UNSIGNED_BYTE, indexBuff);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}

Zdaj lahko primerno dopolnimo še razred GLRenderer in si ogledamo svoj prvi rezultat. V konstruktorju definirajmo nov trikotnik v obliki triangle = new Trikotnik();. V metodi onSurfaceCreated() moramo z novo površino primerno določiti osnovne parametre. Določili bomo barvo za izbris ter izbrali mehko senčenje barv. To bo kasneje dajalo lepe učinke na izrisanih in pobarvanih likih.

gl.glClearColor(0.0f, 0.5f, 0.0f, 1.0f); //barva ozadja - zelena
gl.glShadeModel(GL10.GL_SMOOTH); // senčenje barv - mehko

Oglejmo si še metodo onDrawFrame(). Bolj vešč bralec bo tako pri prej omenjenih in nastavljenih parametrih kot pri tej metodi hitro opazil podobnosti z razvojem za osebne računalnike. Nastavili bomo vidno polje, ki naj bo enako veliko kot polje aplikacije. Nato bomo nastavili perspektivno transformacijo, ki bo vsebovala vidni kot površine, ki jo izrisujemo, razmerje zaslona in omejitev izrisa objektov z najbližjo in najbolj oddaljeno razdaljo objekta (angl. near in far plane), ki ga še želimo izrisati.

Ti dve razdalji se uporabljata iz dveh praktičnih razlogov: objektov, ki so zelo oddaljeni, zaradi svoje "majhnosti" ni treba izrisovati, saj so premajhni ali premalo pomembni, da bi jih bilo treba izrisovati na zaslon. Ker takih objektov ne bomo izrisovali, bomo tudi pridobili na hitrosti izrisa preostalih objektov, saj bo grafični procesor manj obremenjen. Ta razdalja je v igrah pogosto nastavljiva in dostopna uporabniku znotraj vmesnika igre, njen namen pa je predvsem povečevati izris števila okvirov na sekundo na zaslon (angl. frame rate). Razdalja bližine, do katere še izrisujemo, je namenjena izpuščanju objektov, ki so kameri preblizu, in bi zakrili večji del slike, in predmete, ki so za kamero in jih ni treba izrisovati. Ko se v igri sprehajamo za človekom - tretjeosebni način pogleda - želimo, da nas stene prostora, v katerem je ta človek, ne ovirajo in ga vidimo tudi na ozkem hodniku, kjer bi stene zakrivale pogled nanj.

gl.glViewport(0, 0, width, height);
// Naložimo projekcijsko matriko
gl.glMatrixMode(GL10.GL_PROJECTION);
// Matriko nastavimo na identiteto
gl.glLoadIdentity();
//nastavimo perspektivo
GLU.gluPerspective(gl, 45, (float)width / height, 0.1f, 100.f);

V metodi onDrawFrame() bomo trikotnik izrisali. Sam izris trikotnika ne daje občutka prostora, zato se bomo poigrali s premikom trikotnika z levega dela zaslona proti desnemu skupaj s približevanjem gledalcu. Spremenljivka progress bo na intervalu med -10 in 5, ob vsakem izrisu slike pa jo bomo povečali za 0,1.

progress+= 0.1;
//če presežemo interval, ponastavimo na osnovno vrednost
if(progress > 5) progress = -10;
gl.glTranslatef(progress, -progress/2, progress - 10);
triangle.draw(gl);

Zagnana aplikacija izriše celozaslonsko okno z zeleno obarvanim ozadjem in belim trikotnikom, ki se periodično premika mimo nas z naše leve proti desni strani zaslona.

Slika 3: Premik trikotnika z leve na desno. Slika je združen prikaz več stanj animacije.

Funkcionalni problem takšne implementacije premika likov po zaslonu je v hitrosti povečevanja spremenljivke progress. Slednja se poveča ob vsakem izrisu nove slike na zaslon, to v praksi daje različne rezultate glede na strojno opremo posamezne naprave. Podoben učinek smo pred desetletji doživljali ob igranju najljubših igric, kjer je veliko iger ob pritisku na tipko "turbo", ki je bila včasih na ohišju računalnika, začelo premikati objekte po zaslonu precej hitreje oziroma počasneje od običajne hitrosti. Tako kot smo pri vodiču za razvoj 2D igre "Space iInvaders" uporabili časovnike, lahko tukaj na podoben način implementiramo glavno zanko igre (angl. game loop) v naši aktivnosti. V zanki aktivnosti MainActivity preračunamo položaje objektov glede na uporabnikovo akcijo in nove položaje shranimo. V razredu GLRenderer namesto uvedene spremenljivke progress uporabimo posodobljene vrednosti posameznih objektov.

Razredu Trikotnik bomo dodali lastnosti posameznih koordinat. Slednje bomo uporabili v razredu MainActivity, kjer bomo izračunali premik trikotnika in v razredu GLRenderer trikotnik v skladu s spremenjenimi vrednostmi spremenljivk prikazali na zaslonu. Trikotniku dodajmo koordinate.

// inicializacija položaja objekta
public float x = 0f;
public float y = 0f;
public float z = 0f;

V razredu MainActivity bomo implementirali metodo gameLoop(), ki se bo s strani časovnika klicala približno 60-krat na sekundo. Implementiramo opravilo - anonimno ustvarimo nov primerek razreda TimerTask() in mu v metodi run podamo klic prej omenjene metode mainLoop().

// Kreiranje anonimnega razreda,
// ki skrbi za poganjanje glavne
// zanke igre
TimerTask poganjanjeZanke = new TimerTask() {
@Override
public void run() {
gameLoop();
}
};

// ustvarimo nov primerek razreda Timer
// in mu v "urnik" dodamo periodično
// izvedbo opravila poganjanjeZanke.
Timer timer = new Timer();
timer.schedule(poganjanjeZanke, 0, 16);

Uvedimo še objekt Piramida, ki bo dodatno popestril animacijo na zaslonu. Tako kot pri trikotniku bomo tudi pri piramidi določili posamezna oglišča geometrijskega telesa, slednjemu pa dodali še barve stranic. Barve bodo definirane za posamezno oglišče, potrebovali pa bomo tudi medpomnilnik za barve.

private float[] barve = {
1.0f, 0.0f, 1.0f, 1.0f, //magenta
0.0f, 1.0f, 1.0f, 1.0f, //cyan
0.0f, 0.0f, 1.0f, 1.0f, //modra
0.0f, 1.0f, 0.0f, 1.0f, //zelena
1.0f, 0.0f, 0.0f, 1.0f //rdeča
};

Metoda draw() v razredu Piramida je skoraj enaka isti metodi v razredu Trikotnik, dodali pa bomo še implementacijo barv.

gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);

Implementirajmo še izračun koordinat. Spremenljivko progress in pripadajočo logiko bomo preselili iz razreda GLRenderer v gameLoop(). Dodali bomo še objekt Piramida, ki se bo pomikal v nasprotno smer. Sledi izračun koordinat.

public void gameLoop(){
progress+= 0.1; //povečaj napredek
if(progress > 5) progress = -20; //ponastavi interval
//nastavi koordinate trikotnika
renderer.triangle.x = progress;
renderer.triangle.x = -progress/2;
renderer.triangle.x = progress - 10;
//nastavi koordinate piramide
renderer.pyramid.x = progress;
renderer.pyramid.x = progress/2;
renderer.pyramid.x = progress - 10;
}

Hitrost premika objektov na zaslonu je zdaj enaka ne glede na napravo. Koordinate preračunamo 60-krat na sekundo, izris pa se izvede približno 30-krat na sekundo. To bo na večini naprav prineslo pospešitev premika. Del prikaza prehoda piramide in lika je viden na sliki 4.

Slika 4: Prehod trikotnika in piramide po zaslonu

V tokratnem članku smo naredili kratek pregled osnovnih zmožnosti in implementacije 3D grafike, ki jo ponuja androidna platforma. Sprva smo si ogledali razlike in podobnosti 3D grafike med mobilnimi napravami in osebnimi računalniki, dotaknili pa smo se tudi strojne opreme mobilnih naprav, ki se tako po zmožnostih kot tudi po modularni zasnovi približuje sodobnim računalnikom. Razložen osnovni cikel 3D aplikacije je le nadgradnja osnovnega življenjskega cikla androidne aplikacije, obenem pa je poleg slednjega tudi veliko drugih v članku predstavljenih osnovnih gradnikov enakih na PC in androidni platformi. Z malce truda smo dodali vse potrebne osnovne elemente za izdelavo 3D igre ob pomoči knjižnice OpenGL ES, ki je podprta na vseh androidnih napravah. Izdelali smo tudi enostavno animacijo objektov, v prihodnjih člankih pa se bomo poglobili v razvoj igre. Prav nam bo prišlo tudi znanje matematike, saj je razvoj iger zelo odvisen predvsem od tega znanja .

Naroči se na redna tedenska ali mesečna obvestila o novih prispevkih na naši spletni strani!

Komentirajo lahko le prijavljeni uporabniki

 
  • Polja označena z * je potrebno obvezno izpolniti
  • Pošlji