Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Android SDK
Code

Cómo Usar OpenGL ES en Apps Android

by
Difficulty:AdvancedLength:LongLanguages:

Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

Casi todo teléfono Android disponible en el mercado hoy tiene una unidad de procesamiento de gráficos, o GPU. Como su nombre sugiere, esta es una unidad de hardware dedicada a manejar cálculos que son usualmente relacionados a gráficos 3D. Como un desarrollador de apps, puedes hacer uso de la GPU para crear gráficos complejos y animaciones que se ejecutan a velocidades de fotogramas muy altas.

Actualmente hay dos APIs diferentes que puedes usar para interactuar con la GPU del dispositivo Android: Vulkan y OpenGL ES. Mientras que Vulkan está disponible solo en dispositivos ejecutando Android 7.0 o superior, OpenGL ES es soportado por todas las versiones Android.

En este tutorial, te ayudaré a comenzar a usar OpenGL ES 2.0 en aplicaciones Android.

Prerrequisitos

Para poder seguir este tutorial, necesitarás:

  • la última versión de Android Studio
  • un dispositivo Android que soporte OpenGL ES 2.0 o superior
  • una versión reciente de Blender, o cualquier otro software de modelado 3D

1. ¿Qués Es OpenGL ES?

OpenGL, que es abreviación para Open Graphics Library, es una API de plataforma independiente que te permite crear gráficos 3D acelerados por hardware.OpenGL ES, abreviatura para OpenGL for Embedded Systems, es un subconjunto de la API.

OpenGL ES es una API de muy bajo nivel. En otras palabras, no ofrece ningún método que te permita crear rápidamente o manipular objetos 3D. En su lugar, mientras se trabaja con esta, se espera que administres manualmente tareas tales como crear los vértices y caras individuales de objetos 3D, calculando varias transformaciones 3D, y creando diferentes tipos de sombras.

También vale la pena mencionar que el SDK y NDK de Android te permiten escribir código relacionado a OpenGL ES tanto en Java como C.

2. Configuración de Proyecto

Debido a que las APIs OpenGL ES son parte del framework Android, no tienes que agregar ninguna dependencia a tu proyecto para poder usarlas. En este tutorial, sin embargo, estaremos usando la librería Apache Commons IO para leer los contenidos de unos cuántos archivos de texto. Así pues, agrégala como una dependencia compile en tu archivo build.gradle de app module:

Adicionalmente, para poder prevenir a usuarios Google Play que no tengan dispositivos que soporten la versión OpenGL ES que necesitas, agrega la siguiente etiqueta <uses-feature> al archivo de manifiesto de tu proyecto:

3. Crea un Lienzo

El framework Android ofrece dos widgets que pueden actuar como un lienzo para tus gráficos 3D: GLSurfaceView y TextureView. La mayoría de los desarrolladores prefieren usar GLSurfaceView, y eligen TextureView solo cuando pretenden superponer sus gráficos 3D sobre otro widget View. Para la app que estaremos creando en este tutorial, GLSurfaceView será suficiente.

Agregar un widget GLSurfaceView a tu archivo layout no es diferente a agregar cualquier otro widget.

Nota que hemos hecho el ancho de nuestro widget igual a su altura. Hacerlo es importante porque el sistema de coordenadas de OpenGL ES es un cuadrado. Si debes usar un lienzo rectangular, recuerda incluir su relación de aspecto mientras calculas tu matriz de proyección. Aprenderás lo que es una matriz de proyección en un paso posterior.

Inicializar un widget GLSurfaceView dentro de una clase Activity es tan simple como llamar al método findViewById() y pasarle su id a este.

Adicionalmente, debemos llamar al método setEGLContextClientVersion() para especificar explícitamente la versión de OpenGL ES que estaremos usando para dibujar dentro del widget.

4. Crea un Objeto 3D

Aunque es posible crear objetos 3D en Java codificando a mano las coordenadas X, Y, y Z de todos sus vértices, hacerlo es muy molesto. Usar en su lugar herramientas de modelado 3D es mucho más sencillo. Blender es una de esas herramientas. Es de código abierto, poderoso, y muy fácil de aprender.

Ejecuta Blender y presiona X para borrar el cubo por defecto. Después, presiona Shift-A y selecciona Mesh > Toroide. Ahora tenemos un objeto 3D bastante complejo que consiste en 576 vértices.

Torus in Blender

Para poder usar el toroide en nuestra aplicación Android, debemos exportarlo como un archivo Wavefront OBJ. Así pues, ve a Archivo > Exportar > Wavefront (.obj). En la siguiente pantalla, dale un nombre al archivo OBJ, asegúrate de que las opciones Caras Triangulares y Mantener Orden de Vértices estén seleccionadas, y presiona el botón Exportar OBJ.

Exporting 3D object as Wavefront OBJ file

Puedes cerrar ahora Blender y mover el archivo OBJ a la carpeta assets de tu proyecto en Android Studio.

5. Analiza el Archivo OBJ

Si no lo has notado aún, el archivo OBJ que creamos en el paso anterior es un archivo de texto, que puede ser abierto usando cualquier editor de texto.

OBJ file opened with a text editor

En el archivo, cada línea que comienza con una "v" representa un vértice individual. De manera similar, cada línea comenzando con una "f" representa una cara triangular individual. Mientras que cada línea de vértice contiene las coordenadas X, Y, y Z de un vértice, cada línea de cara contiene los índices de tres vértices, que juntos forman una cara. Eso es todo lo que necesitas saber para analizar un archivo OBJ.

Antes de que comiences, crea una nueva clase Java llamada Torus y agrega dos objetos List, uno para los vértices y otro para las caras, como sus variables miembro.

La manera más sencilla de leer todas la líneas individuales del archivo OBJ es usar la clase Scanner y su método nextLine(). Mientras cicla a través de las líneas y llena las dos listas, puedes usar el método startsWith() de la clase String para revisar si la línea actual comienza con una "v" o una "f".

6. Crea Objetos Buffer

No puedes pasar las listas de vértices y caras a los métodos disponibles en la API OpenGL ES directamente. Primero debes convertirlas en objetos buffer. Para almacenar la información de coordenada de vértice, necesitaremos un objeto FloatBuffer. Para la información de cara, que simplemente consiste en índices de vértices, un objeto ShortBuffer será suficiente. 

De manera acorde, agrega las siguientes variables miembro a la clase Torus:

Para inicializar los buffers, debemos crear primero un objeto ByteBuffer usando el método allocateDirect(). Para el buffer de vértices, colocamos cuatro bytes para cada coordenada, con las coordenadas siendo números de punto flotante. Una vez que el objeto ByteBuffer ha sido creado, puedes convertirlo en un FloatBuffer para llamar su método asFloatBuffer().

De manera similar, crea otro objeto ByteBuffer para el buffer de caras. Esta vez, coloca dos bytes por cada índice de vértice porque los indices son literales unsigned short. También, asegúrate de usar el método asShortBuffer() para convertir el objeto ByteBuffer a ShortBuffer.

Poblar el buffer de vértices involucra ciclar a través de los contenidos de verticesList, extrayendo las coordenadas X, Y, y Z de cada elemento, y llamando al método put() para poner datos dentro del buffer. Debido a que verticesList contiene solo cadenas, debemos usar el parseFloat() para convertir las coordenadas de cadenas a valores float.

Nota que en el código de arriba hemos usado el método position() para reestablecer la posición del buffer.

Poblar el buffer de caras es ligeramente diferente. Debes usar el método parseShort() para convertir cada índice de vértice a un valor corto. De manera adicional debido a que los índices comienzan en uno en vez de cero, debes recordar restar uno a estos antes de ponerlos dentro del buffer.

7. Crea Shaders

Para poder generar nuestro objeto 3D, debemos crear un vértice shader con un fragment shader para este. Por ahora, puedes pensar en un shader como un programa muy simple escrito en un lenguaje como C llamdo OpenGL Shading Language, o GLSL abreviado.

Un shader de vértice, como podrías haber imaginado, es responsable de manejar los vértices de un objeto 3D. Un fragment shader, también llamado pixel shader, es responsable de colorear los pixeles del objeto 3D.

Paso 1: Crea un Shader de Vértices

Crea un nuevo archivo llamado vertex_shader.txt dentro de la carpeta res/raw de tu proyecto.

Un shader de vértices debe tener una variable global attribute dentro de este para recibir datos de posición de vértice desde tu código Java. De manera adicional, agrega una variable local uniform para recibir una matriz de proyección de vista desde el código Java.

Dentro de la función main() del shader de vértices, debes establecer el valor de gl_position, una variable GLSL integrada que decide la posición final del vértice. Por ahora, puedes simplemente establecer su valor al producto de las variables locales uniform y attribute.

De manera acorde, agrega el siguiente código al archivo:

Paso 2: Crea un Fragment Shader

Crea un nuevo archivo llamado fragment_shader.txt dentro de la carpeta res/raw de tu proyecto.

Para mantener corto este tutorial, ahora estaremos creando un fragment shader muy minimalista que simplemente asigne el color naranja a todos los pixeles. Para asignar un color a un pixel, dentro de la función main() de un fragment shader, puedes usar la variable gl_FragColor integrada.

En el código de arriba, la primera línea especificando la precisión de números de punto flotante es importante porque un fragment shader no tiene ninguna precisión por defecto para ellos.

Paso 3: Compila los Shaders

De vuelta a la clase Torus, debes ahora agregar código para compilar los dos shaders que creaste. Antes de que lo hagas, sin embargo, debes convertirlos de recursos raw a strings. La clase IOUtils, que es parte de la librería Apache Commons IO, tiene un método toString() para hacer justo eso. El siguiente código te muestra cómo usarlo:

El código del shader debe ser agregado a objetos shader OpenGL ES. Para crear un nuevo objeto shader, usa el método glCreateShader() de la clase GLES20. Dependiendo del tipo de objeto shader que quieras crear, puedes pasarle GL_VERTEX_SHADER o GL_FRAGMENT_SHADER. El método devuelve un entero que sirve como referencia al objeto shader. Un objeto shader recién creado no contiene ningún código. Para agregar el código shader al objeto shader, debes usar el método glShaderSource().

El siguiente código crea objetos shader para el shader de vértices como para el fragment shader:

Ahora podemos pasar los objetos shader al método glCompileShader() para compilar el código que contienen.

8. Crea un Programa

Mientras se genera un objeto 3D, no usas los shaders directamente. En su lugar, los adjuntas a un programa y usas el programa. Así pues, agrega una variable miembro a la clase Torus para almacenar una referencia a un programa OpenGL ES.

Para crear un nuevo programa, usa el método glCreateProgram(). Para adjuntar los objetos shader vertex y fragment a este, usa el método glAttachShader().

En este punto, puedes enlazar el programa y comenzar a usarlo. Para hacerlo, usa los métodos glLinkProgram() y glUseProgram()

9. Dibuja el Objeto 3D

Con los shaders y buffers listos, tenemos todo lo que necesitamos para dibujar nuestro toroide. Agrega un nuevo método a la clase Torus llamado draw:

En un paso anterior, dentro del shader de vértices, definimos una variable position para recibir datos de posición de vértice desde código Java. Ahora es tiempo de enviarle los datos de posición de vértice. Para hacerlo, debemos primero enlazar a la variable position en nuestro código Java usando el método glGetAttribLocation(). De manera adicional, el enlace debe ser habilitado usando el método glEnableVertexAttribArray().

De manera acorde, agrega el siguiente código dentro del método draw():

Para apuntar al enlace position a nuestro buffer de vértices, debemos usar el método glVertexAttribPointer(). Adicional al buffer de vértices mismo, el método espera el número de coordenadas por vértice, el tipo de las coordenadas, y la compensación de bytes para cada vértice. Debido a que tenemos tres coordenadas por vértice y cada coordenadas es un float, la compensación de byte debe ser 3 * 4.

Nuestro shader de vértices también espera una matriz de proyección de vista. Aunque una matriz no siempre es necesaria, usarla te permite tener mejor control sobre cómo se genera tu objeto 3D.

Una matriz de proyección de vista es simplemente el producto de las matrices de vista y proyección. Una matriz de vista te permite especificar las ubicaciones de tu cámara y el punto que está observando. Una matriz de proyección, por el otro lado, te permite no solo mapear el sistema cuadrado de coordenadas para OpenGL ES a la pantalla rectangular de un dispositivo Android, sino también especificar planos cercanos y lejanos del tronco de vista.

Para crear las matrices, puedes simplemente crear tres arreglos float de tamaño 16:

Para inicializar la matriz de proyección, puedes usar el método frustumM() de la clase Matrix. Este espera las ubicaciones de los planos de recorte izquierdo, derecho, inferior, superior, cercano y lejano. Debido a que nuestro lienzo ya es un cuadrado, puedes usar los valores -1 y 1 para los planos de recorte izquierdo y derecho, y superior e inferior. Para los planos de recorte cercano y lejano, siéntete libre de experimentar con distintos valores.

Para inicializar la matriz de vista, usa el método setLookAtM(). Este espera las posiciones de la cámara y el punto que está observando. De nuevo eres libre de experimentar con distintos valores.

Finalmente, para calcular la matriz de producto, usa el método multiplyMM().

Para pasar la matriz de producto al shader de vértices, debes tener un enlace a su variable matrix usando el método glGetUniformLocation(). Una vez que tienes el enlace, puedes apuntarlo a la matriz de producto usando el método glUniformMatrix().

Debes haber notado que aún no hemos usado el buffer de caras. Esto significa que aún no le hemos dicho a OpenGL ES cómo conectar los vértices para formar triángulos, que servirán como las caras de nuestro objeto 3D.

El método glDrawElemens() te permite usar el buffer de caras para crear triángulos. Como sus argumentos, este espera el número total de índices de vértice, el tipo de cada índice, y el buffer de cara.

Por último, recuerda deshabilitar el manejador attribute que habilitaste anteriormente pasar pasar los datos de vértice al shader de vértices.

10. Crea un Generador

Nuestro widget GLSurfaceView necesita un objeto GLSurfaceView.Renderer para poder generar gráficos 3D. También puedes usar el setRenderer() para asociar un generador con el.

Dentro del método onSurfaceCreated() del generador, debes especificar qué tan seguido debe ser generado el objeto 3D. Por ahora, lo vamos a generar solo cuando el gráfico 3D cambia. Para hacerlo, pasa la constante RENDERMODE_WHEN_DIRTY al método setRenderMode(). Adicionalmente, inicializa una nueva instancia del objeto Torus.

Dentro del método onSurfaceChanged() del generador, puedes definir el ancho y alto de tu viewport usando el método glViewport().

Dentro del método onDrawFrame() del generador, agrega una llamada al método draw() de la clase Torus para realmente dibujar el toroide.

En este punto, puedes ejecutar tu app para ver el toroide naranja.

App displaying torus

Conclusión

Ahora sabes cómo usar OpenGL ES en aplicaciones Android. En este tutorial, también aprendiste cómo analizar un archivo Wavefront OBJ y extraer datos de vértices y caras de este. Sugiero que generes unos cuántos objetos 3D más usando Blender e intentes generarlos en la app.

Aunque nos enfocamos solo en OpenGL ES 2.0, entiendo que OpenGL ES 3.x es retro-compatible con OpenGL ES 2.0. Esto significa que puedes simplemente reemplazar la clase GLES20 con las clases GLES30 o GLES31.

Para aprender más acerca de OpenGL ES, puedes consultar sus páginas de referencia. Y para aprender más sobre desarrollo de aplicaciones Android, ¡asegúrate de revisar algunos de nuestros otros tutoriales aquí en Envato Tuts+!


Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.