Android provides a class called GLSurfaceView
, which is a widget that is drawn by OpenGL. The drawing logic is encapsulated via an interface called GLSurfaceView.Renderer
, which we will implement in ARCubeRenderer
. The interface requires the following methods:
onDrawFrame(GL10 gl)
: It is called to draw the current frame.onSurfaceChanged(GL10 gl, int width, int height)
: It is called when the surface size changes. For our purposes, this method does not need to do anything.onSurfaceCreated(GL10 gl, EGLConfig config)
: It is called when the surface is created or recreated. For our purposes, this method does not need to do anything.The GL10
instance, which is passed as an argument, provides access to the standard OpenGL ES 1.0 functionality. Basically, we are interested in two kinds of OpenGL functionality; applying matrix transformations to 3D vertices and then drawing triangles based on the transformed vertices. Our cube will have eight vertices and 12 triangles (six square faces * two triangles per square face). We will specify a color for each vertex and we will describe the triangles in a format called a triangle fan
. A triangle fan is an array of 3 or more vertices. For each vertex v[i]
in the fan, where i >= 2
, a triangle is formed by v[0]
, v[i-1]
, and v[i]
.
Taking any vertex in the cube, we may imagine six triangles (three square faces) fanning out from that vertex. Thus, two triangle fans are enough to specify the 12 triangles, provided that we start the fans from opposite corners of the cube.
Vertices, vertex colors, and triangle fans are all stored in ByteBuffer
instances. Since we only support one style of cube, we will use static instances of ByteBuffer
so that multiple ARCubeRenderer
instances may share them. As member variables, we also want ARFilter
to provide the cube's pose matrix, a CameraProjectionAdapter
to provide the projection matrix, and a scale to allow client code to resize the cube. The declarations of ARCubeRenderer
and its variables are as follows:
public class ARCubeRenderer implements GLSurfaceView.Renderer { public ARFilter filter; public CameraProjectionAdapter cameraProjectionAdapter; public float scale = 100f; private static final ByteBuffer VERTICES; private static final ByteBuffer COLORS; private static final ByteBuffer TRIANGLE_FAN_0; private static final ByteBuffer TRIANGLE_FAN_1;
Since the vertices, colors, and triangle fans are static
variables, we initialize them in a static
block. For each buffer, we must specify the required number of bytes. The vertices take up 96 bytes (8 vertices * 3 floats per vertex * 4 bytes per float). We specify vertices for a cube that is 2 units wide. After populating the buffer, we rewind its pointer to the first index. The code is as follows:
static { VERTICES = ByteBuffer.allocateDirect(96); VERTICES.order(ByteOrder.nativeOrder()); VERTICES.asFloatBuffer().put(new float[] { -1f, 1f, 1f, 1f, 1f, 1f, 1f, -1f, 1f, -1f, -1f, 1f, -1f, 1f, -1f, 1f, 1f, -1f, 1f, -1f, -1f, -1f, -1f, -1f }); VERTICES.position(0);
The vertex colors take up 32 bytes (8 vertices * 4 bytes of RGBA color per vertex). We specify a different color for each vertex, as seen in the following code:
COLORS = ByteBuffer.allocateDirect(32); COLORS.put(new byte[] { // yellow Byte.MAX_VALUE, Byte.MAX_VALUE, 0, Byte.MAX_VALUE, // cyan 0, Byte.MAX_VALUE, Byte.MAX_VALUE, Byte.MAX_VALUE, // black 0, 0, 0, Byte.MAX_VALUE, // magenta Byte.MAX_VALUE, 0, Byte.MAX_VALUE, Byte.MAX_VALUE, Byte.MAX_VALUE, 0, 0, Byte.MAX_VALUE, // red 0, Byte.MAX_VALUE, 0, Byte.MAX_VALUE, // green 0, 0, Byte.MAX_VALUE, Byte.MAX_VALUE, // blue 0, 0, 0, Byte.MAX_VALUE // black }); COLORS.position(0);
The two triangle fans take up 18 bytes each (6 triangles * 3 vertex indices per triangle). We specify fans that are based at the cube's far upper-right and near lower-left corners, as seen in the following code:
TRIANGLE_FAN_0 = ByteBuffer.allocate(18); TRIANGLE_FAN_0.put(new byte[] { 1, 0, 3, 1, 3, 2, 1, 2, 6, 1, 6, 5, 1, 5, 4, 1, 4, 0 }); TRIANGLE_FAN_0.position(0); TRIANGLE_FAN_1 = ByteBuffer.allocate(18); TRIANGLE_FAN_1.put(new byte[] { 7, 4, 5, 7, 5, 6, 7, 6, 2, 7, 2, 3, 7, 3, 0, 7, 0, 4 }); TRIANGLE_FAN_1.position(0); }
When drawing to an instance of GLSurfaceView
, we first clear any previous content by replacing it with a fully transparent color. Then, we check whether a projection matrix and pose matrix are available. If they are, we tell OpenGL to use these matrices and to also move and scale the cube so that we have an appropriately sized cube sitting atop the target. Then, we supply the vertices and vertex colors to OpenGL and tell it to draw the triangle fans. The implementation is as follows:
@Override public void onDrawFrame(final GL10 gl) { gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glClearColor(0f, 0f, 0f, 0f); // transparent if (filter == null) { return; } if (cameraProjectionAdapter == null) { return; } float[] pose = filter.getGLPose(); if (pose == null) { return; } gl.glMatrixMode(GL10.GL_PROJECTION); float[] projection = cameraProjectionAdapter.getProjectionGL(); gl.glLoadMatrixf(projection, 0); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadMatrixf(pose, 0); gl.glTranslatef(0f, 0f, 1f); gl.glScalef(scale, scale, scale); gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); gl.glVertexPointer(3, GL11.GL_FLOAT, 0, VERTICES); gl.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, COLORS); gl.glDrawElements(GL10.GL_TRIANGLE_FAN, 18, GL10.GL_UNSIGNED_BYTE, TRIANGLE_FAN_0); gl.glDrawElements(GL10.GL_TRIANGLE_FAN, 18, GL10.GL_UNSIGNED_BYTE, TRIANGLE_FAN_1); }
Finally, to satisfy the rest of the GLSurfaceView.Renderer
interface, we provide empty implementations of onSurfaceChanged
and onSurfaceCreated
, as seen in the following code:
@Override public void onSurfaceChanged(final GL10 gl, final int width, final int height) { } @Override public void onSurfaceCreated(final GL10 arg0, final EGLConfig config) { }
Now, we are ready to integrate 3D tracking and rendering into our application.