La entrada anterior se centró la clase Compass, que describe la brújula. También presentaba la clase CompassRenderer, que es la encargada de dibujarla en su sitio. Vamos a ver esta clase con más detalle.

La clase CompassRenderer

   1: class CompassRenderer implements GLSurfaceView.Renderer, SensorEventListener {
   2:         private float   mAccelerometerValues[] = new float[3];
   3:         private float   mMagneticValues[] = new float[3];
   4:         private float rotationMatrix[] = new float[16];
   5:         private float remappedRotationMatrix[] = new float[16];
   6:
   7:         private Compass mCompass;
   8:
   9:     public CompassRenderer() {
  10:         mCompass = new Compass();
  11:     }

La clase CompassRenderer cumple dos funciones: capturar la información de los sensores y renderizar la brújula con esa información.

Implementación de la interfaz SensorEventListener

Como hemos visto en otras entradas, el método onSensorChanged es invocado cada vez que hay nueva información disponible de alguno de los sensores a los que nos hemos subscrito:

   1: @Override
   2: public void onSensorChanged(SensorEvent event) {
   3:        synchronized (this) {
   4:             switch(event.sensor.getType()) {
   5:             case Sensor.TYPE_ACCELEROMETER:
   6:                 mAccelerometerValues = event.values.clone();
   7:                 break;
   8:             case Sensor.TYPE_MAGNETIC_FIELD:
   9:                 mMagneticValues = event.values.clone();
  10:                 break;
  11:             default:
  12:                 break;
  13:             }
  14:        }
  15: }

Para obtener la matriz de rotación (como veremos después) sólo necesitamos el acelerómetro y el sensor de campo magnético.

Un punto a tener en cuenta en este caso es la sincronización. Al código de la clase CompassRenderer van a acceder dos threads: el principal y thread de OpenGL. Las llamadas a los métodos de los sensores vendrán del primero, mientras que las llamadas a los métodos de renderización vendrán del segundo. Esto quiere decir que las variables miembro mAccelerometerValues y mMagneticValues serán asignadas en un thread y leídas en el otro, por lo que usamos un lock para evitar que se lean mientras se están asignando.

Creación de la superficie OpenGL

   1: public void onSurfaceCreated(GL10 gl, EGLConfig config) {
   2:     /*
   3:      * By default, OpenGL enables features that improve quality
   4:      * but reduce performance. One might want to tweak that
   5:      * especially on software renderer.
   6:      */
   7:     gl.glDisable(GL10.GL_DITHER);
   8:
   9:     /*
  10:      * Some one-time OpenGL initialization can be made here
  11:      * probably based on features of this particular context
  12:      */
  13:      gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
  14:              GL10.GL_FASTEST);
  15:
  16:      gl.glClearColor(0,0,0,0);
  17: }

Cuando el GLThread crea la superficie ejecuta el método onSurfaceCreated. Las líneas 7 y 13 son optimizaciones que le indican a OpenGL que optimice el rendimiento sobre la calidad de la imagen. La línea 16 establece los valores que se utilizarán para limpiar el buffer de color.

   1: public void onSurfaceChanged(GL10 gl, int width, int height) {
   2:      gl.glViewport(0, 0, width, height);
   3:
   4:      /*
   5:       * Set our projection matrix. This doesn't have to be done
   6:       * each time we draw, but usually a new projection needs to
   7:       * be set when the viewport is resized.
   8:       */
   9:      float ratio = (float) width / height;
  10:      gl.glMatrixMode(GL10.GL_PROJECTION);
  11:      gl.glLoadIdentity();
  12:      gl.glFrustumf(-ratio, ratio, -1, 1, 1, 100);
  13: }

El método onSurfaceChanged después de creada la superficie, y adicionalmente cada vez que esta cambia de tamaño. En la línea 2 establecemos el viewport. Básicamente le decimos a sistema que utilice todo el alto y ancho de la pantalla para pintar el modelo. Las líneas 9-12 inicializan la matriz de proyección y establece la región  que se va a visualizar (ver por ejemplo este tutorial).

Dibujo de la brújula

Cada cuadro correspondiente a la animación de la brújula se dibuja en el método onDrawFrame:

   1: public void onDrawFrame(GL10 gl) {
   2:     // Get rotation matrix from the sensor
   3:     SensorManager.getRotationMatrix(rotationMatrix, null, mAccelerometerValues,
                                          mMagneticValues);
   4:     // As the documentation says, we are using the device as a compass in landscape 
          // mode
   5:     SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_Y,
                                              SensorManager.AXIS_MINUS_X,
                                              remappedRotationMatrix);
   6:
   7:     // Clear color buffer
   8:     gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
   9:
  10:     // Load remapped matrix
  11:     gl.glMatrixMode(GL10.GL_MODELVIEW);
  12:     gl.glLoadIdentity();
  13:     gl.glLoadMatrixf(remappedRotationMatrix, 0);
  14:
  15:     gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
  16:     gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
  17:
  18:     mCompass.draw(gl);
  19: }

En la línea 3 obtenemos la matriz de rotación a partir de los sensores. Como vamos a ver la imagen en formato horizontal giramos los ejes en la línea 5. Las líneas 11-13 cargan la matriz. Después habilitamos los arrays de vértices y colores en el cliente que como vimos usa la clase Compass, y finalmente llamamos al método de dibujo de la brújula (visto en la entrada anterior).

El código completo del proyecto está disponible aquí: ARCompass.