Gráficos de alto rendimiento en Android
El principal objetivo de la sesión consiste en hacer una introducción a los componentes de la interfaz de Android que nos darán soporte para poder reproducir gráficos de alto rendimiento (vídeo o gráficos 3D).
Surface View
Para mostrar gráficos propios podríamos usar un componente que herede de View
. Estos componentes funcionan bien si no necesitamos realizar repintados continuos o mostrar gráficos 3D.
Sin embargo, en el caso de tener una aplicación con una gran carga gráfica, como puede ser un videojuego, un reproductor de vídeo, o una aplicación que muestre gráficos 3D, en lugar de View
deberemos utilizar SurfaceView
. Esta última clase nos proporciona una superficie en la que podemos dibujar desde un hilo en segundo plano, lo cual libera al hilo principal de la aplicación de la carga gráfica.
Vamos a ver en primer lugar cómo crear subclases de SurfaceView
, y las diferencias existentes con View
.
Para crear una vista con SurfaceView
tendremos que crear una nueva subclase de dicha clase (en lugar de View
). Pero en este caso no bastará con definir el método onDraw
, ahora deberemos crearnos un hilo independiente y proporcionarle la superficie en la que dibujar (SurfaceHolder
). Además, en nuestra subclase de SurfaceView
también implementaremos la interfaz SurfaceHolder.Callback
que nos permitirá estar al tanto de cuando la superficie se crea, cambia, o se destruye.
Cuando la superficie sea creada pondremos en marcha nuestro hilo de dibujado, y lo pararemos cuando la superficie sea destruida. A continuación mostramos un ejemplo de dicha clase:
public class VistaSurface extends SurfaceView
implements SurfaceHolder.Callback {
HiloDibujo hilo = null;
public VistaSurface(Context context) {
super(context);
SurfaceHolder holder = this.getHolder();
holder.addCallback(this);
}
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
// La superficie ha cambiado (formato o dimensiones)
}
public void surfaceCreated(SurfaceHolder holder) {
hilo = new HiloDibujo(holder, this);
hilo.start();
}
public void surfaceDestroyed(SurfaceHolder holder) {
hilo.detener();
try {
hilo.join();
} catch (InterruptedException e) { }
}
}
Como vemos, la clase SurfaceView
simplemente se encarga de obtener la superficie y poner en marcha o parar el hilo de dibujado. En este caso la acción estará realmente en el hilo, que es donde especificaremos la forma en la que se debe dibujar el componente. Vamos a ver a continuación cómo podríamos implementar dicho hilo:
class HiloDibujo extends Thread {
SurfaceHolder holder;
VistaSurface vista;
boolean continuar = true;
public HiloDibujo(SurfaceHolder holder, VistaSurface vista) {
this.holder = holder;
this.vista = vista;
continuar = true;
}
public void detener() {
continuar = false;
}
@Override
public void run() {
while (continuar) {
Canvas c = null;
try {
c = holder.lockCanvas(null);
synchronized (holder) {
// Dibujar aqui los graficos
c.drawColor(Color.BLUE);
}
} finally {
if (c != null) {
holder.unlockCanvasAndPost(c);
}
}
}
}
}
Podemos ver que en el bucle principal de nuestro hilo obtenermos el lienzo (Canvas
) a partir de la superficie (SurfaceHolder
) mediante el método lockCanvas
. Esto deja el lienzo bloqueado para nuestro uso, por ese motivo es importante asegurarnos de que siempre se desbloquee. Para tal fin hemos puesto unlockCanvasAndPost
dentro del bloque finally
. Además debemos siempre dibujar de forma sincronizada con el objeto SurfaceHolder
, para así evitar problemas de concurrencia en el acceso a su lienzo.
Para aplicaciones como videojuegos 2D sencillos un código como el anterior puede ser suficiente (la clase View
sería demasiado lenta para un videojuego). Sin embargo, lo realmente interesante es utilizar SurfaceView
junto a OpenGL, para así poder mostrar gráficos 3D, o escalados, rotaciones y otras transformaciones sobre superficies 2D de forma eficiente.
A continuación veremos un ejemplo de cómo utilizar OpenGL (concretamente OpenGL ES) vinculado a nuestra SurfaceView
.
Realmente la implementación de nuestra clase que hereda de SurfaceView
no cambiará, simplemente modificaremos nuestro hilo, que es quien realmente realiza el dibujado. Toda la inicialización de OpenGL deberá realizarse dentro de nuestro hilo (en el método run
), ya que sólo se puede acceder a las operaciones de dicha librería desde el mismo hilo en el que se inicializó. En caso de que intentásemos acceder desde otro hilo obtendríamos un error indicando que no existe ningún contexto activo de OpenGL.
En este caso nuestro hilo podría contener el siguiente código:
public void run() {
initEGL();
initGL();
Triangulo3D triangulo = new Triangulo3D();
float angulo = 0.0f;
while(continuar) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
GL10.GL_DEPTH_BUFFER_BIT);
// Dibujar gráficos aquí
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, -5.0f);
gl.glRotatef(angulo, 0, 1, 0);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
triangulo.dibujar(gl);
egl.eglSwapBuffers(display, surface);
angulo += 1.0f;
}
}
En primer lugar debemos inicializar la interfaz EGL, que hace de vínculo entre la plataforma nativa y la librería OpenGL:
EGL10 egl;
GL10 gl;
EGLDisplay display;
EGLSurface surface;
EGLContext contexto;
EGLConfig config;
private void initEGL() {
egl = (EGL10)EGLContext.getEGL();
display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
int [] version = new int[2];
egl.eglInitialize(display, version);
int [] atributos = new int[] {
EGL10.EGL_RED_SIZE, 5,
EGL10.EGL_GREEN_SIZE, 6,
EGL10.EGL_BLUE_SIZE, 5,
EGL10.EGL_DEPTH_SIZE, 16,
EGL10.EGL_NONE
};
EGLConfig [] configs = new EGLConfig[1];
int [] numConfigs = new int[1];
egl.eglChooseConfig(display, atributos, configs,
1, numConfigs);
config = configs[0];
surface = egl.eglCreateWindowSurface(display,
config, holder, null);
contexto = egl.eglCreateContext(display, config,
EGL10.EGL_NO_CONTEXT, null);
egl.eglMakeCurrent(display, surface, surface, contexto);
gl = (GL10)contexto.getGL();
}
A continuación debemos proceder a la inicialización de la interfaz de la librería OpenGL:
private void initGL() {
int width = vista.getWidth();
int height = vista.getHeight();
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
float aspecto = (float)width/height;
GLU.gluPerspective(gl, 45.0f, aspecto, 1.0f, 30.0f);
gl.glClearColor(0.5f, 0.5f, 0.5f, 1);
}
Una vez hecho esto, ya sólo nos queda ver cómo dibujar una malla 3D. Vamos a ver como ejemplo el dibujo de un triángulo:
public class Triangulo3D {
FloatBuffer buffer;
float[] vertices = {
-1f, -1f, 0f,
1f, -1f, 0f,
0f, 1f, 0f };
public Triangulo3D() {
ByteBuffer bufferTemporal = ByteBuffer
.allocateDirect(vertices.length*4);
bufferTemporal.order(ByteOrder.nativeOrder());
buffer = bufferTemporal.asFloatBuffer();
buffer.put(vertices);
buffer.position(0);
}
public void dibujar(GL10 gl) {
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
}
}
Para finalizar, es importante que cuando la superficie se destruya se haga una limpieza de los recursos utilizados por OpenGL:
private void cleanupGL() {
egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE,
EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
egl.eglDestroySurface(display, surface);
egl.eglDestroyContext(display, contexto);
egl.eglTerminate(display);
}
Podemos llamar a este método cuando el hilo se detenga (debemos asegurarnos que se haya detenido llamando a join
previamente).
A partir de Android 1.5 se incluye la clase GLSurfaceView
, que ya incluye la inicialización del contexto GL y nos evita tener que hacer esto manualmente. Esto simplificará bastante el uso de la librería. Vamos a ver a continuación un ejemplo de cómo trabajar con dicha clase.
En este caso ya no será necesario crear una subclase de GLSurfaceView
, ya que la inicialización y gestión del hilo de OpenGL siempre es igual. Lo único que nos interesará cambiar es lo que se muestra en la escena. Para ello deberemos crear una subclase de GLSurfaceViewRenderer
que nos obliga a definir los siguientes métodos:
public class MiRenderer implements GLSurfaceView.Renderer {
Triangulo3D triangulo;
float angulo;
public MiRenderer() {
triangulo = new Triangulo3D();
angulo = 0;
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
public void onSurfaceChanged(GL10 gl, int w, int h) {
// Al cambiar el tamaño cambia la proyección
float aspecto = (float)w/h;
gl.glViewport(0, 0, w, h);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 45.0f, aspecto, 1.0f, 30.0f);
}
public void onDrawFrame(GL10 gl) {
gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
GL10.GL_DEPTH_BUFFER_BIT);
// Dibujar gráficos aquí
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, -5.0f);
gl.glRotatef(angulo, 0, 1, 0);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
triangulo.dibujar(gl);
angulo += 1.0f;
}
}
Podemos observar que será el método onDrawFrame
en el que deberemos escribir el código para mostrar los gráficos. Con hacer esto será suficiente, y no tendremos que encargarnos de crear el hilo ni de inicializar ni destruir el contexto.
Para mostrar estos gráficos en la vista deberemos proporcionar nuestro renderer al objeto GLSurfaceView
:
vista = new GLSurfaceView(this);
vista.setRenderer(new MiRenderer());
setContentView(vista);
Por último, será importante transmitir los eventos onPause
y onResume
de nuestra actividad a la vista de OpenGL, para así liberar a la aplicación de la carga gráfica cuando permanezca en segundo plano. El código completo de la actividad quedaría como se muestra a continuación:
public class MiActividad extends Activity {
GLSurfaceView vista;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
vista = new GLSurfaceView(this);
vista.setRenderer(new MiRenderer());
setContentView(vista);
}
@Override
protected void onPause() {
super.onPause();
vista.onPause();
}
@Override
protected void onResume() {
super.onResume();
vista.onResume();
}
}
Ejercicios
Antes de empezar a crear los proyectos, debes descargarte las plantillas desde el repositorio mastermoviles-multimedia-android
de bitbucket. En este repositorio se encuentran todas las plantillas que utilizaremos en los ejercicios Android de este módulo.
Gráficos 3D
En las plantillas de la sesión tenemos una aplicación Graficos
en la que podemos ver un ejemplo completo de cómo utilizar SurfaceView
tanto para gráficos 2D con el Canvas
como para gráficos 3D con OpenGL, y también de cómo utilizar GLSurfaceView
.
Si ejecutamos la aplicación veremos un triángulo rotando alrededor del eje Y. Observar el código fuente, y modificarlo para que el triángulo rote alrededor del eje X, en lugar de Y.
También podemos ver que hemos creado, además de la clase
Triangulo3D
, la claseCubo3D
. Modificar el código para que en lugar de mostrar el triángulo se muestre el cubo.
Last updated