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.
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.
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.
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:
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.