Tutorial de OpenGL ES com Android – Exibindo elementos gráficos (Primitivas)

Essa é a parte 2 na série de artigos sobre o OpenGL ES no Android. No artigo anterior vimos como configurar o projeto Android para usar a visão OpenGl fornecida pela plataforma com nosso renderizador. Você pode usar o projeto desse artigo como base para o desse.

Antes de começarmos a exibir coisas, precisamos conhecer alguns conceitos básicos de programação 3D e também nos familiarizar com a terminologia, que se trata do básico de geometria. Gráficos 3D acontecem no Sistema de coordenadas cartersiano.

Isso significa que o sistema de coordenadas usado tem três dimensões: X, Y e Z. Tradicionalmente, X vai da esquerda para a direita, Y da parte inferior para a superior, e Z  da tela para a direção do usuário.

Enquanto lidamos com objetos a serem exibidos (um robô ou um carro, por exemplo), o OpenGL lida com componentes desses objetos. Cada objeto é criado a partir de primitivas que no caso do OpenGL é um triângulo. Cada triângulo tem uma face e uma backface (parte de trás).

Um triângulo é definido por 3 pontos no espaço. Um ponto é chamado de vértex (vértices no plural).

O diagrama a seguir mostra 2 vértices: A e B.

Vertices

Eu desenhei esse diagrama para mostrar como iremos diferenciar entre 2D e 3D. Um vértice é definido pelas suas coordenadas X, Y e Z. Se usarmos 0 para o componente Z todas as vezes, teremos 2D. Você pode enxergar o vértice A como parte do plano definido por X e Y. O vértice B está mais distante na coordenada Z. Se você imaginar o eixo Z como uma linha perpendicular a tela, não enxergaremos B.

Um triângulo é chamado de primitiva. Uma primitiva é o tipo mais simples que o OpenGL entende e é capaz de representar graficamente.

É bem simples: 3 vértices definem um triângulo. Existem outras primitivas também, como quadrados, mas iremos ficar no básico. Cada forma pode ser decomposta em triângulos.

Mencionamos no inicio do texto sobre as faces do triângulo. Por quê isso é importante? No 3D iremos ter objetos com partes viradas para você, como jogador, e partes viradas para o lado oposto. Para poder executar o desenho de maneira eficiente, o OpenGL não desenhará os triângulos que estejam na face volta para o lado contrário do jogador, pois eles não são necessários por estarem escondidos. Isso é chamado de abate de faces (backface culling).

Como o OpenGL determina isso? Isso é determinado pela ordem dos vértices ao desenhar o triângulo. Se a ordem é a anti-horária então se trata de uma face (triângulo verde). A ordem horária dos vértices indica uma face contrária (triângulo vermelho). Essa é a configuração padrão mas naturalmente pode ser alterada. O diagrama a seguir ilustra isso.

Faces

O triângulo vermelho não será desenhado.

Criando e Desenhando um triângulo

Como toda teoria entendida, vamos criar um triângulo e desenha-lo. Um triângulo é composto de três vértices. As coordenadas dos vértices não medidas em pixels. Usaremos float para representar os valores e eles serão relativos um ao outro.

Se o tamanho de um lado for 1.0f e o tamanho de outro lado for 0.5f, isso significa  que o segunda lado é metade do tamanho do primeiro. Quão grande ele será exibido depende de como o viewport é configurado. Imagine o viewport como uma câmera. Quando usamos 2D significa que a câmera é ortogonal à tela.  Se a câmera estiver muito perto, o triângulo aparentará ser maior do que se estiver longe.

Vamos criar a classe Triangle .

package net.obviam.opengl;

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 holding the vertices

	private float vertices[] = {
			-0.5f, -0.5f,  0.0f,		// V1 - first vertex (x,y,z)
			 0.5f, -0.5f,  0.0f,		// V2 - second vertex
			 0.0f,  0.5f,  0.0f			// V3 - third vertex
	};

	public Triangle() {
		// 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);
	}
}

A linha 11 define um FloatBuffer que irá armazenar os vértices de nosso triângulo. Precisamos usar o pacote java.nio pois isso demanda muita entrada e saída.

O array  vertices[] armazena as coordenadas atuais dos vértices. O triângulo que será desenhado é representado pelo diagrama a seguir. Calculamos tudo a partir da origem.

Triangle

No construtor inicializamos o triângulo a partir desse array vertices[].

O que fazemos é preencher vertexBuffer com as coordenadas e configurar a posição do cursor para o inicio do buffer. Estaremos usando esse buffer na chamada OpenGL para exibir as faixas do  triângulo. Atualmente temos apenas um.

Vamos dar uma olhada no renderizador, que é o GlRenderer

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 Triangle 	triangle;	// the triangle to be drawn

	/** Constructor */
	public GlRenderer() {
		this.triangle = new Triangle();
	}

	@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
		triangle.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) {
	}
}

Criamos o triângulo no construtor. O método onDrawFrame(GL10 gl) é o de maior importância para nós. O OpenGL trabalha com variáveis de estado. Cada método que nós chamamos no contexto do OpenGL muda seu estado interno.

Seguindo o método onDrawFrame vemos que a cada vez que um quadro é desenhado, o buffer é limpo, a matriz ModelView é recarregada (não se preocupe se você não entender isso nesse momento), a câmera se move 5 unidades (estamos lidando com unidades aqui, não pixels) e o método draw() do triângulo é chamado.

O método onSurfaceChanged por outro lado, alterna o contexto do OpenGL entre alguns poucos estados. Primeiro ajusta o viewport para o comprimento e altura atual da superfície (de forma que funcione com o estado GL_PROJECTION), em seguida alterna para o estado GL_MODELVIEW para que possamos trabalhar com nossos modelos – o triângulo em nosso caso. Isso fará sentido mais tarde, não se preocupe.

Vamos verificar agora o método draw para o triângulo:

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

	// set the colour for the triangle
	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);
}

Por armazenamos as coordenadas dos vértices do triângulo em um FloatBuffer precisamos permitir que o OpenGL leia a partir desse tipo de dado e entenda que se trata de um triângulo. A Line 02 faz justamente isso.

A Linha 05 configura as cores da entidade (o triângulo em nosso caso) que será desenhada. Observe que os valores de rgb são float e variam de 0.0 a 1.0.

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); dirá ao OpenGL para usar  vertexBuffer para extrair os vértices do triângulo.

O primeiro parâmetro (valor = 3) representa o número de vértices do buffer. O segundo deixa o OpenGL saber qual tipo de dados o buffer armazena. O terceiro parâmetro é o deslocamento do array usando para o vértices. Por não armazenarmos dados extras, nossos vértices são armazenados um seguido do outro e por isso não há deslocamento. Finalmente, o último parâmetro é nosso buffer, que contém os vértices.

gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3); diz ao OpenGL para desenhar as faixas do triângulo encontradas no buffer fornecido anteriormente, começando com o primeiro elemento. Ele também permite descobrir quantos vértices existem.

E é isto. Execute o projeto e você deve ser capaz de visualizar seu primeiro triângulo, dessa forma:

Screen-shot-2011-01-23-at-02.39.11

Baixe o código fonte aqui.

Esse código foi inspirado no código ports para Android do nehe. Para aprender mais sobre OpenGL, é altamente recomendado esses tutoriais.

No próximo artigo criaremos objetos 3D básicos e rotacionaremos eles. Também veremos como usar texturas nos elementos.

  • Leandro Vianna

    Excelente post, vai me ajudar a aprender o OpenGL. Por enquanto vou começar com o 1.0 para depois ir para o 2.0 e posteriormente o 3.0.