Archive

Posts Tagged ‘android’

OpenGL ES 2D para Android

agosto 29, 2009 Comments off

Estuve buscando un ejemplo sencillo de cómo inicializar OpenGL ES en Android con una proyección 2D, como para un juego de plataformas o algo así.

El Android SDK 1.5 incluye unos ejemplos de OpenGL, pero todos me parecieron innecesariamente complicados y es difícil comenzar un proyecto sencillo a partir de allí. Así que uní dos de ellos (SpriteText y Triangle) y los simplifiqué de forma que fuera más sencillo ver todo el proceso.

NOTA: Esto no es un tutorial de OpenGL. Voy a asumir que usted ya tiene algo de experiencia en OpenGL para así solo tener que explicar qué hace cada bloque de código y no función por función.

Al final de este ejemplo podremos mostrar una imagen centrada en la pantalla y girando como la del siguiente screenshot:

Android OpenGL ES 2D sample

Lo primero que vamos a hacer es crear un nuevo Activity y en el evento onCreate agregarle una vista GLSurfaceView:

public class MainActivity extends Activity implements GLSurfaceView.Renderer {
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		mGLSurfaceView = new GLSurfaceView(this);
		mGLSurfaceView.setRenderer(this);
		setContentView(mGLSurfaceView);
	}

La llamada a setRenderer es porque en Android, todo el renderizado debe realizarlo una clase que implemente la interfaz GLSurfaceView.Renderer. Normalmente esto sería una clase separada pero para simplificar las cosas el mismo Activity implementa la interfaz y así le podemos pasar this.

Esta interfaz define tres métodos que implementaremos a continuación: onSurfaceCreated(), onSurfaceChanged() y onDrawFrame().

El primero, onSurfaceCreated(), se llama en cuanto la superficie esté lista para ser utilizada. Aquí incluiremos toda la inicialización que vayamos a hacer. Este método es un poco largo así que vamos por partes:

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
	gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);
	gl.glDisable(GL10.GL_DEPTH_TEST);
	gl.glDisable(GL10.GL_DITHER);
	gl.glDisable(GL10.GL_LIGHTING);

	gl.glEnable(GL10.GL_TEXTURE_2D);
	gl.glClearColor(0, 0, 0, 1);
	gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

Primero deshabilitamos algunas características de OpenGL ES que no necesitamos en 2D y luego habilitamos las texturas y limpiamos la pantalla.

Antes de continuar con el método vamos a ver las coordenadas que se utilizarán para dibujar la imagen.

En OpenGL ES no disponemos de las funciones glVertex3f() que se acostumbra utilizar en el OpenGL completo. En vez de eso se pasan todos las coordenadas de cada punto (vertex) en arreglos de floats y un arreglo de ushorts especifica el orden en que se dibujarán.

Esta técnica se conoce como VBO y en vez de explicar en detalle como funciona pueden leer el tutorial OpenGL Vertex Buffer Object de Song Ho Ahn que es excelente.

Estas son las coordenadas que utilicé:

private static final int TEXTURE_WIDTH = 128;
private static final int TEXTURE_HEIGHT = 128;
private static final float[] sVertexValues = new float[] {
	-TEXTURE_WIDTH/2, -TEXTURE_HEIGHT/2, 0,
	 TEXTURE_WIDTH/2, -TEXTURE_HEIGHT/2, 0,
	-TEXTURE_WIDTH/2,  TEXTURE_HEIGHT/2, 0,
	 TEXTURE_WIDTH/2,  TEXTURE_HEIGHT/2, 0,
};
private static final float[] sTexCoordValues = new float[] {
	0, 1,
	1, 1,
	0, 0,
	1, 0,
};
private static final char[] sIndexValues = new char[] {
	0, 1, 2,
	1, 2, 3,
};

Como ven, sVertexValues es un cuadrado de 128 pixels centrado en (0,0). El cuadrado lo dibujamos utilizando dos triángulos que definimos en sIndexValues. Algo como esto.

Coordenadas para dibujar un cuadrado con OpenGL ES

Es decir, sVertexValues son las coordenadas para A, B, C y D. De forma similar, sTexCoordValues es la posición de la textura y sIndexValues es el orden en que se deben utilizar los vertex para dibujar los dos triángulos: A, B y C para el primero y B, C y D para el segundo.

En cualquier lenguaje racional simplemente podríamos simplemente pasar esos arreglos a la función OpenGL y listo, pero en Java tenemos que evitar que garbage collector mueva los arreglos mientras estamos usándolos así que lo que hace el siguiente bloque de código es copiar las coordenadas a objetos Buffer:

final int FLOAT_SIZE = 4;
final int CHAR_SIZE = 2;
mVertexBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * sVertexValues.length)
	.order(ByteOrder.nativeOrder()).asFloatBuffer();
mTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * sTexCoordValues.length)
	.order(ByteOrder.nativeOrder()).asFloatBuffer();
mIndexBuffer = ByteBuffer.allocateDirect(CHAR_SIZE * sIndexValues.length)
	.order(ByteOrder.nativeOrder()).asCharBuffer();

for (int i = 0; i < sVertexValues.length; ++i)
	mVertexBuffer.put(i, sVertexValues[i]);
for (int i = 0; i < sTexCoordValues.length; ++i)
	mTexCoordBuffer.put(i, sTexCoordValues[i]);
for (int i = 0; i < sIndexValues.length; ++i)
	mIndexBuffer.put(i, sIndexValues[i]);

Yep. Es un bloque código bastante estúpido (e innecesario en cualquier otro lenguaje) pero lo necesitamos porque estamos programando en Java. Pero continuemos: Ahora vamos a cargar la textura (la imagen que vamos a mostrar):

gl.glGenTextures(1, sTextureIDWorkspace, 0);
int textureID = sTextureIDWorkspace[0];

gl.glBindTexture(GL10.GL_TEXTURE_2D, textureID);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);

Acabamos de crear una textura en memoria de video y le cambiamos algunas opciones de configuración.

	Bitmap bitmap;
	InputStream is = getResources().openRawResource(R.drawable.robot);
	try {
		bitmap = BitmapFactory.decodeStream(is, null, sBitmapOptions);
	} finally {
		try {
			is.close();
		} catch (IOException e) {
			// Ignore
		}
	}

	GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
	bitmap.recycle();

	Runtime r = Runtime.getRuntime();
	r.gc();
}

Y en este bloque cargamos el bitmap desde un archivo de recursos (R.drawable.robot), lo convertimos al formato correcto y lo asignamos a la textura que habíamos creado. Finalmente es un buen momento para llamar al garbage collector.

Fiuuu. Por fin terminamos onSurfaceCreated(). Ese era el método más complicado. Ahora vamos a ver onSurfaceChanged(). Este método es llamado después de onSurfaceCreated() y cuando el usuario gira el teléfono así que es el lugar perfecto para configurar la proyección:

public void onSurfaceChanged(GL10 gl, int width, int height) {
	gl.glViewport(0, 0, width, height);
	gl.glMatrixMode(GL10.GL_PROJECTION);
	gl.glLoadIdentity();
	gl.glOrthof(0, width, 0, height, 0, 1);
	gl.glShadeModel(GL10.GL_FLAT);
	gl.glEnable(GL10.GL_BLEND);
	gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
	gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
	gl.glEnable(GL10.GL_TEXTURE_2D);
}

Aquí cambiamos la proyección a ortográfica que es la que se utiliza para dibujar en 2D y configuramos algunas cosas adicionales para las texturas.

En onDrawFrame() por fin vamos a dibujar algo.

public void onDrawFrame(GL10 gl) {
	gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
	gl.glMatrixMode(GL10.GL_MODELVIEW);
	gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
	gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
	gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureID);
	gl.glPushMatrix();
	gl.glLoadIdentity();

Este bloque es sencillo: Limpiamos la pantalla y le decimos a OpenGL ES que vamos a pasarle las coordenadas en arreglos (glEnableClientState() con GL_VERTEX_ARRAY y GL_TEXTURE_COORD_ARRAY). Le decimos también cual textura vamos a utilizar y finalmente nos aseguramos que estemos en (0,0).

gl.glTranslatef(160, 240, 0);
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
gl.glRotatef(angle, 0, 0, 1.0f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mTexCoordBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, sIndexValues.length, GL10.GL_UNSIGNED_SHORT,
	mIndexBuffer);

Con eso nos movemos al centro de la pantalla y giramos la imagen según el milisegundo en el que nos encontremos. Enviamos las coordenadas a OpenGL y el se encarga de dibujar los dos triángulos.

	gl.glPopMatrix();
	gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
	gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}

Devolvemos todo a como lo encontramos y terminamos de pintar el frame.

Pueden descargar el código de ejemplo de OpenGL ES 2D para Android SDK 1.5. Está bajo la misma licencia del SDK ya que está basado en los ejemplos que se incluyen allí.

El proyecto está para NetBeans (Lo siento pero Eclipse me da muchos problemas) así que tendrán que instalar NBAndroid o importarlo manualmente en un proyecto nuevo de Eclipse.

Categories: General Tags: , ,