Una vez obtenida la imagen de la cámara pasamos a la brújula. Necesitaremos dos cosas: obtener los datos de orientación y dibujarlos en el lugar adecuado. Empezaremos inicializando los sensores y los datos iniciales de la brújula.

Empezando con OpenGL

Como dijimos en la entrada anterior vamos a usar OpenGL para dibujar la brújula. Una advertencia: antes de realizar esta serie de entradas no sabía nada de OpenGL, así que es probable es que el código que incluyo no sea muy ortodoxo, incluso puede que contenga errores. Aún así funciona correctamente en el HTC Magic.

Para aprender algo de OpenGL utilicé el libro “The OpenGL Programming Guide 5th Edition. The Official Guide to Learning OpenGL Version 2.1, también conocido como el OpenGL Red Book. Por supuesto hay gran cantidad de tutoriales de OpenGL por internet. Los [ejemplos de Android sobre OpenGL ES](http://developer.android.com/guide/samples/ApiDemos/src/com/example/android/apis/graphics/index.html) permiten hacerse una idea de cómo usar OpenGL en Android. Algunas de las clases que aparecen en esta entrada están basadas en esos ejemplos.

Lo primero que necesitamos para trabajar con OpenGL es una superficie sobre la que pintar. La SDK incluye una diseñada para OpenGL: GLSurfaceView, un tipo especial de SurfaceView cuya principal característica es que ejecuta el código de renderizado en un thread aparte (llamado GLThread). Para ello, a la vista GLSurfaceView se le asigna un renderizador, que es una clase que hereda de GLSurfaceView.Renderer:

   1: class CompassRenderer implements GLSurfaceView.Renderer, SensorEventListener {

En nuestro caso, dado que esta clase va a renderizar la brújula en base a los datos de los sensores, haremos que implemente también SensorEventListener (como veíamos en esta entrada), para tener los datos en la misma clase.

Pero primero tenemos que decirle a nuestra actividad que use esta vista además de la vista de la cámara:

   1: public class ARCompass extends Activity {
   2:     private SensorManager mSensorManager;
   3:     private CameraView mCameraView;
   4:     private GLSurfaceView mGLSurfaceView;
   5:
   6:
   7:     /** Called when the activity is first created. */
   8:     @Override
   9:     public void onCreate(Bundle savedInstanceState) {
  10:         super.onCreate(savedInstanceState);
  11:
  12:         // Hide the window title.
  13:         requestWindowFeature(Window.FEATURE_NO_TITLE);
  14:
  15:         mGLSurfaceView = new GLSurfaceView(this);
  16:         mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
  17:         CompassRenderer compassRenderer = new CompassRenderer(true);
  18:         mGLSurfaceView.setRenderer(compassRenderer);
  19:         mGLSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
  20:
  21:         setContentView(mGLSurfaceView);
  22:
  23:         mCameraView = new CameraView(this);
  24:         addContentView(mCameraView, new LayoutParams(LayoutParams.WRAP_CONTENT,
                                                         LayoutParams.WRAP_CONTENT));
  25:
  26:         mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  27:
  28:         List<Sensor> listSensors = mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
  29:         if (listSensors.size() > 0)
  30:         {
  31:             mSensorManager.registerListener(compassRenderer, listSensors.get(0),
                                                   SensorManager.SENSOR_DELAY_UI);
  32:         }
  33:
  34:         listSensors = mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);
  35:         if (listSensors.size() > 0)
  36:         {
  37:             mSensorManager.registerListener(compassRenderer, listSensors.get(0),
                                                                   SensorManager.SENSOR_DELAY_UI);
  38:         }
  39:     }
  40:  ...
  41: }

En las líneas 15-19 creamos la superficie OpenGL, establecemos los componentes de color RGB a 8 bits con profundidad de buffer a 60 bits, creamos nuestra clase CompassRenderer (que veremos después), le decimos a la superficie que vamos a renderizar en la clase CompassRenderer y finalmente establecemos en la superficie un formato de pixel que soporte varios de canal alfa. Este último paso es necesario para que la superficie sea transparente y poder ver lo que hay debajo, que será la imagen de la cámara.

En las líneas 21-24 asociamos la vista OpenGL a la actividad, creamos la vista de la cámara y la añadimos. La vista de la cámara queda así detrás de la vista OpenGL, visible porque esta última tiene una superficie transparente.

El resto del código registra los Listeners del acelerómetro y del sensor de campo magnético.

La clase Compass

La clase Compass le indica al renderizador las características del objeto a renderizar. Contiene un método draw que será llamado por el renderizador cada vez que quiera dibujar un frame:

   1: class Compass
   2: {
   3:     private FloatBuffer   mVertexBuffer;
   4:     private IntBuffer   mColorBuffer;
   5:     private ByteBuffer  mIndexBuffer;
   6:
   7:     public Compass()
   8:     {
   9:          ...
  10:     }
  11:
  12:     public void draw(GL10 gl)
  13:     {
  14:         gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
  15:         gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
  16:         gl.glDrawElements(GL10.GL_LINES, 32 + 6 + 10 + 8 + 8, GL10.GL_UNSIGNED_BYTE,
                                mIndexBuffer);
  17:     }
  18: }

La clase contiene tres buffers: uno con los vértices de cada primitiva a dibujar (en nuestro caso las primitivas son líneas), otro con los colores, y el último con el orden en el que se van a aplicar cada uno de los vértices. Los buffers se asignan en el constructor, como veremos después, y contienen una descripción de la posición y el color del modelo. En nuestro caso el modelo es una serie de líneas verticales y cuatro letras que se distribuyen en una circunferencia en el plano XY alrededor del punto (0, 0, 0).

La línea 14 define el array de vértices a partir de nuestro buffer. Son vértices de tres coordenadas, de tipo float, sin stride (desplazamiento entre vértices consecutivos, usado para empaquetar información de cada vértice, ver este artículo para más información). La línea 15 define el array de colores. Y la línea 16 renderiza las primitivas: indicamos el tipo de primitivas (en este caso líneas) y el número de elementos a renderizar (en este caso vértices, 32 de la brújula, 6, 10, 8 y 8 para cada letra).

Veamos ahora el constructor:

   1: public Compass()
   2:  {
   3:      int one = 0x10000;
   4:
   5:      int colorLines[] = {
   6:              0,  one,    0,  one,
   7:              0,  one,    0,  one,
   8:        };
   9:
  10:      int colorLetters[] = {
  11:              //North
  12:              one,  one,    0,  one,
  13:              one,  one,    0,  one,
  14:              one,  one,    0,  one,
  15:              one,  one,    0,  one,
  16:              one,  one,    0,  one,
  17:              one,  one,    0,  one,
  18:              // South
  19:              one,  one,    0,  one,
  20:              one,  one,    0,  one,
  21:              one,  one,    0,  one,
  22:              one,  one,    0,  one,
  23:              one,  one,    0,  one,
  24:              one,  one,    0,  one,
  25:              one,  one,    0,  one,
  26:              one,  one,    0,  one,
  27:              one,  one,    0,  one,
  28:              one,  one,    0,  one,
  29:              // East
  30:              one,  one,    0,  one,
  31:              one,  one,    0,  one,
  32:              one,  one,    0,  one,
  33:              one,  one,    0,  one,
  34:              one,  one,    0,  one,
  35:              one,  one,    0,  one,
  36:              one,  one,    0,  one,
  37:              one,  one,    0,  one,
  38:              // West
  39:              one,  one,    0,  one,
  40:              one,  one,    0,  one,
  41:              one,  one,    0,  one,
  42:              one,  one,    0,  one,
  43:              one,  one,    0,  one,
  44:              one,  one,    0,  one,
  45:              one,  one,    0,  one,
  46:              one,  one,    0,  one,
  47:      };
  48:
  49:      // Buffers to be passed to gl*Pointer() functions
  50:      // must be direct, i.e., they must be placed on the
  51:      // native heap where the garbage collector cannot
  52:      // move them.
  53:      //
  54:      // Buffers with multi-byte datatypes (e.g., short, int, float)
  55:      // must have their byte order set to native order
  56:
  57:      // (( vertices_per_compass_line * coords_per_vertex * lines_number) 
  58:      // + north_vertices * coords_per_vertex + south_vertices * coords_per_vertex 
  59:      // + east_vertices * coords_per_vertex + west_vertices * coords_per_vertex) 
  60:      // * bytes_per_float
  61:      ByteBuffer vbb = ByteBuffer.allocateDirect(((2 * 3 * 16) + (6 * 3) + (10 * 3) +
                                                       (8 * 3) + (8 * 3)) * 4);
  62:      vbb.order(ByteOrder.nativeOrder());
  63:      mVertexBuffer = vbb.asFloatBuffer();
  64:
  65:      // ((total_compass_vertices * coords_per_color) + 
  66:      // (north_vertices * coords_per_color)  + (south_vertices * coords_per_color))
  67:      // * bytes_per_int
  68:      ByteBuffer cbb = ByteBuffer.allocateDirect(((32 * 4) + (6 * 4) + (10 * 4) +
                                                        (8 * 4) + (8 * 4)) * 4);
  69:      cbb.order(ByteOrder.nativeOrder());
  70:      mColorBuffer = cbb.asIntBuffer();
  71:
  72:      mIndexBuffer = ByteBuffer.allocateDirect(32 + 6 + 10 + 8 + 8);
  73:      float x;
  74:      float y;
  75:      float z;
  76:      for (int i = 0; i < 16; i++)
  77:      {
  78:          if (i % 2 == 0)
  79:              if (i % 4 == 0)
  80:                  z = 6.0f;
  81:              else
  82:                  z = 4.0f;
  83:          else
  84:              z = 2.0f;
  85:
  86:          x = (float)(Math.sin(((double)i / 16) * 2 * Math.PI) * 32);
  87:          y = (float)(Math.cos(((double)i / 16) * 2 * Math.PI) * 32);
  88:          mVertexBuffer.put(x);
  89:          mVertexBuffer.put(y);
  90:          mVertexBuffer.put(-z);
  91:          mIndexBuffer.put((byte)(2 * i));
  92:
  93:          mVertexBuffer.put(x);
  94:          mVertexBuffer.put(y);
  95:          mVertexBuffer.put(z);
  96:          mIndexBuffer.put((byte)(2 * i + 1));
  97:
  98:          mColorBuffer.put(colorLines);
  99:      }
 100:
 101:      float north[] = {
 102:          -2.0f, 32.0f, 7.0f,
 103:          -2.0f, 32.0f, 11.0f,
 104:          -2.0f, 32.0f, 11.0f,
 105:          2.0f, 32.0f, 7.0f,
 106:          2.0f, 32.0f, 7.0f,
 107:          2.0f, 32.0f, 11.0f,
 108:      };
 109:      mVertexBuffer.put(north);
 110:      byte indices[] = {
 111:              32, 33, 34, 35, 36, 37,
 112:      };
 113:      mIndexBuffer.put(indices);
 114:
 115:      float south[] = {
 116:              2.0f, -32.0f, 7.0f,
 117:              -2.0f, -32.0f, 7.0f,
 118:              -2.0f, -32.0f, 7.0f,
 119:              -2.0f, -32.0f, 9.0f,
 120:              -2.0f, -32.0f, 9.0f,
 121:              2.0f, -32.0f, 9.0f,
 122:              2.0f, -32.0f, 9.0f,
 123:              2.0f, -32.0f, 11.0f,
 124:              2.0f, -32.0f, 11.0f,
 125:              -2.0f, -32.0f, 11.0f,
 126:      };
 127:      mVertexBuffer.put(south);
 128:      indices = new byte[]{
 129:              38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
 130:      };
 131:      mIndexBuffer.put(indices);
 132:
 133:      float east[] = {
 134:              32.0f, -2.0f, 7.0f,
 135:              32.0f, 2.0f, 7.0f,
 136:              32.0f, -2.0f, 9.0f,
 137:              32.0f, 2.0f, 9.0f,
 138:              32.0f, -2.0f, 11.0f,
 139:              32.0f, 2.0f, 11.0f,
 140:              32.0f, 2.0f, 7.0f,
 141:              32.0f, 2.0f, 11.0f,
 142:      };
 143:      mVertexBuffer.put(east);
 144:      indices = new byte[]{
 145:              48, 49, 50, 51, 52, 53, 54, 55,
 146:      };
 147:      mIndexBuffer.put(indices);
 148:
 149:      float west[] = {
 150:              -32.0f, 2.0f, 11.0f,
 151:              -32.0f, 1.0f, 7.0f,
 152:              -32.0f, 1.0f, 7.0f,
 153:              -32.0f, 0, 9.0f,
 154:              -32.0f, 0, 9.0f,
 155:              -32.0f, -1.0f, 7.0f,
 156:              -32.0f, -1.0f, 7.0f,
 157:              -32.0f, -2.0f, 11.0f,
 158:      };
 159:      mVertexBuffer.put(west);
 160:      indices = new byte[]{
 161:              56, 57, 58, 59, 60, 61, 62, 63,
 162:      };
 163:      mIndexBuffer.put(indices);
 164:
 165:      mColorBuffer.put(colorLetters);
 166:
 167:      mColorBuffer.position(0);
 168:      mVertexBuffer.position(0);
 169:      mIndexBuffer.position(0);
 170:  }

Esta parte contiene código aparentemente ilógico, como arrays cuyos valores se asignan a mano en vez de en bucle. La idea es que se vea con más claridad de dónde sale cada cosa y para qué se usa.

La variable colorLines contiene los colores de los dos vértices de una línea (en este caso verde, para una explicación del color ver esta sección del libro rojo). Como vamos a crear cada línea en un bucle usaremos siempre ese array. La variable colorLetters contiene el color (amarillo) de cada vértice de cada línea de cada letra. Vamos a asignar los vértices de las letras una a una, así que esta variable contiene un color por vértice.

Después creamos los buffers, teniendo cuidado a la hora de dar el tamaño correcto en bytes de cada array.

En las líneas 76-99 vemos el bucle que crea cada línea de la brújula. Imaginamos un círculo de radio 32 en el plano XY. Las líneas (un total de 16) serán verticales a ese plano, con longitudes de 12 para las correspondientes a N, S, E y O, 8 para las que indiquen NO, NE, SE y SO, y 4 para las demás. La coordenada z se asigna al principio. Después se calculan x e y en función del seno y el coseno del ángulo (dividimos 2*PI en 16 porciones y vamos haciendo las cuentas).  En las líneas 88-96 asignan cada vértice, donde se puede ver que para cada línea de la brújula solo varía la coordenada z.

La variable mIndexBuffer contiene un array de bytes que le indacarán al renderizador en qué orden se deben procesar los vértices (en este array se puede indicar, por ejemplo, que se repitan vértices ya utilizados). El bucle acaba añadiendo los datos de color para cada vértice.

El resto del constructor es más de lo mismo, pero para las letras. Se dibujan las letras N, S, W, E con líneas, de una forma muy simple, y además utilizando como primitivas líneas sueltas, en vez de líneas continuas (se podía haber usado la primitiva GL_LINE_STRIP para no repetir los vértices que son comunes a más de una línea).

En la siguiente y última entrada, veremos cómo mostrar todo esto en pantalla.