Sviluppare un gioco per Android – Lezione 12: visualizzare elementi grafici con OpenGL ES
Android Blog Italia.
Ogni triangolo ha una faccia frontale e una opposta (face e backface), inoltre esso è definito da tre punti nello spazio: i vertici (vertices, vertex al singolare). Nell’immagine di seguito, potete vedere i due vertici A e B.
L’immagine qui sopra è essenziale per capire la differenza tra 2D e 3D. Un vertice, infatti, è definito grazie alle sue 3 coordinate (come B), mentre nel caso di A abbiamo impostato la coordinata z pari a 0. Se tutti i nostri elementi avessero questa caratteristica, allora inizieremmo a parlare di programmazione 2D.
Il triangolo è un oggetto di tipo primitivo, il più semplice che OpenGL riconosce ed è in grado di rappresentare. Naturalmente ce ne sono altri, ma noi ci limiteremo alle basi. Ogni oggetto può essere composto da triangoli. Ritorniamo, ora, alle facce del triangolo in questione.
Si tratta di uno dei concetti essenziali di programmazione 3D, infatti ogni oggetto avrà una faccia rivolta verso di voi, e una nascosta. Quella nascosta non sarà disegnata da OpenGL proprio perché non ce n’è bisogno (backface culling), ma come fa OpenGL a determinare tutto questo? Molto semplice: se l’ordine dei vertici è antiorario, abbiamo quella che nel gergo, come soprascritto, viene chiamata face, altrimenti si ha a che fare con una backface. Questo meccanismo è di default, ma può essere modificato. Ad ogni modo, l’immagine qui di seguito vi chiarirà le idee (il triangolo rosso non verrà disegnato).
Visualizzare elementi grafici: creare e disegnare un triangolo
A questo punto, abbiamo concluso con la teoria e ciò che ci resta da fare riguarda la creazione e il disegno del triangolo.
Come abbiamo già detto, il triangolo è definito da tre vertici e le rispettive coordinate non vengono misurate in pixel. Useremo il tipo float per rappresentare i valori delle coordinate, le quali saranno l’una relativa all’altra. Ad esempio, se due lati sono lunghi rispettivamente 1.0f e 0.5f, implichiamo che la lunghezza del secondo è l’esatta metà di quella del primo.
Per quanto riguarda la grandezza del triangolo, essa dipenderà dalla viewport che utilizzeremo. Immaginate quest’ultima come una fotocamera che, nel caso di grafica 2D, è ortogonale allo schermo. Se la fotocamera è vicina, il triangolo apparirà grande, ma più si allontana più esso rimpicciolirà. A questo punto, creiamo la classe Triangle.java:
package com.example.lezioneopengl_androidblog; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import javax.microedition.khronos.opengles.GL10; public class Triangle { private FloatBuffer vertexBuffer; // buffer contenente i vertici private float vertices[] = { -0.5f, -0.5f, 0.0f, // V1 - primo vertice (x,y,z) 0.5f, -0.5f, 0.0f, // V2 - secondo vertice 0.0f, 0.5f, 0.0f // V3 - terzo vertice }; public Triangle() { // il tipo float ha 4 bytes quindi allochiamo per ogni coordinata 4 bytes ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * 4); vertexByteBuffer.order(ByteOrder.nativeOrder()); // allochiamo la memoria dal byte buffer vertexBuffer = vertexByteBuffer.asFloatBuffer(); // rempiamo vertexBuffer con i vertici vertexBuffer.put(vertices); // settiamo la posizione del puntatore all'inizio del buffer vertexBuffer.position(0); } public void draw(GL10 gl) { gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // settiamo il colore di background // gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // per mostrare il colore // gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // settiamo il colore per il triangolo gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f); // puntiamo i nostri vertici gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); // Disegniamo i vertici come TRIANGLE_STRIP gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3); gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); } }
Al rigo 11 dichiariamo il FloatBuffer che otterrà i vertici del nostro triangolo. Questi ultimi sono contenuti nell’array vertices[]. Potete capire meglio come verrà rappresentato il triangolo dall’immagine qui di seguito:
Nel costruttore, inizializziamo il triangolo, riempiendo il FloatBuffer con i vertici e impostando la posizione del puntatore all’inizio. Ora diamo un’occhiata al metodo draw().
Poiché noi memorizziamo le coordinate dei vertici in un FloatBuffer dobbiamo abilitare OpenGL affinché esso possa leggerle e capire che si trovano lì. Al rigo 37 facciamo proprio questo. Al rigo 45, invece, impostiamo un colore per il triangolo che sarà visualizzato.
Con glVertexPointer informiamo OpenGL che deve utilizzare vertexBuffer per estrarre i vertici e gli passiamo, come parametro iniziale, il numero di coordinate che saranno utilizzate per ogni vertice. Il secondo parametro, invece, fa capire ad OpenGL che tipo di dati sono contenuti nel buffer, mentre il terzo altro non è che l’offset nell’array utilizzato per i vertici.
Infine, utilizziamo glDrawArrays per disegnare la nostra entità come un triangolo, a partire dal primo elemento trovato nel buffer. Diamo, un’occhiata, prima di concludere, ai cambiamenti apportati al nostro renderer:
public class GlRenderer implements Renderer { private Triangle triangle; // il triangolo da disegnare public GlRenderer() { this.triangle = new Triangle(); } @Override public void onDrawFrame(GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // resetta la Modelview Matrix gl.glLoadIdentity(); // Disegno gl.glTranslatef(0.0f, 0.0f, -5.0f); // muove di 5 unità nello schermo // gl.glScalef(0.5f, 0.5f, 0.5f); // scala il triangolo del 50% // nel caso sia troppo grande triangle.draw(gl); // disegna il triangolo } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { if(height == 0) { //controlliamo la divisione per 0 height = 1; } gl.glViewport(0, 0, width, height); //resettiamo la Viewport corrente gl.glMatrixMode(GL10.GL_PROJECTION); //Selezioniamo la Projection Matrix gl.glLoadIdentity(); //Resettiamo la Projection Matrix //Calcoliamo l'Aspect Ratio dello schermo GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f); gl.glMatrixMode(GL10.GL_MODELVIEW); //Selezioniamo la Modelview Matrix gl.glLoadIdentity(); //Resettiamo la Modelview Matrix } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { // TODO Auto-generated method stub } }
Concentriamoci su OnDrawFrame(). Dovete sapere che OpenGL funziona con variabili di stato e, come potete vedere, ogni metodo cambia il contesto interno. Ogni volta che un frame viene disegnato, il buffer ottenuto viene eliminato, la matrice ModelView viene ricaricata (tralasciatela al momento) e la fotocamera viene allontanata di 5 unità (non vengono utilizzati i pixel ma le unità). Infine viene chiamato il metodo draw() del triangolo.
In onSurfaceChanged() invece, in primo luogo si imposta la viewport con la larghezza e l’altezza attuale (in modo che funzioni con lo stato GL_PROJECTION), poi transitiamo allo stato GL_MODELVIEW in modo da poter lavorare con i nostri modelli (il triangolo nel nostro caso). Vedremo tutto questo più specificatamente nella prossima lezione.
Avviando l’applicazione a questo punto, dovreste ritrovarvi davanti il seguente risultato:
Sviluppare un gioco per Android – Lezione 12: visualizzare elementi grafici con OpenGL ES
Android Blog Italia.