Home > Guide e tutorial per Android > Sviluppare un gioco per Android – Lezione 14: Texture Mapping con OpenGL ES pt.2

Sviluppare un gioco per Android – Lezione 14: Texture Mapping con OpenGL ES pt.2

programmazione-android14Siamo giunti al nostro secondo appuntamento con il Texture Mapping. Vi ricordate dove eravamo rimasti? Abbiamo creato la classe Square.java e, ora, ci toccherà applicare la texture al nostro quadrato. Abbiamo bisogno dunque di posizionare come sempre l’immagine (useremo la cartella drawable-mdpi), caricarla, indicare al renderer che vogliamo utilizzarla come texture e, infine, specificare dove posizionarla. OpenGL utilizza i vertici per posizionare l’immagine, quindi abbiamo bisogno di creare appositamente un array che li conterrà. In questo caso, stiamo lavorando sempre in 2D (il concetto è simile per il 3D, ma più complesso). Siete pronti? Iniziamo!

Innanzitutto, dovremo aggiungere un altro array per le coordinate della texture nella classe Square.java, lo chiameremo textureBuffer (notate la similarità con vertexBuffer?). Infatti, questo nuovo array verrà utilizzato nel costruttore Square nello stesso identico modo del precedente. Il codice di seguito vi chiarirà le idee.

private FloatBuffer textureBuffer;	// buffer contenente le coordinate della texture
private float texture[] = {    		
	// Mapping delle coordinate per i vertici
	0.0f, 1.0f,		// (V2)
	0.0f, 0.0f,		// (V1)
	1.0f, 1.0f,		// (V4)
	1.0f, 0.0f		// (V3)
};

/** texture pointer */
private int[] textures = new int[1];

public Square() {
	// il tipo float ha 4 bytes quindi allochiamo per ogni coordinata 4 bytes
	ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
	byteBuffer.order(ByteOrder.nativeOrder());

	// allochiamo la memoria dal byte buffer
	vertexBuffer = byteBuffer.asFloatBuffer();

	//  rempiamo vertexBuffer con i vertici
	vertexBuffer.put(vertices);

	// settiamo la posizione del puntatore all'inizio del buffer
	vertexBuffer.position(0);

	byteBuffer = ByteBuffer.allocateDirect(texture.length * 4);
	byteBuffer.order(ByteOrder.nativeOrder());
	textureBuffer = byteBuffer.asFloatBuffer();
	textureBuffer.put(texture);
	textureBuffer.position(0);
}

In questo codice, tuttavia, viene introdotto un nuovo concetto: il texture pointer. Vediamo subito di cosa si tratta.

Esso verrà utilizzato da un nuovo metodo che ora andremo ad implementare. Il secondo passo, infatti, è il più importante: la creazione del metodo loadGLTexture().  Quest’ultimo verrà chiamato dal renderer all’avvio e, per la precisione, in onSurfaceCreated(). Lo scopo principale del nuovo metodo consiste nel caricare l’immagine dal disco e associarla ad una texture nella repository OpenGL.

Essenzialmente, verrà assegnato un ID alla texture in modo da distinguerla dalle altre, eventuali, texture. Il texture pointer servirà proprio a memorizzare questi ID e, poiché stiamo utilizzando una sola immagine, la sua dimensione sarà proprio pari a 1. Diamo un’occhiata più da vicino a loadGLTexture().

public void loadGLTexture(GL10 gl, Context context) {
	// Caricamento texture
	Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),
			R.drawable.android);

	// generazione texture pointer...
	gl.glGenTextures(1, textures, 0);
	// ...e associazione con il nostro array
	gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

	// filtri
	gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
	gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

	// Specifichiamo la texture 2D 
	GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

	// Liberiamo la memoria
	bitmap.recycle();
}

Come potete vedere dal codice riportato sopra, iniziamo con il caricare l’immagine bitmap e, in questo modo, abbiamo già generato automaticamente l’ID per l’immagine. A questo punto, grazie a glGenTextures, generiamo il nome della texture e lo memorizziamo nel texture pointer. Qui prestate attenzione, infatti abbiamo parlato di nome, ma in realtà quello che viene generato è un intero.

Successivamente, con l’ausilio di glBindTexture, associamo la texture al nome appena generato (texture [0]). Ciò significa che in questa porzione di codice utilizzeremo la texture memorizzata al limite dell’array, infatti non abbiamo fatto altro che, in un certo senso, attivare la texture. Se avessimo avuto molteplici texture da applicare ad altri quadrati, avremmo dovuto attivarle proceduralmente poco prima di utilizzarle.

Successivamente, abbiamo impostato alcuni filtri tipici di OpenGL. Non serve preoccuparsene approfonditamente al momento. Essi sono utili, ad esempio, per espandere o ridurre la texture al fine di coprire il quadrato sul quale l’applichiamo, ed è proprio quello che abbiamo fatto.

Infine, utilizziamo le utility Android per specificare che creiamo la texture 2D a partire dalla bitmap, in modo da creare la texture internamente nel formato nativo basandola sulla bitmap caricata. L’ultimo, ma non meno importante, rigo di codice, ci permette di liberare la memoria, operazione che andrebbe fatta sempre. Ricordate: la memoria a nostra disposizione non è illimitata e le immagini sono file abbastanza grandi.

N.B. Una nota riguardante le bitmap caricate va fatta: ricordate di utilizzare sempre immagini che abbiano come altezza e larghezza una potenza di 2 (non por forza quadrati).

A questo punto, vediamo come è cambiato il metodo draw().

public void draw(GL10 gl) {
	// associamo la texture precedentemente generata
	gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

	gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
	gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

	gl.glFrontFace(GL10.GL_CW);

	gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
	gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

	gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);

	gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}

Non ci sono grosse modifiche rispetto a ciò che abbiamo visto nella scorsa lezione. Tra le più importanti, dovete capire l’utilizzo di glBindTexture, che ci permette di associare la texture precedentemente generata, indicandola grazie all’utilizzo del texture pointer, mentre con il metodo glEnableClientState, quando lo utilizziamo la seconda volta, abilitiamo il Texture Mapping nel contesto corrente. Inoltre, con glTexCoordPointer indichiamo le coordinate della texture al renderer e, con le ultime istruzioni, disabilitiamo il Texture Mapping. 

UV Mapping

Se avete studiato con attenzione il codice sopra riportato, vi sarete sicuramente accorti che l’ordine delle coordinate per la texture è diverso da quello specificato per i vertici. La motivazione sfocia in quello che viene chiamato UV Mapping. Per comprendere meglio ciò, esaminate il seguente schema:

UVMapping

Come potete vedere, il percorso seguito per le coordinate della texture è, praticamente, l’inverso di quello per disegnare il quadrato che, se ricordate, è composto da due triangoli. L’argomento riguardante la mappatura UV è abbastanza complesso (qui potete trovare maggiori informazioni se volete dar sfogo alla vostra curiosità), ma vi basti ricordare di seguire l’ordine inverso dei vertici se il lavoro da svolgere non è molto complesso.

Per concludere questa lezione, dobbiamo modificare il nostro renderer affinché la texture venga caricata all’avvio. Di seguito potete vedere il metodo onSurfaceCreated().

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
	// Carichiamo la texture per il quadrato
	square.loadGLTexture(gl, this.context);

	gl.glEnable(GL10.GL_TEXTURE_2D);			
	gl.glShadeModel(GL10.GL_SMOOTH); 			
	gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); 	
	gl.glClearDepthf(1.0f); 					
	gl.glEnable(GL10.GL_DEPTH_TEST); 			
	gl.glDepthFunc(GL10.GL_LEQUAL); 			

	gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); 

}

Come potete vedere, al rigo 3 viene caricata la texture e, inoltre, forniamo al nostro quadrato il contesto corrente. L’operazione si rende necessaria poiché l’oggetto Square carica la texture su di esso e deve conoscere il percorso della bitmap. Per farlo, basterà modificare la parte iniziale di GlRenderer.java così come segue:

private Square square;	
private Context context;

public GlRenderer(Context context) {
	this.context = context;
	this.square = new Square();
}

Naturalmente, nella classe MainActivity setteremo il renderer con la seguente istruzione:

glSurfaceView.setRenderer(new GlRenderer(this));

Per quanto riguarda le restanti istruzioni presenti in onSurfaceCreated() non preoccupatevene al momento. Se approfondirete il vostro studio di OpenGL ES vi verranno in aiuto spesso, in questo caso le abbiamo utilizzate per configurare il renderer con alcuni valori. Avviando l’applicazione, vi dovreste ritrovare davanti un risultato come il seguente:

Immagine