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:
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:
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 ... end
em 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:
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 (
da activity glSurfaceView.setRenderer(new GlRenderer(this));
)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.
Baixe o código fonte aqui.
Traduzido de obviam.net