OpenGL ES Contenido 1 Introducción 2 Primer programa OpenGL 2.1 Inicialización de OpenGL 2.2 Construcción de la clase Renderer 3 Trabajo domiciliario 3.1 Resumen 3.2 Comentario adicional 4 Laboratorio sobre Primer programa OpenGL 4.1 Plan de la clase 4.1.1 Dibujar un triángulo en la pantalla 4.1.2 Definición de un Triángulo 4.1.3 Inicializar las Formas 4.1.4 Dibujo de la forma 5 Actividad en el laboratorio 6 Anexo 6.1 Clase Triangle 6.2 Definiciones útiles Este documento debe ser leído previamente a la clase, así como el primer programa OpenGL deben realizarse como actividades domiciliarias. En el laboratorio comenzaremos con el punto cuarto. 1 Introducción OpenGL ES define una API1 para la representación de gráficos. No define un sistema de ventanas. OpenGL ES está diseñado para ser combinado con una biblioteca que sabe cómo crear y acceder a las ventanas a través del sistema operativo para permitir que trabaje en una variedad de plataformas, en Android esta biblioteca se llama EGL. Cuando se quiera poner el rendering2 de OpenGL en la pantalla, se utilizan llamadas a la biblioteca EGL. Dice wikipedia: “La interfaz de programación de aplicaciones , abreviada como API (del inglés : Application Programming Interface), es el conjunto de subrutinas , funciones y procedimientos (o métodos , en la programación orientada a objetos ) que ofrece cierta biblioteca para ser utilizado por otro software como una capa de abstracción.” 2 Rendering: proceso de dibujar en la pantalla, usando programas, un objeto 2D (bidimensional) o 3D (tridimensional). 1 NOTA Los siguientes ejemplos fueron extraídos de las guías de capacitación de Android Developers donde se pueden ver en su original en inglés: Building an OpenGL ES Environment . Nosotros, además de traducirlos, hemos agregado muchas explicaciones, la mayoría de las cuales provienen de OpenGL ES 2 for Android 2 Primer programa OpenGL Para dibujar gráficos con OpenGL ES en su aplicación para Android, se debe crear un contenedor de vista para los mismos. Una de las formas más directas para hacer esto es poner en práctica a la vez un objeto de tipo GLSurfaceView y un objeto de tipo GLSurfaceView.Renderer . El GLSurfaceView es un contenedor de vista de los gráficos de dibujo con OpenGL y GLSurfaceView.Renderer controla lo que se dibuja dentro de esa vista. 2.1 Inicialización de OpenGL Como ya se aclaró, en este ejemplo vamos a inicializar OpenGL mediante el uso de la clase GLSurfaceView . Esta lección introductoria explica cómo llevar a cabo una implementación mínima de GLSurfaceView y GLSurfaceView.Renderer en una actividad sencilla. GLSurfaceView se encarga de los aspectos más duros de inicialización de OpenGL, como la configuración de la pantalla y la representación en un subproceso que corre en segundo plano (background thread) . Esta representación (rendering) se realiza en un área especial de la pantalla, llamada superficie ; a esto también se refiere a veces como una ventana gráfica (viewport) . Las aplicaciones Android que utilizan OpenGL ES, al igual que cualquier otra aplicación, deben interactuar con el usuario y lo hacen a través de una interfaz de usuario. La principal diferencia con otras aplicaciones es lo que se pone como diseño (layout) de su actividad. Muchas aplicaciones usan widgets3 de tipo TextView , Button y ListView . En una aplicación que utiliza OpenGL ES también se puede agregar al layout un GLSurfaceView , como lo hace el siguiente ejemplo que muestra una implementación mínima de una actividad que utiliza un GLSurfaceView como su vista principal. Lo primero que haremos es crear un proyecto en Android Studio. Cerramos el proyecto anterior y procedemos como se explicó en el documento 1-AndroidSDK (File->New->New Project) creamos un proyecto Android con Application name : OpenGLES20 3 widget es un elemento reutilizable de la interfaz gráfica. Company Domain: iie.fing.edu.uy. Marcamos Phone and tablet , Minimun SDK : API 14. En Add Activity to Movile : Blank Activity nombre OpenGLES20Activity Verificar en el Manifiesto que el nombre de la actividad sea OpenGLES20Activity Lo que se va a hacer en este ejemplo es inicializar OpenGL y borrar la pantalla continuamente. Eso es lo mínimo que necesitamos tener en un programa de OpenGL que realmente hace algo. Esto se define en el archivo “OpenGLES20Activity.java”. La implementación se muestra a continuación: public class OpenGLES20Activity extends Activity { private GLSurfaceView glSurfaceView ; Override @ public void onCreate ( Bundle savedInstanceState ) { super . onCreate ( savedInstanceState ); / / Crear una instancia GLSurfaceView y ponerla // como contenido de la vista de esta Actividad. glSurfaceView = new MyGLSurfaceView ( this ); setContentView ( glSurfaceView ); } } Esto es todo el código necesario, por tanto deberán sustituir el código generado por AndroidStudio por este código, dejando solo la declaración del package al principio: package uy.edu.fing.iie.opengles20; Para que esto funcione debe importar ciertas bibliotecas que definen algunas cosas que utilizamos. Para ello se agrega arriba, justo después de la declaración del “package”, lo siguiente: import android.app.Activity; import android.opengl.GLSurfaceView; import android.os.Bundle; En general, cuando AndroidStudio detecta algún error lo señala poniendo en rojo la palabra que corresponda al error. Una ayuda del sistema es que si se coloca el puntero en esa palabra y se oprime a la vez Alt+Enter, se ofrecen opciones de solución. En particular si faltan declaraciones en general es porque se ha olvidado importar las bibliotecas correspondientes, en ese caso es posible posicionarse en la línea de código correspondiente y oprimir a la vez Alt+Enter, entonces AndroidStudio sugiere las bibliotecas que hay que importar. Observar el Manifiesto, que en la sección “activity” debe contener algo por el estilo de <activity android:name=". OpenGLES20Activity " android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> Esta actividad principal, en lugar de usar directamente GLSurfaceView lo que hace es usar una clase especial, que vamos a crear a continuación con el nombre MyGLSurfaceView y que será la que contiene la vista. Observe que el AndroidStudio subraya en rojo MyGLSurfaceView, indicando que es una variable desconocida. La crearemos a continuación. Para ello, vamos a la ventana a la derecha, donde está el árbol con todos los archivos de la aplicación y dentro del directorio “java” posicionamos el cursor sobre el package (uy.edu.fing.iie.opengles20) y creamos una nueva clase mediante botón derecho -> New->Java Class a la que damos el nombre MyGLSurfaceView. Esto crea un nuevo archivo en nuestro package con nombre MyGLSurfaceView.java, que contiene una clase pública del mismo nombre (por el momento vacía), y la cual haremos que “extienda” (extends) GLSurfaceView . El código esencial para una GLSurfaceView mínima (simplemente actualiza el contexto y crea un renderizador que se encargará de dibujar) es el siguiente: public class MyGLSurfaceView extends GLSurfaceView { private final MyGLRenderer mRenderer ; public MyGLSurfaceView ( Context context ){ super ( context ); // Create an OpenGL ES 2.0 context setEGLContextClientVersion ( 2 ); super .setEGLConfigChooser( 8 , 8 , 8 , 8 , 16 , 0 ); //para simulador mRenderer = new MyGLRenderer (); // Set the Renderer for drawing on the GLSurfaceView setRenderer ( mRenderer ); // Render the view only when there is a change in the drawing data //setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } } Este código debe sustituir al código vacío que nos había propuesto AndroidStudio: public class MyGLSurfaceView { } Resolviendo los errores de importación como se dijo anteriormente (con Alt+Enter sobre las palabras subrayadas en rojo) se logrará importar dos bibliotecas: import android.content.Context; import android.opengl.GLSurfaceView; Pero no logramos resolver MyGLRenderer , lo cual se debe a que no la hemos creado aún. Esta clase, GLSurfaceView, no hace mucho por sí misma, ya que el dibujo real de los objetos se controla en la clase MyGLRenderer de la cual creamos aquí un objeto mRenderer = new MyGLRenderer() y cuya implementación se hará más adelante. Esta clase va a implementar la interfaz GLSurfaceView.Renderer que configuramos en esta vista. De hecho, el código de GLSurfaceView es tan mínimo que podríamos tener la tentación de eliminarlo y simplemente crear una instancia de GLSurfaceView en nuestra actividad principal. La hemos agregado con el fin de capturar eventos de touch , que usaremos más adelante. Otra adición opcional a GLSurfaceView es establecer el modo de hacer que sólo se dibuje la vista cuando hay un cambio en los datos del entorno de dibujo utilizando: GLSurfaceView.RENDERMODE_WHEN_DIRTY poniendo al final de la clase, después del constructor el siguiente código: // Hacer el Render solo cuando se produce un cambio en los datos del dibujo setRenderMode ( GLSurfaceView . RENDERMODE_WHEN_DIRTY ); Esta configuración hace que el marco GLSurfaceView no se vuelva a dibujar hasta que se llame requestRender() , lo que resulta más eficiente para esta aplicación de ejemplo. 2.2 Construcción de la clase Renderer La implementación de la clase GLSurfaceView.Renderer , o procesador (renderer), dentro de una aplicación que utiliza OpenGL ES es donde las cosas empiezan a ponerse interesantes. Esta clase controla lo que se dibuja en la GLSurfaceView con la que está asociado. Hay tres métodos en un renderer que son llamados por el sistema Android para dibujar en una GLSurfaceView : ● ● ● onSurfaceCreated() - Llamado una sola vez para establecer el entorno de OpenGL ES de la vista. onDrawFrame() - Llamado para cada redibujado de la vista. onSurfaceChanged() - Llamado si la geometría de la vista cambia, por ejemplo, cuando la orientación de la pantalla del dispositivo cambia. De la misma forma que hicimos con la clase anterior (es decir posicionándonos sobre el package y haciendo botón derecho -> New->Java Class), creamos una nueva clase con nombre MyGLRenderer. Para que sea un renderizador debe implementar la interfaz GLSurfaceView.Renderer. A continuación damos un código muy básico de un renderizador OpenGL ES, que no hace más que dibujar un fondo rojo en la GLSurfaceView : public class MyGLRenderer implements GLSurfaceView . Renderer { public void onSurfaceCreated ( GL10 unused , EGLConfig config ) { // Restablecer el color de fondo a rojo. // El primer componente es el rojo, el segundo es verde, // el tercero es azul, y el último componente es alfa, // que no utilizamos en esta lección. GLES20 . glClearColor ( 1.0f , 0.0f , 0.0f , 1.0f ); } /** * OnDrawFrame se llama toda vez que se necesita dibujar un nuevo cuadro. en cada refresco de pantalla. */ public void onDrawFrame ( GL10 unused ) { // Redibujar el color de fondo GLES20 . glClear ( GLES20 . GL_COLOR_BUFFER_BIT ); } / ** * OnSurfaceChanged se llama cada vez que la superficie ha cambiado. * Esto se llama al menos una vez cuando se inicializa la superficie. * Tenga en cuenta que Android normalmente reinicia una actividad en cada * rotación, y en ese caso, el procesador será destruido y uno nuevo creado. * * @param width * El nuevo ancho, en pixeles. * @param height * La nueva altura, en pixeles. */ } public void onSurfaceChanged ( GL10 unused , int width , int height ) { GLES20 . glViewport ( 0 , 0 , width , height ); } ¡Eso es todo al respecto de este primer ejemplo! *Normalmente esto se hace 3 Trabajo domiciliario Hacer prácticamente lo que se explicó: Crear un proyecto con nombre OpenGLES20 en iie.fing.edu.uy cuya actividad principal sea l a O penGLES20Activity que ya hemos explicado. Luego en el mismo package agregamos las clases MyGLSurfaceView y MyGLRenderer con los contenidos vistos arriba. L uego cargamos el código al simulador o en el celular y lo probamos. Si todo salió bien, deberá aparecer una pantalla de color rojo. 3.1 Resumen Lo que hemos realizado es crear una aplicación de Android sencilla que muestra una pantalla roja usando OpenGL. Aunque este código no hace nada muy interesante, mediante la creación de estas clases hemos sentado las bases que necesitamos para empezar a dibujar los elementos gráficos con OpenGL. 3.2 Comentario adicional En los días en que todo se hacía en el software, por lo general era un desperdicio borrar la pantalla. El proceso se optimiza suponiendo que se puede pintar por encima de todo, por lo que no hay necesidad de borrar cosas de la trama anterior. Esto se hacía para ahorrar tiempo de procesamiento. Esta optimización ya no es útil en la actualidad. Las últimas GPU funcionan de forma diferente, y utilizan técnicas de renderizado especiales que en realidad pueden trabajar más rápido si se borra la pantalla. Al decirle a la GPU que borre la pantalla, ahorramos tiempo que se habría perdido en la copia del cuadro anterior. Debido a la forma en que las GPUs trabajan hoy en día, la limpieza de la pantalla también ayuda a evitar problemas como el parpadeo o a cosas que no se dibujan. Preservar el contenido anterior puede conducir a resultados inesperados e indeseables. Los interesados en aprender más sobre este tema pueden seguir los siguientes enlaces: • http://developer.amd.com/gpu_assets/gdc2008_ribble_maurice_TileBasedGpus.pdf • http://www.beyond3d.com/content/articles/38/ 4 Laboratorio sobre Primer programa OpenGL Se supone que todos han leído y probado, previo al laboratorio, la aplicación que se explica en el trabajo domiciliario. De lo contrario, en la clase no hay tiempo suficiente para hacer todo lo que se propone. 4.1 Plan de la clase ● ● ● Primero vamos a crear un nuevo proyecto con los mismos elementos del trabajo domiciliario. Es decir, la primera actividad será la de borrar la pantalla. Luego empezaremos a dibujar figuras geométricas. Lo primero será hacer un triángulo ubicado en el centro de la pantalla. El triángulo presentará deformaciones de escala debido a la forma en que que se representa la pantalla en OpenGL, por ese motivo vamos a aplicar una matriz de transformación que lo adapte al display Suponemos que en Android Studio tenemos creado el proyecto OpenGLES20 de la actividad domiciliaria. Modificar la clase OpenGLES20Activity de la siguiente forma: package uy.edu.fing.iie. opengles20 ; // class OpenGLES20Activity import android.app.Activity; import android.opengl.GLSurfaceView; import android.os.Bundle; public class OpenGLES20Activity extends Activity { private GLSurfaceView glSurfaceView; Override @ public void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); } / / Create a GLSurfaceView instance and set it // as the ContentView for this Activity glSurfaceView = new MyGLSurfaceView( this ); setContentView(glSurfaceView); Override @ protected void onPause() { super .onPause(); // The following call pauses the rendering thread. // If your OpenGL application is memory intensive, // you should consider de-allocating objects that // consume significant memory here. glSurfaceView.onPause(); } } Override @ protected void onResume() { super .onResume(); // The following call resumes a paused rendering thread. // If you de-allocated graphic objects for onPause() // this is a good place to re-allocate them. glSurfaceView.onResume(); } Comentarios respecto al código anterior En este programa hemos agregado al trabajo domiciliario la gestión de los eventos de tiempo de vida de las actividades en Android. Control de los eventos de actividad del ciclo de vida de Android Tenemos que controlar los eventos de actividad del ciclo de vida de Android; de lo contrario el celular va a bloquearse si el usuario cambia a otra aplicación. Vamos a añadir los siguientes métodos para redondear nuestra clase de actividad: Métodos onPause(), onResume() Es muy importante contar con estos métodos para que nuestra superficie de vista pueda pausar y reanudar correctamente el hilo de la representación de fondo (background rendering thread), así como la liberación y renovación del contexto OpenGL. Si no lo hacemos, nuestra aplicación puede bloquearse y ser matada por Android. Lo siguiente que haremos es crear las clases MyGLSurfaceView y MyGLRenderer con los contenidos que ya se vieron. Renderizado en un subproceso en segundo plano (Thread) Los métodos de renderizado serán llamados en un hilo separado por el GLSurfaceView. El GLSurfaceView procesará continuamente por defecto, por lo general a la tasa de refresco de la pantalla, pero también se puede configurar la vista de superficie para trabajar sólo bajo petición llamando GLSurfaceView.setRenderMode(), con GLSurfaceView.RENDERMODE_WHEN_DIRTY como argumento. Ahora estamos listos para probar nuestro código y ver qué pasa. Usted debe ver una pantalla roja. Intente cambiar el color y después ejecutar el programa de nuevo para ver qué pasa. Usted debe ver que el color en la pantalla coincide con los cambios en el código. Fíjese que el color se fija en la clase MyGLRenderer que está en el archivo MyGLRenderer.java En Resumen Este ejercicio corresponde a la tarea domiciliaria, con el agregado de que ahora respondemos al ciclo de vida de la actividad de Android y limpiamos la pantalla. Ahora tenemos una base que podemos aprovechar para todos nuestros proyectos en 2D. En los próximos ejercicios, vamos a seguir construyendo sobre esta base, aprenderemos a programar la GPU, y añadir más características. 4.1.1 Dibujar un triángulo en la pantalla Vamos a empezar por aprender a construir objetos mediante el uso de un conjunto de puntos independientes conocidos como vértices , y luego vamos a aprender a dibujar objetos mediante el uso de shaders , pequeños programas que comunican a OpenGL cómo dibujar un objeto. Estos dos conceptos son muy importantes porque casi todos los objetos se construyen uniendo vértices en puntos , líneas y triángulos , y todas estas primitivas son dibujadas mediante el uso de shaders . Primero vamos a aprender acerca de los vértices para que podamos construir nuestra imagen y posicionarla en el mundo usando coordenadas espaciales OpenGL. Definición de formas Ser capaz de definir las formas que se elaborarán en el marco de una vista OpenGL ES es el primer paso en la creación de gráficos. Dibujar figuras con OpenGL ES puede ser un poco difícil sin saber algunas cosas básicas sobre cómo OpenGL ES espera definir objetos gráficos. Esta lección explica el sistema de coordenadas OpenGL ES con relación a la pantalla del dispositivo Android, los conceptos básicos de la definición de una forma, las caras de la forma, así como la definición de un triángulo (y e ventualmente un cuadrado). Luego continuaremos mediante la creación de un conjunto de shaders muy básicos para dibujar estas figuras en la pantalla y, a medida que avanzamos, vamos a aprender acerca de los colores y la interacción por touch (es decir, tocando la pantalla). Sistema de coordenadas OpenGL Por ahora, todo lo que necesitamos saber es que OpenGL asignará la pantalla en el rango [-1, 1], tanto para la coordenada X como para la Y. Esto significa que el borde izquierdo de la pantalla corresponderá a -1 en el eje X, mientras que el borde derecho de la pantalla corresponde a 1. El borde inferior de la pantalla corresponde a -1 en el eje Y, mientras que el borde superior de la pantalla corresponderá a 1. Figura 1: Representación de la pantalla en coordenadas OpenGL Por defecto, OpenGL ES supone un sistema de coordenadas espaciales X,Y,Z ( es decir en tres dimensiones, o como nos gusta abreviar, en 3D). Nosotros vamos a trabajar en dos dimensiones (2D) por lo cual ponemos la coordenada Z=0: (X, Y, Z) [0, 0, 0] especifica el centro del marco GLSurfaceView, [1, 1, 0] es la esquina superior derecha del marco y [- 1, -1, 0] es la esquina inferior izquierda del marco. Para una ilustración de este sistema de coordenadas, consulte la guía del desarrollador de OpenGL ES. Tenga en cuenta que las coordenadas de los puntos de una figura se definen en un orden siguiendo el sentido antihorario. El orden de dibujo es importante porque define qué cara es la cara frontal de la forma. 4.1.2 Definición de un Triángulo OpenGL ES nos permite definir objetos y dibujarlos usando coordenadas en el espacio tridimensional. Por lo tanto, antes de poder dibujar un triángulo, debemos definir sus coordenadas. En OpenGL, la forma típica de hacer esto es definir un conjunto de vértices mediante sus coordenadas (números en punto flotante). Nuestra figura va a ser plana y la ubicamos en el plano (X,Y) por lo cual Z = 0.0f para todos los puntos. Poner una f al final del número indica que estamo trabajando en formato punto flotante. Podemos entonces declarar un triángulo de la siguiente forma: static float triangleCoords [] = { // en sentido antihorario: 0.0f , 0.433f , 0.0f , // 1 arriba - 0.5f , - 0.433f , 0.0f , // 2 abajo izquierda }; 0.5f , - 0.433f , 0.0f // 3 abajo derecha Son las coordenadas aproximadas de los vértices de un triángulo equilátero de base 1 centrado en la pantalla (altura sen(60º)). A estos efectos debemos crear una clase Triangle en el mismo package y cuyo archivo Triangle.java diga lo siguiente: package uy.edu.fing.iie. opengles20 ; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; public class Triangle { private FloatBuffer vertexBuffer ; // número de coordenadas por vértice en este arreglo static final int COORDS_PER_VERTEX = 3 ; static float triangleCoords [] = { // sentido antihorario: 0.0f , 0.433f , 0.0f , // 1 arriba - 0.5f , - 0.433f , 0.0f , // 2 abajo izquierda }; 0.5f , - 0.433f , 0.0f // 3 abajo derecha // Establecer color con valores de rojo, verde, azul y alfa (opacidad) float color [] = { 0.63671875f , 0.76953125f , 0.22265625f , 1.0f }; public Triangle () { // inicializar el buffer de los vértices bb con las coordenadas de la forma ByteBuffer bb = ByteBuffer . allocateDirect ( triangleCoords . length * 4 ); // (numero de valores * 4 bytes por float) // usar el orden de bytes de la plataforma (endianness) bb . order ( ByteOrder . nativeOrder ()); // create a floating point buffer from the ByteBuffer vertexBuffer = bb . asFloatBuffer (); // add the coordinates to the FloatBuffer vertexBuffer . put ( triangleCoords ); // set the buffer to read the first coordinate } } vertexBuffer . position ( 0 ); Para una máxima eficiencia, se escriben estas coordenadas en un ByteBuffer , que se pasa al pipeline de gráficos OpenGL ES para su procesamiento ( ByteBuffer bb) 4.1.3 Inicializar las Formas Antes de hacer cualquier dibujo, se deben inicializar y cargar las formas que planeamos dibujar. A menos que la estructura (las coordenadas originales) de las formas que utiliza cambien durante el curso de la ejecución del programa, las figuras se deben inicializar en el método onSurfaceCreated() del renderizado para obtener eficiencia de memoria y de procesamiento. Introducimos los siguiente cambios en nuestra clase MyGLRenderer : public class MyGLRenderer implements GLSurfaceView . Renderer { ... private Triangle mTriangle ; private SquaremSquare ; public void onSurfaceCreated ( GL10 unused , EGLConfig config ) { ... } } ... / / initialize a triangle mTriangle = new Triangle (); // initialize a square mSquare = new Square (); // no lo usamos Noten que aparecerá subrayado en rojo, indicando un error por no poder resolver el símbolo tanto bajo la variable Triangle como Square. Triangle será resuelto cuando creemos la clase Triangle. Para resolver el error asociado a Square pueden comentar las líneas asociadas a Square por el momento y más adelante hacer una clase square y descomentarlas. 4.1.4 Dibujo de la forma Dibujar una forma definida usando OpenGL ES 2.0 requiere una cantidad significativa de código, ya que se debe proporcionar una gran cantidad de detalles al pipeline de representación de gráficos. En concreto, debemos definir lo siguiente: ● ● ● Vertex Shader - Código OpenGL ES de gráficos para la presentación de los vértices de una figura. Fragment Shader - Código OpenGL ES para sombrear la cara de una forma con colores o texturas. Program - Un objeto OpenGL ES que contiene los shaders que desea utilizar para la elaboración de una o más formas. Necesitamos al menos un vertex shader para dibujar una forma y un fragment shader para colorear esa forma. Estos shaders se deben compilar y luego se añaden a un programa de OpenGL ES, que luego se utiliza para dibujar la forma. En Java el código shader se representa mediante una cadena de caracteres (String). He aquí un ejemplo de cómo definir shaders básicos que se pueden utilizar para dibujar una forma en la clase Triangle . Editamos dicha clase y agregamos a lo anterior lo siguiente: public class Triangle { private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}" ; private final String fragmentShaderCode = "precision mediump float;" + "uniform vec4 vColor;" + "void main() {" + " gl_FragColor = vColor;" + "}" ; // Aca sigue lo que ya había en la clase ... } Los Shaders contienen un código en lenguaje OpenGL Shading Language ( GLSL ) que debe ser compilado antes de ser utilizados en el entorno OpenGL ES. Para compilar este código, cree un método de utilidad en su clase MyGLRenderer : public static int loadShader ( int type , String shaderCode ){ // create a vertex shader type (GLES20.GL_VERTEX_SHADER) // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) int shader = GLES20 . glCreateShader ( type ); // add the source code to the shader and compile it GLES20 . glShaderSource ( shader , shaderCode ); GLES20 . glCompileShader ( shader ); return shader ; } Con el fin de dibujar la forma, se deben compilar los códigos shader, agregarlos a un objeto de programa OpenGL ES y luego vincularlos al programa. Esto se puede hacer en el constructor de su objeto dibujado (triángulo en nuestro caso), por lo que sólo se realiza una vez4 . Modificar nuevamente la clase triángulo agregando lo siguiente en los lugares adecuados: public class Triangle () { // variables anteriores ... private final int mProgram ; public Triangle () { // codigo anterior ... int vertexShader = MyGLRenderer . loadShader ( GLES20 . GL_VERTEX_SHADER , vertexShaderCode ); int fragmentShader = MyGLRenderer . loadShader ( GLES20 . GL_FRAGMENT_SHADER , fragmentShaderCode ); // create empty OpenGL ES Program mProgram = GLES20 . glCreateProgram (); // add the vertex shader to program GLES20 . glAttachShader ( mProgram , vertexShader ); // add the fragment shader to program GLES20 . glAttachShader ( mProgram , fragmentShader ); // creates OpenGL ES program executables GLES20 . glLinkProgram ( mProgram ); } } En este punto, ya estamos listos para añadir las llamadas reales que dibujarán la forma. El dibujo de formas con OpenGL ES requiere que se especifiquen varios parámetros para decirle al canal de renderizado lo que quiere dibujar y cómo dibujarlo. Puesto que las opciones de dibujo pueden variar de forma, es una buena idea hacer que las clases de forma (triángulo en este caso) contengan su propia lógica de dibujo. En la misma clase Triangle, cree un método draw() para dibujar la forma triángulo. Este código establece los valores de posición y de color para los shaders de vértices y fragmentos, y luego ejecuta la función de dibujo. Incorporar a principio de la clase las declaraciones de variables: private int mPositionHandle ; private int mColorHandle ; private final int vertexCount = triangleCoords . length / COORDS_PER_VERTEX ; private final int vertexStride = COORDS_PER_VERTEX * 4 ; // 4 bytes per vertex y agregar al final la definición de draw(): public void draw () { // Add program to OpenGL ES environment GLES20 . glUseProgram ( mProgram ); // get handle to vertex shader's vPosition member mPositionHandle = GLES20 . glGetAttribLocation ( mProgram , "vPosition" ); // Enable a handle to the triangle vertices GLES20 . glEnableVertexAttribArray ( mPositionHandle ); // Prepare the triangle coordinate data 4 La compilación de los shaders OpenGL ES y los programas de vinculación son costosos en términos de ciclos de CPU y tiempo de procesamiento, por lo que debe evitar hacer esto más de una vez. Si usted no sabe el contenido de sus shaders en tiempo de ejecución, debe crear su código de manera que sólo se creen una vez y luego se almacenen en caché para su uso posterior. GLES20 . glVertexAttribPointer ( mPositionHandle , COORDS_PER_VERTEX , GLES20 . GL_FLOAT , false , vertexStride , vertexBuffer ); } / / get handle to fragment shader's vColor member mColorHandle = GLES20 . glGetUniformLocation ( mProgram , "vColor" ); // Set color for drawing the triangle GLES20 . glUniform4fv ( mColorHandle , 1 , color , 0 ); // Draw the triangle GLES20 . glDrawArrays ( GLES20 . GL_TRIANGLES , 0 , vertexCount ); // Disable vertex array GLES20 . glDisableVertexAttribArray ( mPositionHandle ); Una vez que tienes todo este código en su lugar, la elaboración de este objeto sólo requiere una llamada al método draw() desde dentro del método onDrawFrame() de MyGLRenderer : public void onDrawFrame ( GL10 unused ) { ... } mTriangle . draw (); Para que una aplicación use OpenGL ES 2.0 API, se debe realizar la siguiente declaración en el manifiesto: <? xml version= "1.0" encoding= "utf-8" ?> < manifest xmlns: android = "http://schemas.android.com/apk/res/android" package= "uy.edu.fing.iie.opengles20" > < uses-feature android :glEsVersion= "0x00020000" android :required= "true" /> < application ... Cuando se ejecuta la aplicación, se debe ver algo como esto: Figura 2: Triángulo sin ajustar las escalas. Hay algunos problemas con este ejemplo de código. En primer lugar, no vas a impresionar a tus amigos. En segundo lugar, el triángulo tiene una forma estirada y cambia al cambiar la orientación de la pantalla del dispositivo. La razón de que la forma esté sesgada es debido al hecho de que los vértices del objeto no han sido corregidos para las proporciones de la zona de la pantalla donde se muestra el GLSurfaceView . Podemos solucionar este problema utilizando las vistas de proyección y de cámara, cosa que haremos en la próxima lección. Por último, el triángulo es estacionario, lo que resulta un poco aburrido. En la lección de añadir movimiento ( Adding Motion ), podremos hacer rotar esta forma y hacer un uso más interesante del pipeline de gráficos OpenGL ES. 5 Actividad en el laboratorio Modificar la clase Triangle para: ● ubicar el triángulo en otra posición. ● cambiar el color del triángulo 6 Anexo A. 6.1 Clase Triangle A continuación damos el código completo de la clase Triangle // Triangle.java package uy.edu.fing.iie. opengles20 ; import android.opengl.GLES20; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; public class Triangle { private final String vertexShaderCode = "attribute vec4 vPosition;" + "void main() {" + " gl_Position = vPosition;" + "}" ; private final String fragmentShaderCode = "precision mediump float;" + "uniform vec4 vColor;" + "void main() {" + " gl_FragColor = vColor;" + "}" ; rivate p FloatBuffer vertexBuffer ; // número de coordenadas por vértice en este arreglo static final int COORDS_PER_VERTEX = 3 ; static float triangleCoords [] = { // sentido antihorario: }; .0f 0 , 0.433f , 0.0f , // 1 arriba - 0.5f , - 0.433f , 0.0f , // 2 abajo izquierda 0.5f , - 0.433f , 0.0f // 3 abajo derecha / / Establecer color con valores de rojo, verde azul y alfa (opacidad) float color [] = { 0.63671875f , 0.76953125f , 0.22265625f , 1.0f }; private final int mProgram ; public Triangle() { / / inicializar el buffer de los vértices bb con las coordenadas de la forma ByteBuffer bb = ByteBuffer. allocateDirect ( triangleCoords . length * 4 ); // (numero de valores * 4 bytes por float) // usar el orden de bytes de la plataforma (endianness) bb.order(ByteOrder. nativeOrder ()); / / create a floating point buffer from the ByteBuffer vertexBuffer = bb.asFloatBuffer(); // add the coordinates to the FloatBuffer vertexBuffer .put( triangleCoords ); // set the buffer to read the first coordinate vertexBuffer .position( 0 ); int vertexShader = MyGLRenderer.loadShader(GLES20. GL_VERTEX_SHADER , vertexShaderCode ); int fragmentShader = MyGLRenderer.loadShader(GLES20. GL_FRAGMENT_SHADER , fragmentShaderCode ); // create empty OpenGL ES Program mProgram = GLES20. glCreateProgram (); // add the vertex shader to program GLES20. glAttachShader ( mProgram , vertexShader); // add the fragment shader to program GLES20. glAttachShader ( mProgram , fragmentShader); // creates OpenGL ES program executables GLES20. glLinkProgram ( mProgram ); } rivate int p mPositionHandle ; private int mColorHandle ; private final int vertexCount = triangleCoords . length / COORDS_PER_VERTEX ; private final int vertexStride = COORDS_PER_VERTEX * 4 ; // 4 bytes per vertex public void draw() { // Add program to OpenGL ES environment GLES20. glUseProgram ( mProgram ); // get handle to vertex shader's vPosition member mPositionHandle = GLES20. glGetAttribLocation ( mProgram , "vPosition" ); // Enable a handle to the triangle vertices GLES20. glEnableVertexAttribArray ( mPositionHandle ); // Prepare the triangle coordinate data GLES20. glVertexAttribPointer ( mPositionHandle , COORDS_PER_VERTEX , GLES20. GL_FLOAT , false , vertexStride , vertexBuffer ); } / / get handle to fragment shader's vColor member mColorHandle = GLES20. glGetUniformLocation ( mProgram , "vColor" ); // Set color for drawing the triangle GLES20. glUniform4fv ( mColorHandle , 1 , color , 0 ); // Draw the triangle GLES20. glDrawArrays (GLES20. GL_TRIANGLES , 0 , vertexCount ); // Disable vertex array GLES20. glDisableVertexAttribArray ( mPositionHandle ); } B. 6.2 Definiciones útiles Rendering El renderizado es el proceso de generar un modelo de imagen 2D o 3D (o modelos que colectivamente se podrían llamar: archivos de escena ), por medio de programas de ordenador. Además, al resultado de un modelo de este tipo se le puede llamar una representación . Un archivo de escena contiene objetos de un idioma o datos de una estructura estrictamente definidos; que contendría la geometría, el punto de vista, la textura , la iluminación , y la información de sombreado , como una descripción de la escena virtual. A continuación se pasan los datos contenidos del archivo de escena, a un programa de renderizado para procesar y la salida a un archivo de imagen digital o gráfico de trama . Las computadoras han experimentado un cambio significativo en los últimos años con la introducción de una tarjeta de video independiente y el aumento de la aceleración de gráficos por hardware[4] . Esto ha llevado a la necesidad de crear un pipeline de gráficos programable que puede ser manipulado por los shaders .[5] Shader Un shader es un programa que le dice a un ordenador cómo dibujar algo de una manera específica y única. Los shaders calculan los efectos de renderizado en el hardware de gráficos con un alto grado de flexibilidad. La mayoría de los shaders se codifican para una unidad de procesamiento de gráficos ( GPU ) específica, aunque esto no es un requisito estricto. Se suelen utilizar lenguajes de sombreado para programar el canal de renderizado de la GPU. La posición, tono, saturación, brillo y contraste de todos los píxeles , vértices o texturas utilizadas para construir una imagen final pueden ser modificados sobre la marcha, utilizando algoritmos definidos en el sombreado, y pueden ser modificados por variables externas o texturas introducidas por el programa llamando shader . GPU Una GPU es un dispositivo especialmente diseñado, capaz de ayudar a una CPU en la realización de los cálculos de renderizado complejos. Si una escena debe parecer relativamente realista y previsible en condiciones de iluminación virtual, el software de renderización debe resolver la ecuación de renderizado . A medida que las GPU evolucionaron, las principales bibliotecas de software de gráficos como OpenGL y Direct3D comenzaron a proveer los shaders necesarios. Las primeras GPU con capacidad-shader sólo admitían el sombreado de píxeles, pero luego se introdujeron rápidamente los vertex shaders una vez que los desarrolladores se dieron cuenta del poder de shaders. Shaders de geometría se introdujeron recientemente con Direct3D 10 y OpenGL 3.2. La canalización básica de gráficos es la siguiente: ● La CPU envía instrucciones (programas compilados en lenguaje de sombreado )y datos de la geometría a la unidad de procesamiento de gráficos, que se encuentra en la tarjeta gráfica. ● Dentro del vertex shader , la geometría se transforma. ● Si un shader de geometría está activo, en la unidad de procesamiento gráfico se realizan algunos cambios en las geometrías de la escena. ● Si un shader teselación se encuentra en la unidad de procesamiento gráfico y activa, las geometrías de la escena se pueden subdividir. ● La geometría calculada se triangula (subdividido en triángulos). ● Los triángulos se descomponen en fragmentos quads (un fragmento quad es un fragmento primitivo de 2 × 2). ● Quads Fragmento se modifican de acuerdo con el fragment shader . ● Se realiza el examen a fondo, los fragmentos que pasan se graban en la pantalla y puede ser que consigan ser mezclados en el frame buffer . El pipeline de gráficos utiliza estos pasos con el fin de transformar los datos tridimensionales (y / o bidimensionales) en datos bidimensionales útiles para mostrar. En general, esta es una matriz de píxeles grande o " frame buffer ". GLSL La forma más sencilla de incluir el código glsl en Java es usando Strings como vimos arriba, pero también se puede poner el código glsl en el directorio res/raw del proyecto y escribir un programa Java que lo lea desde allí y otro que lo compile, lo cual complica un poco las cosas. En GLSL el programa sería : //vertex_shader_code.glsl attribute vec4 vPosition; void main() { gl_Position = vPosition; } // fragment_shader_code.glsl precision mediump float; uniform vec4 vColor; void main() { gl_FragColor = vColor; } Para más detalles de cómo usar esta forma ver el libro OpenGL ES 2 for Android .