OpenGL ES

Anuncio
 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​
​
Square​mSquare​
​
;
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​
. 
Descargar