Mapeamento de textura com o OpenGL no Android (Exibindo imagens usando OpenGL e Squares)

Nos dois artigos anteriores (artigo 1 e artigo 2) introduzimos o Open GL ES do Android. Agora vamos dar um passo adiante e desenvolve-los. Nesse artigo, criaremos um quadro de avisos (que é um quadrado_ e aplicaremos uma textura a ele. Um textura não é nada mais do que uma imagem bitmap. Quando trabalhamos em 2D ajustamos a coordenada Z para 0. Cobriremos 3D a seguir. Isso é muito útil para jogos 2D e é a forma preferida de exibir imagens usando OpenGL, por ser muito rápida.


Nos artigos anteriores vimos como exibir triângulos. Como exibir quadrados? Um quadrado é composto de 2 triângulos.

O diagrama a seguir mostra isso:

Square-Triangles

Existe uma coisa interessante a observar aqui. O quadrado é ABDC ao invés de ABCD. Por quê isso? Por causa da forma como o OpenGL encadeia os triângulos.

O que você vê é um triangle strip. Um triangle strip é uma série de triângulos conectados, 2 triângulos em nosso caso.

O OpenGL desenha o seguinte triangle strip (que é um quadrado) usando os vértices na seguinte ordem:

Triângula 1: V1 -> V2 -> V3
Triângulo 2: V3 -> V2 -> V4

É desenhado o primeiro triângulo usando os vértices em ordem, em seguida toma-se o último vértice do triângulo anterior e usa-se o último lado do triângulo como base para o novo triângulo. Isso também tem benefícios: eliminados dados redundantes da memória.

Peque o projeto do artigo anterior e crie uma nova classe chamada Square.

Se você comparar a classe Square com a classe Triangle, você observará apenas uma diferença:

package net.obviam.opengl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL10;

public class Square {

	private FloatBuffer vertexBuffer;	// buffer holding the vertices

	private float vertices[] = {
			-1.0f, -1.0f,  0.0f,		// V1 - bottom left
			-1.0f,  1.0f,  0.0f,		// V2 - top left
			 1.0f, -1.0f,  0.0f,		// V3 - bottom right
			 1.0f,  1.0f,  0.0f			// V4 - top right
	};

	public Square() {
		// a float has 4 bytes so we allocate for each coordinate 4 bytes
		ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
		vertexByteBuffer.order(ByteOrder.nativeOrder());

		// allocates the memory from the byte buffer
		vertexBuffer = vertexByteBuffer.asFloatBuffer();

		// fill the vertexBuffer with the vertices
		vertexBuffer.put(vertices);

		// set the cursor position to the beginning of the buffer
		vertexBuffer.position(0);
	}

	/** The draw method for the square with the GL context */
	public void draw(GL10 gl) {
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

		// set the colour for the square
		gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);

		// Point to our vertex buffer
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

		// Draw the vertices as triangle strip
		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);

		//Disable the client state before leaving
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
	}
}

A diferença está marcada nas linhas 13-18. Isso mesmo, adicionamos um mais vértice ao array vertices. Agora vamos mudar o GlRenderer de forma que ao invés de um Triangle usemos um Square.

package net.obviam.opengl;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLU;
import android.opengl.GLSurfaceView.Renderer;

public class GlRenderer implements Renderer {

	private Square 		square;		// the square

	/** Constructor to set the handed over context */
	public GlRenderer() {
		this.square		= new Square();
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		// clear Screen and Depth Buffer
		gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

		// Reset the Modelview Matrix
		gl.glLoadIdentity();

		// Drawing
		gl.glTranslatef(0.0f, 0.0f, -5.0f);		// move 5 units INTO the screen
												// is the same as moving the camera 5 units away
		square.draw(gl);						// Draw the triangle

	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		if(height == 0) { 						//Prevent A Divide By Zero By
			height = 1; 						//Making Height Equal One
		}

		gl.glViewport(0, 0, width, height); 	//Reset The Current Viewport
		gl.glMatrixMode(GL10.GL_PROJECTION); 	//Select The Projection Matrix
		gl.glLoadIdentity(); 					//Reset The Projection Matrix

		//Calculate The Aspect Ratio Of The Window
		GLU.gluPerspective(gl, 45.0f, (float)width / (float)height, 0.1f, 100.0f);

		gl.glMatrixMode(GL10.GL_MODELVIEW); 	//Select The Modelview Matrix
		gl.glLoadIdentity(); 					//Reset The Modelview Matrix
	}

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
	}
}

Ao executar a aplicação será produzido o seguinte resultado:

Screen-shot-2011-01-26-at-01.58.37

Examinando isso, o método draw() da classe Square deve fazer sentido agora.

public void draw(GL10 gl) {
	gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

	// set the colour for the square
	gl.glColor4f(0.0f, 1.0f, 0.0f, 0.5f);

	// Point to our vertex buffer
	gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

	// Draw the vertices as triangle strip
	gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);

	//Disable the client state before leaving
	gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}

Em primeiro lugar permitimos que o OpenGL use um array de vértices para renderização. Nosso array contém os vértices de nosso quadrado.

gl.glVertexPointer (linha 5) diz ao renderizado do OpenGL de onde obter os vértices e de qual tipo eles são. O primeiro parâmetro diz quantas coordenadas são usadas para um vértice. Usamos 3 (x,y,z). O segundo parâmetro informa que os valores são do tipo  float. O terceiro parâmetro é o deslocamento entre os vértices do array.  Isso recebe o nome strife. Como temos um array fortemente compacto, o deslocamento é 0. Finalmente, o último parãmetro diz onde os vértices estão armazenados. Naturalmente, será passado nosso buffervertexBuffer.

gl.glDrawArrays na linha 11 diz ao OpenGL para desenhar a primitiva. Que tipo de primitiva? A que foi especificada no primeiro parâmetro: GL10.GL_TRIANGLE_STRIP. Ele usa os vértices do buffer configurado previamente e segue as regras das faixas dos triângulos descritas anteriormente. O segundo parâmetro especifica o índice inicial para os vértices do array. O terceiro informa quantos vértices usar para o polígono a ser renderizado. Por termos especificado na sentença anterior (gl.glVertexPointer) que 3 coordenadas definem um vértice, forneceremos o tamanho de nosso array de vértices divido por 3. Como existem 9 elementos no array definimos 3 vértices.

glDisableClientState(GL10.GL_VERTEX_ARRAY) desativa o estado de renderização de um array que contém os vértices.

Imagine glEnableClientState e glDisableClientState como sentenças begin ... endem um programa. Basicamente, entramos com sub-rotinas no renderizador do OpenGL. Uma vez que entramos em uma sub-rotina, configuramos variáveis (buffer do vértice, cores, etc) e executamos outras sub-rotinas (desenho de vértices). Depois de tudo feito, encerramos a sub-rotina. Trabalhamos com isolamentos dentro do renderizador.

Certifique-se de executar a aplicação nesse estágio e entender o que está acontecendo.

Criando a textura

Agora vem a parte divertida. Vamos carregar uma imagem e criar uma textura. Uma textura é uma imagem. Para ver como carregar imagens em sua aplicação Android, veja esse artigo. Estaremos trabalhando com a classe Square pois queremos aplicar a textura ao quadrado.

Precisamos carregar a imagem, informar ao renderizador do OpenGL que queremos usa-la como uma textura, e finalmente, informamos ao renderizador exatamente onde exibiremos a textura em nossa primitiva (quadrado).

Imagine isso como se você estivesse pondo uma película em uma janela ou parede. No projeto que pode ser baixado no final desse artigo, é fornecido uma imagem do tamanho da janela, de forma que o canto superior esquerdo da película será o canto superior esquerdo da janela. E é isso, vamos partir para o trabalho.

O OpenGL usa os vértices para planejar onde por as coisas. Dessa forma, precisamos criar um array para a imagem. Mas dessa vez, será um array 2D pois o bitmpa é como uma folha de papel plano. Adicione as coordenadas do array para a textura:

private FloatBuffer textureBuffer;	// buffer holding the texture coordinates
private float texture[] = {    		
		// Mapping coordinates for the vertices
		0.0f, 1.0f,		// top left		(V2)
		0.0f, 0.0f,		// bottom left	(V1)
		1.0f, 1.0f,		// top right	(V4)
		1.0f, 0.0f		// bottom right	(V3)
};

Precisamos criar textureBuffer de forma similar ao vertexBuffer. Isso acontece no construtor e apenas reusamos o byteBuffer. Verifique como fica o novo construtor:

public Square() {
	ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4); 
	byteBuffer.order(ByteOrder.nativeOrder());
	vertexBuffer = byteBuffer.asFloatBuffer();
	vertexBuffer.put(vertices);
	vertexBuffer.position(0);

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

Adicionaremos um importante método à classe Square: o método loadGLTexture. Esse método será chamado a partir do renderizador quando ele for iniciado. Isso carregará a imagem do disco e a ligará á textura no repositório OpenGL. Basicamente associará um ID interno para a imagem pré-processado e será usada a API do OpenGL para identifica-la dentre outras texturas.

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

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

	// generate one texture pointer
	gl.glGenTextures(1, textures, 0);
	// ...and bind it to our array
	gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

	// create nearest filtered texture
	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);

	// Use Android GLUtils to specify a two-dimensional texture image from our bitmap 
	GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

	// Clean up
	bitmap.recycle();
}

Precisamos de um array que aponte para a textura. Este array é onde o OpenGL armazenará os nomes das texturas que usaremos em nossa aplicação. Por termos apenas uma imagem, criaremos um array de tamanho 1.

A Linha 06 carrega o bitmap Android que foi copiado previamente copiado no diretório/res/drawable-mdpi, de forma que o ID já esteja gerado.

Uma observação sobre esse bitmap. Encoraja-se que seja um quadrado. Isso facilita um bocado com o escalonamento. Dessa forma, certifique que seus bitmaps sejam quadrados (6×6, 12×12, 128×128, etc.). Se não forem quadrados, certifique-se de que seus comprimentos e alturas sejam múltiplos de 2 (2, 4, 8, 16, 32, …). Você pode ter um bitmap de 128×512 e ele ser perfeitamente utilizável e estar otimizado.

A Linha 10 gera nomes para as texturas. Em nosso caso, gera um nome e armazena no array  textures. Mesmo que diga name, de fato ele armazena umint. É um pouco confuso, mas é a forma que é.

A Linha 12 conecta a textura com o nome gerado (texture[0]). O que isso significa é que qualquer coisa que faça uso das texturas nessa sub-rotina, usará a textura ativa. Se tivermos múltiplas texturas e múltiplos quadrados as erem usados, teremos que ativar a textura apropriada para cada quadrado imediatamente antes deles serem usados.

As Linhas 15 e 16 configuram alguns filtros a serem usados com a textura. Informamos ao OpenGL quais tipos de filtros usaremos quando precisarmos encolher ou expandir a textura para cobrir o quadrado. Escolhemos alguns algoritmos básicos para escalonar a imagem. Não se preocupe com isso nesse momento.

Na linha 19 usamos os utilitários do Android para especificar a imagem da textura como nosso bitmap. Ela cria a imagem (textura) internamente no formato nativo baseado em nosso bitmap.

Na linha 22 liberamos memória. Isso não deve ser esquecido pois a memória do dispositivo é muito limitada e imagens são grandes.

Agora veremos como o método draw() ficou depois dessas modificações.

	public void draw(GL10 gl) {
		// bind the previously generated texture
		gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

		// Point to our buffers
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

		// Set the face rotation
		gl.glFrontFace(GL10.GL_CW);

		// Point to our vertex buffer
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

		// Draw the vertices as triangle strip
		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);

		//Disable the client state before leaving
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
	}
}

Não é uma grande modificação em relação ao artigo anterior. As adições estão documentadas e são as seguintes:
A Linha 03 conecta a textura com o nome (ID inteiro) armazenado emtextures[0].
A Linha 07 ativa o mapeamento da textura no contexto do OpenGl atual.
A Linha 14 fornece o contexto OpenGL com as coordenadas da textura.

Depois de desenhar a primitiva com as texturas, desligamos o mapeamento da textura junto com a renderização da primitiva.

Importante – Mapeamento UV

Se você observar atentamente, a ordem dos vértices no mapeamento do array de coordenadas da textura não segue a ordem dos vértices do array de coordenadas do quadrado.

Há um explicação muito boa do mapeamento das coordenadas da textura aqui: http://iphonedevelopment.blogspot.com/2009/05/opengl-es-from-ground-up-part-6_25.html. Tentarei explicar isso rapidamente aqui. Examine o diagrama a seguir:

UVMapping

O quadrado é composto de 2 triângulos e os vértices estão na seguinte ordem:

1 – inferior esquerdo
2 – inferior direito
3 – superior esquerdo
4 – superior direito

Observe que o caminho é anti-horário. As coordenadas da textura estarão na ordem: 1 -> 3 -> 2 -> 4

Apenas tenha esse mapeamento em mente e rotacione-o se você começar sua forma de um canto diferente. Para ler mais sobre o mapeamento UV veja a entrada sobre o tema na wikipedia ou pesquisa no google sobre ele.

Para finalizar e fazer isso funcionar, precisamos fornecer um contexto ao nosso renderizador de forma que possamos carregar a textura na inicialização. O método onSurfaceCreated deve ficar dessa forma:

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
	// Load the texture for the square
	square.loadGLTexture(gl, this.context);

	gl.glEnable(GL10.GL_TEXTURE_2D);			//Enable Texture Mapping ( NEW )
	gl.glShadeModel(GL10.GL_SMOOTH); 			//Enable Smooth Shading
	gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); 	//Black Background
	gl.glClearDepthf(1.0f); 					//Depth Buffer Setup
	gl.glEnable(GL10.GL_DEPTH_TEST); 			//Enables Depth Testing
	gl.glDepthFunc(GL10.GL_LEQUAL); 			//The Type Of Depth Testing To Do

	//Really Nice Perspective Calculations
	gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); 
}

A Linha 03 carrega a textura. O resto das linhas apenas configura o renderizador com alguns valores. Você não precisa se preocupar com eles agora.

Você precisará fornecer o contexto da aplicação ao objeto Square, porque o próprio objeto carrega a textura e precisa saber o caminho para o bitmap.

Apenas forneça o contexto para o renderizador no método onCreate (glSurfaceView.setRenderer(new GlRenderer(this));)da activity Run, e tudo estará feito.

Certifique-se de que o renderizador possui o contexto declarado e configure-o através do construtor. Extraído da classe GlRendered.

private Square 		square;		// the square
private Context 	context;

/** Constructor to set the handed over context */
public GlRenderer(Context context) {
	this.context = context;

	// initialise the square
	this.square = new Square();
}

Se você executar o código visualizará um quadrado com um robô colocado sobre ele.

android_textre

Baixe o código fonte aqui.

Traduzido de obviam.net