Android i OpenGL

Android i OpenGL

offline
  • Srđan Tot
  • Am I evil? I am man, yes I am.
  • Pridružio: 12 Jul 2005
  • Poruke: 2483
  • Gde živiš: Ljubljana

Napisano: 05 Feb 2012 20:38

Korišćenje OpenGL biblioteke na Androidu se ne razlikuje mnogo od načina na kojem se koristi na računarima. Za sada (posle par meseci igranja), jedina razlika je način kreiranja "prozora" i obrada poruka, ali čak se ni to ne razlikuje previše.

Pre nego što počnemo da pišemo programčiće, potrebno je podesiti razvojno okruženje.

Prvi korak je instalacija Android SDK. Instalacija je prilično jednostavna, dovoljno je raspakovati arhivu (za Windows postoji program za instalaciju) i pokrenuti SDK Manager koji će instalirati potrebne fajlove.
Android SDK: http://developer.android.com/sdk/index.html

Drugi korak je instalacija Eclipse (za Android se preporučuje Classic verzija). Instalacija Eclipse se takođe svodi na raspakivanje arhive.
Eclipse: http://www.eclipse.org/downloads/

Treći korak je instalacija ADT dodatka za Eclipse koji omogućava korišćenje alata za Android iz Eclipse. Kompletno uputstvo za instalaciju se nalazi na ADT Plugin sajtu.
ADT Plugin: http://developer.android.com/sdk/eclipse-adt.html

Za kraj nam ostaje još pripremanje jednog virtualnog uređaja za testiranje aplikacija. Android SDK sadrži program AVD Manager koji se za to koristi. Pri kreiranju je dovoljno upisati ime uređaja, izabrati verziju Androida (za sada je verzija 1.5 sasvim dovoljna), i upisati veličinu virtualne SD kartice (512MB je sasvim dovoljno za testiranje).

Za one koji do sad nisu nikad pravili programe za Android bi sad bio pravi momenat da malo pogledaju od čega se sve sastoji klasična Android aplikacija: http://developer.android.com/guide/topics/fundamentals.html

Za početak ćemo napraviti programčić koji će pripremiti OpenGL, pravilno reagovati na standardne Android događaje (pokretanje, pauza, nastavljanje, gašenje,...) i "nacrtati" prazan plavičasti prozor. Nov projekat kreiramo tako što izaberemo File->New->Project... i zatim, na levoj strani, izaberemo Android Project. Zatim sledi unos osnovnih parametara aplikacije... ime, izbor verzije Androida (za sad ćemo uzeti 1.5), ime paketa i glavne activity klase, kao i minimalna verzija SDKa (za Android 1.5 je to verzija 3). Prilikom kreiranja nove aplikacije nam Eclipse kreira par standardnih datoteka koje su u našem slučaju nepotrebne, pa ćemo ih zato obrisati/popraviti. Prva je main.xml (izgled osnovnog prozora u slučaju da ne koristimo OpenGL), tu datoteku možemo slobodno da obrišemo. Sledeća je strings.xml u kojoj su definisani tekstovi koji se koriste u aplikaciji. Tekst app_name ćemo ostaviti jer on sadrži ime aplikacije, ali nam tekst hello ne treba... njega brišemo. Još dve datoteke će vas možda zanimati, ako ne sada, onda sigurno kasnije. To su AndroidManifest.xml (sadrži informacije o aplikaciji) i ic_launcher.png (sadrži ikonu aplikacije), ali ih sada nećemo menjati.

Pre nego što se bacimo na programiranje, trebalo bi u kratkim crtama objasniti kako će aplikacija izgledati. Prvo što ćemo imati je klasa koja definiše osnovni activity i koja će se pokrenuti na početku aplikacije. Ona će kreirati view koji će, za razliku od standardnih aplikacija, biti klasa koja nasleđuje GLSurfaceView. Ta klasa će pripremiti OpenGL i pozivati funkcije za crtanje. Same funkcije za crtanje će biti definisane u posebnoj klasi. Ukratko, imaćemo 3 klase: MainActivity, SceneView i SceneRenderer.

Da krenemo s programiranjem. Prva na vrsti je MainActivity klasa:
public class MainActivity extends Activity {    private SceneView glView = null;        @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         this.requestWindowFeature(Window.FEATURE_NO_TITLE);         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);         glView = new SceneView(this);         setContentView(glView);     }     @Override     protected void onPause() {         super.onPause();         glView.onPause();     }     @Override     protected void onResume() {         super.onResume();         glView.onResume();     }     }
onCreate funkcija se poziva na početku programa i zadužena je za pripremanje prozora. Igre se na računaru uglavnom pokreću u "fullscreen" modu. Na Androidu postoji sličan mod, tj. moguće je isključiti ispis imena programa i traku sa notifikacijama. Ime programa se isključuje pozivom funkcije requestWindowFeature(Window.FEATURE_NO_TITLE), a traka sa notifikacijama funkcijom getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN). Na kraju nam ostaje još kreiranje klase za prikaz OpenGL sadržaja.
onPause i onResume funkcije su vrlo bitne jer bez njih se OpenGL prozor ne bi pravilno odzivao na sistemske događaje... u našem slučaju će aplikacija prestati s crtanjem kada se druga aplikacija pokrene, i nastaviti kada naša aplikacija ponovo dođe na vrh.

SceneView klasa je zadužena za pripremanje OpenGL i za pozivanje funkcija za crtanje u pravom trenutku:
public class SceneView extends GLSurfaceView {        public SceneView(Context context) {         super(context);         setRenderer(new SceneRenderer());     } }
Većinu posla obavlja klasa GLSurfaceView koju nasleđujemo. Naš posao je samo da kažemo koja klasa će vršiti crtanje.

Zadnje što ostaje je SceneRenderer klasa koja implementira funkcije definisane u Renderer interfejsu:
public class SceneRenderer implements Renderer {    @Override    public void onDrawFrame(GL10 gl) {       gl.glClear(GL10.GL_COLOR_BUFFER_BIT);    }    @Override    public void onSurfaceChanged(GL10 gl, int width, int height) {       gl.glViewport(0, 0, width, height);    }    @Override    public void onSurfaceCreated(GL10 gl, EGLConfig config) {       gl.glClearColor(0.2f, 0.1f, 0.8f, 1.0f);    } }
Ove 3 funkcije su dovoljne za pravilno crtanje. onSurfaceCreated se poziva kada je OpenGL spreman za crtanje i u njoj postavljamo parametre koje moramo postaviti odmah na početku, pre crtanja. Funkcija onSurfaceChanged se poziva na početku i svaki put kada se veličina prozora promeni (recimo, kada igrač okrene telefon za 90 stepeni) i u njoj se uglavnom postavljaju dimenzije za crtanje i matrice. Zadnja funkcija je onDrawFrame u kojoj pišemo kod za crtanje. Ona se poziva za svaki frejm i trebalo bi da se završi što je pre moguće (u njoj nema smisla učitavati slike i raditi slične stvari koje je moguće odraditi u onSurfaceCreated ili onSurfaceChanged).
Verovatno ste primetili da svaka funkcija kao prvi parametar ima GL10 gl. To je interfejs koji nam omogućava zvanje OpenGL funkcija... postoje i drugi načini, ali nam je ovaj za početak dovoljan.

I to je to... najjednostavnija OpenGL aplikacija za Android.
Kod aplikacije možete preuzeti ovde: https://www.mycity.rs/must-login.png

Dopuna: 06 Feb 2012 21:11

Prazan ekran nije baš zanimljiv, zato ćemo ovog put nacrtati nešto. Za početak ćemo nacrtati samo jedan trougao... ništa posebno, ali uzevši u obzir da se na kraju krajeva sve crta iz manje ili više trouglova, ovaj sistem možemo iskoristiti i za crtanje kompleksnijih objekata.

Ovog puta ćemo promeniti samo klasu za crtanje. Prvo što moramo da uradimo je da prilikom kreiranja pripremimo niz koji opisuje koordinate i boje našeg trougla:
public class SceneRenderer implements Renderer {    private ByteBuffer glVertices;    private final int positionSize = 3 * Float.SIZE / 8;    private final int colorSize = 4 * Float.SIZE / 8;    private final int vertexSize = positionSize + colorSize;     @Override    public void onSurfaceCreated(GL10 gl, EGLConfig config) {       gl.glDisable(GL10.GL_LIGHTING);       gl.glClearColor(0.2f, 0.1f, 0.8f, 1.0f);       glVertices = ByteBuffer.allocateDirect(3 * vertexSize).order(ByteOrder.nativeOrder());       glVertices.asFloatBuffer().put(new float[] {          1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,          0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,          -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f       });    } }
Kao što vidite, priprema niza je prilično jednostavna. Na početku smo definisali veličine svake komponente i celog vertexa da bi nam kasnije bilo lakše. Te podatke smo iskoristili za pripremanje novog buffera i u njega upisali koordinate i boje (OpenGL očekuje podatke u istom formatu kao i na računarima). U našem slučaju smo upisali 3 vertexa koji sadrže X, Y i Z koordinatu, i R, G, B i A komponente boje.

Pošto ćemo ovog puta nešto zaista crtati, moramo pravilno postaviti i matrice:
public class SceneRenderer implements Renderer {    @Override    public void onSurfaceChanged(GL10 gl, int width, int height) {       gl.glViewport(0, 0, width, height);              gl.glMatrixMode(GL10.GL_PROJECTION);       gl.glLoadIdentity();       GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 1000.0f);              gl.glMatrixMode(GL10.GL_MODELVIEW);       gl.glLoadIdentity();       GLU.gluLookAt(gl, 0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);    } }
Taj deo je potpuno isti kao i kad bi na računaru postavljali matrice. U ovom slučaju smo postavili matricu projekcije na standardnu perspektivu, a kameru smo postavili 5 jedinica ispred trougla (trougao ima Z koordinatu postavljenu na 0). To je dovoljno da se trougao u celoti nacrta na ekranu.

Za kraj ostaje samo crtanje:
public class SceneRenderer implements Renderer {    @Override    public void onDrawFrame(GL10 gl) {       gl.glClear(GL10.GL_COLOR_BUFFER_BIT);              glVertices.position(0);       gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);       gl.glVertexPointer(3, GL10.GL_FLOAT, vertexSize, glVertices);       glVertices.position(positionSize);       gl.glEnableClientState(GL10.GL_COLOR_ARRAY);       gl.glColorPointer(4, GL10.GL_FLOAT, vertexSize, glVertices);              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 3);              gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);       gl.glDisableClientState(GL10.GL_COLOR_ARRAY);    } }
I opet, oni koji su koristili OpenGL na računarima će videti da ni ovde nema razlike u pisanju koda. Na početku obaveštavamo OpenGL da ćemo koristiti niz za definisanje koordinata vertexa, i zatim mu dajemo niz i par informacija da bi znao pravilno da pročita podatke iz njega. gl.glVertexPointer(3, GL10.GL_FLOAT, vertexSize, glVertices) znači da su koordinate definisane u 3 float vrednosti, da je veličina celog vertexa vertexSize (tu vrednost smo na početku postavili) i da je niz u glVertices. Nakon toga uradimo isto i za boje, s tim da buffer pomerimo napred da pokazuje na prvu boju u nizu.
OpenGL je posle toga spreman za crtanje i funkcijom gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 3) mu kažemo da nacrta trouglove od prvog elementa iz niza (prvi element je na poziciji 0) i da upotrebi 3 vertexa (toliko je potrebno za 1 trougao).
Za kraj jednostavno kažemo da smo završili i da OpenGL može da zaboravi na podatke koje smo mu postavili pre crtanja.
U našem slučaju smo podatke za crtanje mogli postaviti već u onSurfaceCreated funkciji i da ovde samo crtamo, što bi bilo optimalnije rešenje, ali se ovako lepše vidi kako ceo proces teče.

Ovo je rezulata našeg koda:


Kod aplikacije možete preuzeti ovde: https://www.mycity.rs/must-login.png

Dopuna: 07 Feb 2012 23:12

A sada... sličice Smile
Kod pripremanja tekstura za Android treba obratiti pažnju na dimenzije slike. Veliki broj telefona ne ume da radi sa slikama čije dimenzije nisu "power of 2", tj. 2 na nešto. Za ovaj primer sam namerno izabrao logo MyCity foruma koji ima dimenzije 245x110 i zbog toga nije savršen za nas. Rešenje je prilično jednostavno, u sliku koja ima prvu pogodnu veću dimenziju postavimo našu sliku u gornji levi ugao. U ovom slučaju logo bi stavili u sliku veličine 256x128.

Pošto ćemo našu sliku staviti u resurse programa, trebaće nam jedan dodatan podatak prilikom kreiranja klase za crtanje, odnosno klase koja će učitavati sliku iz resursa. Taj podatak je kontekst koji je zadužen za čitanje fajlova iz paketa aplikacije. Zbog toga moramo napraviti 2 izmene, jednu u SceneView i jednu u SceneRenderer klasi:
public class SceneRenderer implements Renderer {        private Context context;    public SceneRenderer(Context context) {       this.context = context;    } } public class SceneView extends GLSurfaceView {    public SceneView(Context context) {         super(context);         setRenderer(new SceneRenderer(context));     } }

U SceneRenderer ćemo dodati još jednu funkciju koja zna da učita sliku iz bilo kojeg InputStreama i prebaci je u OpenGL teksturu:
public class SceneRenderer implements Renderer {        private int loadTexture(GL10 gl, InputStream stream) {       final int[] textures = new int[1];       gl.glGenTextures(1, textures, 0);       gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);       gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);             gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);       GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, BitmapFactory.decodeStream(stream), 0);       return textures[0];    } }
Kao što se vidi iz koda, tekstura se kreira na standardan način... jedina zanimljiva funkcija je GLUtils.texImage2D koja zna da napuni trenutno izabranu teksturom podacima iz slike koju joj prosledimo.

Imamo sve što nam je potrebno da učitamo i prikažemo sliku, ostaje nam još samo da pripremimo vertexe koji će nacrtati pravougaonik i pravilno mapirati teksturu:
public class SceneRenderer implements Renderer {        private ByteBuffer glVertices;    private final int positionSize = 3 * Float.SIZE / 8;    private final int texCoordSize = 2 * Float.SIZE / 8;    private final int vertexSize = positionSize + texCoordSize;    private int glTexture;    private final int imgWidth = 245;    private final int imgHeight = 110;    private final int texWidth = 256;    private final int texHeight = 128;        @Override    public void onDrawFrame(GL10 gl) {       gl.glClear(GL10.GL_COLOR_BUFFER_BIT);              glVertices.position(0);       gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);       gl.glVertexPointer(3, GL10.GL_FLOAT, vertexSize, glVertices);       glVertices.position(positionSize);       gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);       gl.glTexCoordPointer(2, GL10.GL_FLOAT, vertexSize, glVertices);              gl.glEnable(GL10.GL_TEXTURE_2D);       gl.glBindTexture(GL10.GL_TEXTURE_2D, glTexture);              gl.glPushMatrix();       gl.glRotatef((float)(SystemClock.uptimeMillis()) / 10.0f, 0.0f, 1.0f, 0.0f);              gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);              gl.glPopMatrix();              gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);       gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);       gl.glDisable(GL10.GL_TEXTURE_2D);    }    @Override    public void onSurfaceCreated(GL10 gl, EGLConfig config) {       gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);       gl.glEnable(GL10.GL_BLEND);       gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);       gl.glDisable(GL10.GL_LIGHTING);       gl.glClearColor(0.2f, 0.1f, 0.8f, 1.0f);              final float maxXCoord = (float)imgWidth / (float)texWidth;       final float maxYCoord = (float)imgHeight / (float)texHeight;       final float aspect = (float)imgHeight / (float)imgWidth;       glVertices = ByteBuffer.allocateDirect(4 * vertexSize).order(ByteOrder.nativeOrder());       glVertices.asFloatBuffer().put(new float[] {          1.0f, -aspect, 0.0f, maxXCoord, maxYCoord,          1.0f, aspect, 0.0f, maxXCoord, 0.0f,          -1.0f, -aspect, 0.0f, 0.0f, maxYCoord,          -1.0f, aspect, 0.0f, 0.0f, 0.0f       });              glTexture = loadTexture(gl, context.getResources().openRawResource(R.drawable.logo));    } }
Za razliku od prethodnog primera, u ovom naš vertex sadrži koordinate vertexa i koordinate teksture. Pošto smo na početku napravili malo veću sliku da bi se bolje slagala s OpenGLom, sada moramo obaviti računskih operacija da bi izračunali koji deo teksture nam treba. Uzevši u obzir da znamo koliko je velik deo koji nam treba i veličinu cele slike, lako dobijamo željeni procenat. Da slika ne bude deformisana, računamo i širinu pravougaonika na osnovu veličine slike.

Pri samom crtanju, radimo sve skoro isto kao u prošlom primeru. Kažemo OpenGLu da ćemo korisiti poziciju i koordinate teksture iz vertexa, postavimo buffer, i pošto sada koristimo i teksturu, uključimo crtanje teksture i postavimo je na vrednost koju smo dobili preko funkcije loadTexture.

Neko će se možda zapitati kako OpenGL crta 2 trougla ako smo definisali samo 4 vertexa, a za 1 trougao su potrebna 3. Prilikom crtanja smo rekli da buffer crta kao GL_TRIANGLE_STRIP, što u suštini znači da za prvi trougao iskoristi 3 vertexa, a za svaki sledeći iskoristi jedan nov + dva od prethodnog trougla. Na taj način smanjujemo veličinu buffera i ubrzavamo crtanje... mada, i to je potpuno isto kao pri korišćenju OpenGLa na računaru Smile

Sve u svemu, rezultat ovog koda je:


Kod aplikacije možete preuzeti ovde: https://www.mycity.rs/must-login.png



Registruj se da bi učestvovao u diskusiji. Registrovanim korisnicima se NE prikazuju reklame unutar poruka.
Ko je trenutno na forumu
 

Ukupno su 523 korisnika na forumu :: 2 registrovanih, 0 sakrivenih i 521 gosta   ::   [ Administrator ] [ Supermoderator ] [ Moderator ] :: Detaljnije

Najviše korisnika na forumu ikad bilo je 3028 - dana 22 Nov 2019 07:47

Korisnici koji su trenutno na forumu:
Korisnici trenutno na forumu: miroslav_eric, Profica2