sábado, 5 de mayo de 2012

¡OpenGL en Android para todos!


En este primer post vamos a aprender a manejar los conceptos básicos de una de las librerías graficas más utilizadas, OpenGL.
Luego iremos haciendo cosas un poco más complejas, hasta llegar a gráficos en 3D y sus respectivas operaciones, pero primero lo primero.
Lo primero que debes conocer antes de empezar a graficar con OpenGL, son las clases a utilizar, y son:
-          GLSurfaceView, la clase que te permite escribir las aplicaciones con OpenGL
-          GLSurfaceView.Renderer, es una interfaz de renderizado genérica donde escribirás el código de lo que quieras que se dibuje.

PARA ENTENDER MÁS RÁPIDO…
Para que puedas asimilar la forma de graficar en OpenGL rápidamente, puedes hacer una pequeña analogía con un pintor y un lienzo, en este caso el pintor sería la interfazGLSurfaceView.Renderer y el lienzo sería GLSurfaceView. Por lo dicho anteriormente, elGLSurfaceView sería el parámetro que pasaríamos en el método setContentView(), que usualmente se coloca al sobrescribir el método onCreate().
Como ya sabrás al implementar un interfaz, tenemos que implementar los métodos que esta trae, en este caso son:
  • onSurfaceCreated(), este método es llamado cuando la superficie (a.k.a. surface) es creada o es re-creada, como este método es llamado al inicio del renderizado, es un buen lugar para colocar lo que no variara en el ciclo del renderizado. Ejm: el color de fondo, la activación del índice z, etc, etc.
  • onDrawFrame(), este método en particular es el encargado de dibujar sobre la superficie (a.k.a. surface)
  • onSurfaceChanged(), este método es llamado cuando la superficie cambia de alguna manera, por ejemplo al girar el móvil y colocarlo en posición de paisaje.

MANOS A LA OBRA (O MÁS BIEN AL CÓDIGO) – LIENZO
Explicada y entendida la teoría anterior, vamos a echar mano al código, primero crearemos una actividad de manera regular, el único cambio que haremos en este caso, será al definir la Vista de contenidos.
import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
 
public class TutOpenGL extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
   GLSurfaceView view = new GLSurfaceView(this);
     view.setRenderer(new OpenGLRenderer());
     setContentView(view);
    }
}
MANOS A LA OBRA (O MÁS BIEN AL CÓDIGO) – PINTOR
Siguiendo con la analogía de la primera parte, el lienzo está puesto, ahora vamos a codificar al pintor, en este caso es una clase extra que debe implementar la interfazGLSurfaceView.Renderer.
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
 
import java.util.Random;
import android.opengl.GLSurfaceView.Renderer;
 
public class MyRenderer implements Renderer{
 
 Random aleatorio = new Random();
 
 @Override
 public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
  float r = aleatorio.nextFloat();
  float g = aleatorio.nextFloat();
  float b = aleatorio.nextFloat();
  gl.glClearColor(r, g, b, 1.0f);
 }
 
 @Override
 public void onDrawFrame(GL10 gl) {
  gl.glClear(gl.GL_COLOR_BUFFER_BIT);
 }
 
 @Override
 public void onSurfaceChanged(GL10 gl, int width, int height) {
 }
 
}
Lo que conseguimos con este código es algo simple, cada vez que gires tu móvil del modo retrato al modo paisaje el fondo cambiará de color.
El método gl.glClearColor() establece el color que se utilizará para limpiar la pantalla, para motivos prácticos dejamos que se elija colores al azar, y es llamado a través del métodogl.glClear(), dado que el parámetro que le pasamos indica que limpie solo el buffer de color, que en este caso, esta en el método onDrawFrame().
Al momento de llamar al método onSurfaceChanged() se destruye la Vista de Contenido por lo que vuelve a llamar al método onCreate() y en este método se asigna el color de limpieza, por lo que cada vez que se gira el móvil, se vuelve a asignar un color diferente y es el que se muestra en pantalla.
Sin excepción alguna, todos los renderizados en 3D están formados por vértices, bordes y caras, resumiendo: polígonos. Es por ello que entender como se generan es una parte importante para entender como trabaja esta librería.
Para empezar con este tutorial no se necesitará de nuevas librerías.
LA TEORÍA POR DELANTE…
Antes de meter mano al código, deberás recordar tu teoría básica de geometría (no te asustes, cuando digo lo básico, es lo verdaderamente básico):
-          Vértice(s): Un vértice es la base de todo, a partir de esto se generan los demás partes que permitan graficar polígonos o modelos en 3D. Un vértice es el punto donde 2 o más bordes se encuentran.
-          Borde (o Línea): Un borde (o línea) es la unión de 2 vértices. En un modelo 3D, el borde puede ser compartido por 2 caras o polígonos.
-          Cara: Es uno (de los tantos) lados que pueda contar el polígono o el modelo en 3D. La modificación de una cara afecta a sus vértices y a sus bordes.
Todas estas definiciones anteriores son conocidas como las primitivas de dibujo.
MANOS AL CÓDIGO
Ahora que tenemos en cuenta la teoría básica sobre la que trabaja OpenGL, vamos a identificar como podemos generar cada uno de los puntos explicados en el segmento anterior:
-          Vértice: Para definir un vértice en OpenGL, se necesita establecer un array del tipo float con los puntos definidos en (x,y,z).
Ejm:
  • Para un punto sería:
float vertice[] = { 1f ,1f ,0f }; // el punto esta ubicado en la coordenada (1,1,0)
  • Para un cuadrado sería:
float vertices[] = {
 
-1f,  1f, 0f,           //vértice ubicado en (-1,1,0)
 
-1f, -1f, 0f,          //vértice ubicado en (-1,-1,0)
 
1f, -1f, 0f,           //vértice ubicado en (1,-1,0)
 
1f, 1f, 0f              //vértice ubicado en (1,1,0)
 
}
  • Para una pirámide de base triangular sería:
float vertices[] = {
 
-1f,   -1f, 0f,        //vértice ubicado en (-1,-1,0)
 
1f,   -1f, 0f,         //vértice ubicado en (1,-1,0)
 
0f,  0.8f, 0f,         //vértice ubicado en (0,0.8, 0)
 
0f,    0f, 2f          //vértice ubicado en (0, 0, 2)
 
};
-          Borde: En OpenGL no se definen los bordes, estos quedan automáticamente establecidos al definir las caras (esto lo veremos en el sgte. punto). Para modificar un borde solo tienes que modificar los vértices que lo componen.
-          Cara: La definición de caras usando OpenGL es a través de triángulos, por ejemplo para definir un cuadro de vértices v0, v1, v2, v3, seria de la siguiente manera:
short caras[] = {
 
0, 1, 2,                  // primer triangulo formado
 
0, 2, 3                    // segundo triangulo formado
 
// juntando los dos triángulos forman un cuadrado
 
}
SE HORNEA DESPUÉS DE PREPARAR LA MASA
Con esto quiero decir, que el orden si importa al momento de generar las caras, porque es aquí cuando se especifica la dirección con la que se dibujaran (en la de las agujas del reloj o en contra de estas). Si quieres mejorar el rendimiento del renderizado, deberás graficar todos tus dibujos en la misma dirección, asi mediante código podremos esconder la parte trasera de tu dibujo cuando no sea necesario mostrarla.
La dirección del renderizado, mediante código, se realiza de la sgte. manera:
gl.glFrontFace(GL10.GL_CCW);
Para poder poder usar las caracteristicas de OpenGL, como las de esconder las caras que están del mirando a la pantalla, se deben activar primero y una vez aplicadas se deben desactivar.
De la siguiente manera:
gl.glEnable(GL10.GL_CULL_FACE);
y después indicar que lado debe esconder:
gl.glCullFace(GL10.GL_BACK);
y al final lo deshabilitamos:
gl.glDisable(GL10.GL_CULL_FACE);
MOMENTO DE MOSTRAR ALGO EN PANTALLA
Para poder mostrar el grafico cuyos vértices y caras hayamos definido, podemos utilizar 2 metodos:
-          public abstract void glDrawArrays(int mode, int first, int count)
-          public abstract void glDrawElements(int mode, int count, int type, Buffer indices)
El primer método dibuja la grafica según el orden como se haya definido en la construcción de los vértices. El segundo método necesita unos parámetros extras para graficar, necesita el orden en el que se graficaran los vértices.
Lo único común a ambas funciones es que ambos necesitan saber que primitivas deben renderizar. Hay varias formas de renderizar las primitivas:
-          GL_POINTS: Renderizar solo los vértices.
-          GL_LINE_STRIP: Renderizar segmentos de línea conectados entre si por un vértice.
-          GL_LINE_LOOP: Renderiza igual que el anterior, pero une el punto final con el punto inicial.
-          GL_LINES: Renderiza segmentos de línea entre 2 vertices independientes de otros vértices.
-          GL_TRIANGLES: Renderiza superficies mediante triángulos usando 3 puntos de referencia.
-          GL_TRIANGLE_STRIP: Renderiza triángulos continuos usando 3 puntos de referencia
-          GL_TRIANGULE_FAN: Renderiza triángulos usando un punto en común, a modo de abanico
CODIGO PURO Y DURO
Para este pequeño ejemplo necesitaremos crear 3 archivos:
-          La actividad principal (puede ser la utilizada en el post anterior)
-          El renderizador (el archivo que implementara la interfaz GLSurfaceView.Renderer)
-          y una clase que crearemos indicando numero de vértices, caras y demás características del dibujo que queramos ver en pantalla.
La actividad principal:
import android.app.Activity;
 
import android.opengl.GLSurfaceView;
 
import android.os.Bundle;
Con esto terminamos las importaciones que necesitamos para que no haya problemas en el código.
public class OpenGLProjectActivity extends Activity {
 
@Override
 
protected void onCreate(Bundle savedInstanceState) {
 
super.onCreate(savedInstanceState);
 
GLSurfaceView surface = new GLSurfaceView(this);
 
MyRenderer renderer = new MyRenderer();
 
surface.setRenderer(renderer);
 
setContentView(surface);
 
}
 
}
En el método onCreate() instanciamos una superficie sobre la que dibujaremos y nuestro renderizado, que será el encargado de dibujar.
El renderizador:

import javax.microedition.khronos.egl.EGLConfig;
 
import javax.microedition.khronos.opengles.GL10;
 
import android.opengl.GLSurfaceView.Renderer;
 
import android.opengl.GLU;
Importaciones necesarias para que el codigo compile sin problemas.
public class MyRenderer implements Renderer{
 
Piramide piramide = new Piramide();
 
@Override
 
public void onSurfaceCreated(GL10 gl, EGLConfig arg1) {
 
// Establece el color de fondo (r,g,b,a)
 
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
 
// Habilita el sombreado suave
 
gl.glShadeModel(GL10.GL_SMOOTH);
 
// Configura el buffer de profundidad
 
gl.glClearDepthf(1.0f);
 
// Habilita el testeo de profundidad
 
gl.glEnable(GL10.GL_DEPTH_TEST);
 
// El tipo de testeo de profundidad a hacer
 
gl.glDepthFunc(GL10.GL_LEQUAL);
 
// Calculo de perspectivas
 
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
 
}
Como lo habias indicado en el primer tutorial, en el método onSurfaceCreated() se establecen los valores cuya variación será poca o nula durante la ejecución del programa, en este caso establecemos:
-          el color de fondo mediante gl.glClearColor()
-          el sombreado suave en el renderizado mediante gl.glShadeModel()
-          el buffer de profundidad mediante gl.glClearDepth()
-          un parámetro de renderizado,el testeo de profundidad, mediante gl.glEnable()
-          el tipo de testeo de profundidad mediante gl.glDepthFunc()
-          la calidad del color y la interpolacion de las coordenadas de la textura mediante gl.glHint() y lo ponemos en la máxima calidad posible.
@Override
 
public void onDrawFrame(GL10 gl) {
 
// Limpia la pantalla y el buffer de profundidad
 
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
 
// Reemplaza la matriz actual con la matriz identidad
 
gl.glLoadIdentity();
 
// Traslada 4 unidades en el eje Z
 
gl.glTranslatef(0, 0, -4);
 
// Dibuja nuestra piramide
 
piramide.draw(gl);
 
}
En el método onDrawFrame(), insertamos el código que queremos que se ejecute durante el renderizado, por lo que hacemos lo sgte:
-          Limpiamos la pantalla y el buffer
-          Cargamos la matriz identidad
-          Trasladamos el dibujo 4 unidades en el eje z
-          Llamamos al metodo draw de nuestro objeto Piramide, instanciado anteriormente.
@Override
 
public void onSurfaceChanged(GL10 gl, int width, int height) {
 
// Establece el puerto de vista actual  al nuevo tamaño
 
gl.glViewport(0, 0, width, height);
 
// Selecciona la matriz de proyeccion
 
gl.glMatrixMode(GL10.GL_PROJECTION);
 
// Reinicia la matriz de proyeccion
 
gl.glLoadIdentity();
 
// Calcula la proporcion del aspecto de la ventana
 
GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f,100.0f);
 
// Selecciona la matriz de la vista del modelo
 
gl.glMatrixMode(GL10.GL_MODELVIEW);
 
// Reinicia la matriz de la vista del modelo
 
gl.glLoadIdentity();
 
}
 
}
En el método onSurfaceChanged() colocamos los ajustes que queramos hacer cuando la superficie sobre la que dibujamos sufra alguna modificación. En este caso:
-          Utilizamos gl.glViewPort() para afinar la transformación de los ejes x,y de las coordenadas de un dispositivo a las de una ventana
-          gl.glMatrixMode() para especificar a que matriz se le van a aplicar los cambios posteriores
-          Gl.glLoadIdentity() para reiniciar la matriz indicada en el método anterior
-          GLU.gluPerspective() para calcular el ratio de la ventana
-          Gl.glMatrixMode, de nuevo, para seleccionar la matriz de vista de modelo
-          Gl.glLoadIdentity para reiniciar la matriz de la vista del modelo
Nuestra clase:
Para este tutorial graficaremos una pirámide triangular, por lo que nuestra clase se llamara (a que no adivinas) Piramide.
import java.nio.ByteBuffer;
 
import java.nio.ByteOrder;
 
import java.nio.FloatBuffer;
 
import java.nio.ShortBuffer;
 
import javax.microedition.khronos.opengles.GL10;
 
public class Piramide {
 
//Nuestros Vertices
 
private float vertices[] = {
 
-1f, -1f, 0f,
 
1f, -1f, 0f,
 
0f, 0.8f, 0f,
 
0f,  0f, 2f
 
};
 
Definimos los vértices de nuestra pirámide en (x,y,z)
 
// La forma como vamos a conectarlos
 
private short caras[] = {
 
0,1,2,
 
0,2,3,
 
0,1,4,
 
1,2,4,
 
2,3,4,
 
3,0,4
 
};
 
Definimos que vértices vamos a unir formando las caras triangulares
 
private float colors[] = {
 
1f, 0f, 0f, 1f,
 
0f, 1f, 0f, 1f,
 
0f, 0f, 1f, 1f,
 
1f, 0f, 1f, 1f
 
};
 
Definimos los colores que vamos a utilizar en cada vertice
 
private FloatBuffer vertexBuffer;
 
private FloatBuffer colorBuffer;
 
private ShortBuffer indexBuffer;
 
public Piramide() {
 
// un float es de 4 bytes, por lo que multiplicaremos el numero de vertices por 4
 
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
 
vbb.order(ByteOrder.nativeOrder());
 
vertexBuffer = vbb.asFloatBuffer();
 
vertexBuffer.put(vertices);
 
vertexBuffer.position(0);
 
// un short es de 2 bytes, por lo que multiplicaremos el numero de vertices por 2
 
ByteBuffer ibb = ByteBuffer.allocateDirect(caras.length * 2);
 
ibb.order(ByteOrder.nativeOrder());
 
indexBuffer = ibb.asShortBuffer();
 
indexBuffer.put(caras);
 
indexBuffer.position(0);
 
ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
 
cbb.order(ByteOrder.nativeOrder());
 
colorBuffer = cbb.asFloatBuffer();
 
colorBuffer.put(colors);
 
colorBuffer.position(0);               }
En el constructor de nuestra clase inicializaremos las variables, de vértices, caras y colores, de manera que OpenGL pueda interpretarlas correctamente
public void draw(GL10 gl) {
 
// Contra las agujas del reloj
 
gl.glFrontFace(GL10.GL_CCW);
 
// Habilitar el sacrificio de caras a ocultar
 
gl.glEnable(GL10.GL_CULL_FACE);
 
// Aca se indica que cara se sacrificara, en este caso, la de atras
 
gl.glCullFace(GL10.GL_BACK);
 
// Habilitar el buffer de vertices para la escritura y cuales se usaran para el renderizado
 
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
 
// Especifica la localizacion y el formato de los datos de un array de vertices a utilizar para el renderizado
 
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
 
// Habilita el buffer para el color del grafico
 
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
 
// Señala donde se encuentra el buffer del color
 
gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);
 
//Dibujamos las superficies
 
gl.glDrawElements(GL10.GL_TRIANGLES, caras.length, GL10.GL_UNSIGNED_SHORT, indexBuffer);
 
// Desactiva el buffer de los vertices
 
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
 
//Desactiva la caracteristica de sacrificios de las caras
 
gl.glDisable(GL10.GL_CULL_FACE);
 
// Desahilita el buffer del color
 
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);          }
 
}
En este ultimo método de nuestra clase habilitamos todas las caracterisiticas explicadas en el tutorial para que el dibujo se pueda hacer de manera correcta.
El resultado final de todo este codigo es el siguiente:
Si te gusto el tutorial, compártelo, cualquier duda o pregunta pueden dejarla en los comentarios.

7 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. se puede desarrollar en netbeans y cuales son las libreras k hay k descargar o como le hago si esta interesante el tema me gustaria desarrollar algo de este estilo pero si pudieras ayudarme te lo agradeciera

    ResponderEliminar
  3. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  4. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  5. Muchas gracias por el aporte.
    Sólo tengo una duda, al girar la piramide del modo gl.glRotatef(mAngle, 0.0f, 1.0f, 1.0f); (con mAngle previamente definido en una funcion aparte) note que no todas las caras de la piramide se encuentran unidas, porque sucede eso? y como puedo solucionarlo
    Gracias

    ResponderEliminar