teaching machines

CS 491 Lecture 21 – Bluecheckers

December 1, 2011 by . Filed under cs491 mobile, fall 2011, lectures.

Agenda

3-D on Android

Android offers a special GLSurfaceView for fast rendering. It also provides Java bindings for the OpenGL library. This added layer can reduce performance, so ultimately we’re going to want to use our low-level C++ code to do all the drawing. But let’s just use Java for a moment.

Let’s first make a class SphereView which extends GLSurfaceView and implements GLSurfaceView.Renderer. This bears some similarity to SurfaceView with its SurfaceHolder.Callback that we discussed a couple of weeks ago. It’s constructor sets up the drawing surface:

public SphereView(Context context,
                  AttributeSet attrs) {
  super(context, attrs);
  setEGLContextClientVersion(2);
  setRenderer(this);
}

We want OpenGL ES version 2.0, and the rendering callbacks will be implemented by this class. Under the hood, GLSurfaceView starts up a rendering thread not unlike the ones we made. We can distill it down to something along the lines of:

while (true) {
  synchronized(someLockObject) {
    if needsToStop return
    if eventQueued run event
    if surface needed creating, call renderer.onSurfaceCreated
    if surface resized, call renderer.onSurfaceChanged
    call renderer.onDrawFrame
  }
}

Let’s start our callbacks off with something really simple:

@Override
public void onDrawFrame(GL10 gl) {
  GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
  GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}

@Override
public void onSurfaceChanged(GL10 gl,
                             int width,
                             int height) {
  GLES20.glViewport(0, 0, width, height);
}

@Override
public void onSurfaceCreated(GL10 gl,
                             EGLConfig config) {
}

The good Android folks decided to hijack the interface used for OpenGL ES 1.*, which required an OpenGL instance to do all drawing. We ignore this argument in our callbacks. The first place we know the canvas size is onSurfaceChanged, so we declare there that our drawing will cover its entirety. In onDrawFrame, we clear the canvas green.

Okay, that’s as far as we go on the Java side of things.

Integrating native code

Okay, now it’s time to hook in our C++ sphere renderer. This’ll take a few steps:

  1. Install the Android NDK.
  2. Make a folder named jni sibling to src in your Eclipse project.
  3. Move or link all your C++ code over to the jni directory.
  4. Add jni/Application.mk:
    APP_STL := gnustl_static
  5. Add jni/Android.mk:
    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := sphere
    LOCAL_SRC_FILES := bridge.cpp BaseRenderer.cpp LogAndroid.cpp MessagedException.cpp Scanner.cpp \
                       ShaderProgram.cpp SphereRenderer.cpp Trimesh.cpp Utilities.cpp UtilitiesOpenGL.cpp \
                       VertexArray.cpp VertexAttributes.cpp WindowManager.cpp
    LOCAL_LDLIBS := -llog -lGLESv2
    
    include $(BUILD_SHARED_LIBRARY)
  6. Add a JNI bridge file. JNI provides a mechanism for running platform-dependent code through Java. The JNI system is very particularly about how you name your functions, i.e., Java_fully_qualified_Classname_methodName. One must also decorate them with JNIEXPORT and JNICALL. Each function has at least two arguments: a pointer to the virtual machine environment and the invoking object reference. You can specify further.
    #include <jni.h>
    
    #include "SphereRenderer.h"
    
    extern "C" {
      JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_initializeGL(JNIEnv *env, jobject obj);
      JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_setSize(JNIEnv *env, jobject obj, jint width, jint height);
      JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_drawFrame(JNIEnv *env, jobject obj);
    }
    
    SphereRenderer *renderer;
    
    JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_initializeGL(JNIEnv *env, jobject obj) {
      renderer = new SphereRenderer(0, 0, NULL);
      renderer->InitializeGL();
    }
    
    JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_setSize(JNIEnv *env, jobject obj, jint width, jint height) {
      renderer->SetSize(width, height);
    }
    
    JNIEXPORT void JNICALL Java_org_twodee_sphere_SphereLib_drawFrame(JNIEnv *env, jobject obj) {
      renderer->DrawFrame();
    }
  7. Add a SphereLib class that Javafies your C++ bridge:
    public class SphereLib {
      static {
        System.loadLibrary("sphere");
      }
    
      public static native void initializeGL();
    
      public static native void setSize(int width,
                                        int height);
    
      public static native void drawFrame();
    }
    
  8. Go to your project’s Properties. Click Builders. Add a new Programs Builder. For Location, point to the NDK build utility (I use ${system_property:user.home}/tarballs/android-ndk/ndk-build.) For working directory, pick your jni directory. In the Refresh tab, refresh the project. In the Build Options tab, rebuild whenever anything in jni changes.
  9. In your GLSurfaceView, call your wrapped C++ methods.

UI Events

Just as before, UI events will trigger UI callbacks on the UI thread. If in response we need to alter the state of the renderer, we need to be threadsafe. We can call a synchronized method, but the lock has to be the same one that the renderer itself uses. I don’t know that we have access to it.

The alternative is to use GLSurfaceView’s queueEvent method, which safely adds a Runnable of our devising to an event queue. That event will eventually get picked off by the drawing thread and executed.

Let’s add code so that the user can move the light source by tapping on the screen.

BlueCheckers

A couple of weeks ago I got the latest issue of Make magazine in the mail. On page 56, Charles Platt describes a 2-player Chinese Checkers game. I thought, “That’d be fun on a mobile device.” Let’s talk about its design. I’ll draw its interface…

Based on our sphere code, what would you do to make this app happen? And remember, you’re a computer scientist, so:

Next time, we’ll see how to persist our game state and add a Bluetooth player.

TODO