Chapter 5. Hybrid 3D Graphics with OpenGL and JNI

The classic Asteroids arcade game presented in the previous chapter provided a great introduction to drawing techniques in Android, using polygons and user-defined layouts. Now it's time to ramp things up a notch.

In this chapter, you will learn a neat trick to mix OpenGL code in Java and C. This is a key step in reusing large portions of OpenGL C code along with Java code, thus using the best features of each language for maximum savings in time and costs.

Any game developer knows that OpenGL is the holy grail of advanced game development. You won't find any powerful games that are not written with this API, because it takes advantage of hardware acceleration, which is infinitely superior to any kind of software renderer.

OpenGL can be a scary subject to the newcomer due to its complexity. But you don't have to be an OpenGL guru to understand what it does and how to draw elements with this API. All you need is the desire to learn a powerful and exciting tool for gaming.

The goal of this chapter is not to teach you OpenGL (a whole book wouldn't be enough for that), but to show you how you can take the Android OpenGL sample provided by Google and modify it in a completely different way by mixing OpenGL API calls in both Java and native C for maximum reusability.

Some may say this is simply another OpenGL chapter for a mobile device (dime a dozen, right?). Well, it is not. This chapter presents a technique for OpenGL in Android that is unique, and at the time of this writing, not available anywhere in the Android sphere (on the Web). This is a technique I stumbled on by accident when thinking about porting the game Quake to Android. In a nutshell, the technique consists of creating the OpenGL context, display, and surface objects in Java, and performing all drawing operations natively in C. At the end of the rendering cycle, a JNI callback is used by the C engine to tell the Java side to swap the buffers (render the image). The cycle then repeats itself. This technique is extremely useful when you have a 200,000-line code game like Quake, and rewriting this code in Java is simply not feasible.

The chapter starts by examining the OpenGL tumbling cubes sample to expose how OpenGL works in Java. Next, we will look at how sections of the rendering process can be implemented in the native layer, and how everything is bound by JNI. The final section discusses some of the limitations of the OpenGL OpenGL Embedded System when it comes to advanced 3D games.

Let's get started.

The Power of Mobile Devices

Mobile Android devices have become pretty powerful for graphics development. Check out the following hardware stats for the T-Mobile G1:

  • ARM processor running at 500MHz

  • Graphics processing unit (GPU) with 256KB of RAM

  • 320-by-480 pixel display

To make good use of the GPU, Google has included the OpenGL Embedded System (ES) within Android. OpenGL ES provides the software API to make high-performance, hardware-accelerated games possible. This is a Java API, which is good news for Java developers who wish to create 3D games from scratch, but bad news for C developers who wish to reuse 3D engines written in C. 3D game engines are very complex and large, and are mostly written in C. Rewriting these engines in Java would be a very difficult task, consuming significant development and time resources.

Consider how easy it is to reuse OpenGL code in C. Let's look at another powerful smart phone: Apple's iPhone. If you search the iPhone App Store (or the Web), you will find that dozens of OpenGL-based 3D games have already been ported to the platform, including some of the greatest 3D shooters for the PC: Wolfenstein 3D, Doom, and Quake I. Even Quake III Arena—a game that has extremely advanced 3D graphics for a mobile device—has been ported! What do all these games have in common? They are written in C. Furthermore, Apple provides a C toolchain that makes it easy to have the games running in the platform. Clearly, Android is at a big disadvantage in this field. Nevertheless, porting these games to Android is still possible.

Even though Android supports only Java development, the Android OS is built in a stripped version of GNU Linux featuring a C runtime. Using an ARM C toolchain, you can write and compile C code and bind it to Java using JNI.

Note

As noted in previous chapters, Google doesn't support this type of native development, but it seems Google is being pushed by the overwhelming number of 3D games being ported to the iPhone and other mobile devices. So much so, that Google has recently released the Android NDK, a set of tools and header files for native development, introduced in Chapter 1.

OpenGL the Java Way

Let's look at how OpenGL graphics are done within Java. For this exploration, you need to create a project to hold the GL tumbling cubes application from the Android samples. Here is how:

  1. Click the New Android Project button.

  2. In the New Android Project dialog box, enter a project name, such as ch05.OpenGL.

  3. Specify the build target as Android 1.5.

  4. Enter an application name, such as OpenGL Java.

  5. Enter a package name, such as opengl.test.

  6. Select Create Activity and enter JavaGLActivity.

  7. Specify the minimum SDK version as 3. Figure 5-1 shows the completed dialog box for this example.

  8. Click Finish.

Note

The original sample code will be modified to fit the changes described throughout this chapter.

New Android project for the OpenGL sample

Figure 5-1. New Android project for the OpenGL sample

The Android cubes sample consists of the following Java classes (see Figure 5-2):

  • GLSurfaceView: This is an implementation of SurfaceView that uses a dedicated surface for displaying an OpenGL animation. The animation will run in a separate thread (GLThread).

  • GLThread: This is a generic thread with a loop for GL operations. Its job is to perform resource initialization. It also delegates rendering to an instance of the Renderer interface.

  • Renderer: This is a generic interface for rendering objects. In this case, we will be rendering two tumbling cubes.

  • EglHelper: This is a GL helper class used to do the following:

    • Initialize the EGL context.

    • Create the GL surface.

    • Swap buffers (perform the actual drawing).

  • CubeRenderer: This is an implementation of the Renderer interface to draw the cubes.

  • Cube: This class encapsulates a GL cube, including vertices, colors, and indices for each face.

Because the sample needs to be slightly modified to illustrate the concepts of the chapter, the following classes have been added for this purpose:

  • JavaGLActivity: This is the Android activity that will start the Java-only version of the application.

  • NativeGLActivity: This activity will start the hybrid version of the sample (with Java/C/JNI code).

  • Natives: This class defines the native methods used by this sample.

Resource list for the OpenGL sample

Figure 5-2. Resource list for the OpenGL sample

The Android manifest needs to be updated to include the new activities defined in the previous paragraph, as shown in bold in Listing 5-1.

Example 5-1. Manifest File for This Chapter's Example

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="opengl.test"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon"
        android:label="@string/app_name">
        <activity android:name=".JavaGLActivity"
                  android:label="OpenGL Java">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".NativeGLActivity"
                  android:label="OpenGL Native">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-sdk android:minSdkVersion="3" />
</manifest>

The following lines tell Android to create two application launchers in the device launchpad, one for each of the activities OpenGL Java and OpenGL Native:

<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

Let's start with the Java-only implementation. Figure 5-3 defines the basic workflow of the OpenGL application. The figure shows the main activity (JavaGLActivity), which creates the rendering surface (GLSurfaceView). The surface creates a thread (GLThread) and renderer (CubeRenderer). GLThread contains the loop that invokes the renderer draw() method that draws the tumbling cubes seen on the device display.

Workflow of the Java-only cubes sample

Figure 5-3. Workflow of the Java-only cubes sample

Java Main Activity

When the user starts the application, the JavaGLActivity.onCreate() method will be called (see Listing 5-2). Here is where the surface view (mGLSurfaceView) is initialized and set as the application content:

mGLSurfaceView = new GLSurfaceView(this);
mGLSurfaceView.setRenderer(new CubeRenderer(true));
setContentView(mGLSurfaceView);

Note that the GL surface view must use a renderer (CubeRenderer in this case), which implements the Renderer interface and takes a Boolean argument indicating if a translucent background should be used.

Example 5-2. Main Activity for the Java-Only Version of the GL Cubes Sample

package opengl.test;

import opengl.scenes.GLSurfaceView;
import opengl.scenes.cubes.CubeRenderer;
import android.app.Activity;
import android.os.Bundle;

public class JavaGLActivity extends Activity
{
private GLSurfaceView mGLSurfaceView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGLSurfaceView = new GLSurfaceView(this);

        try {
            mGLSurfaceView.setRenderer(new CubeRenderer(true));
            setContentView(mGLSurfaceView);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onPause() {
        // Ideally a game should implement onResume() and onPause()
        // to take appropriate action when the activity loses focus
        super.onPause();
        mGLSurfaceView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume();
    }
}

When the application loses focus or resumes, the onPause() or onResume() method will be called, respectively. These methods delegate to the surface view (GLSurfaceView) to take the appropriate action, such as saving application state or suspending/resuming the rendering process.

Surface View

The class GLSurfaceView (see Listing 5-3) defines the surface where the tumbling cubes animation will take place. The class constructor starts by initializing a callback to receive notifications when the surface is changed, created, or destroyed:

mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU);

By implementing SurfaceHolder.Callback and calling SurfaceHolder.addCallback(), the class will receive the events:

  • surfaceCreated(SurfaceHolder holder): This is called immediately after the surface is first created. In this case, the surface delegates to the inner thread GLThread.surfaceCreated().

  • surfaceDestroyed(SurfaceHolder holder): This method is called immediately before a surface is being destroyed. After returning from this call, the surface should not be accessed. In this case, the method delegates to the rendering thread GLThread.surfaceDestroyed().

  • surfaceChanged(SurfaceHolder holder, int format, int w, int h): This method is called immediately after any structural changes (format or size) have been made to the surface. Here is where you tell the inner thread that the size has changed. This method is always called at least once, after surfaceCreated(). The second argument of this method (format) is the pixel format of the graphics defined in the PixelFormat class.

Example 5-3. Surface View for the GL Cubes Sample

package opengl.scenes;

import opengl.jni.Natives;
import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * An implementation of SurfaceView that uses the dedicated surface for
 * displaying an OpenGL animation. This allows the animation to run in a
 * separate thread, without requiring that it be driven by the update
 * mechanism of the view hierarchy.
 *
 * The application-specific rendering code is delegated to a GLView.Renderer
 * instance.
 */
public class GLSurfaceView extends SurfaceView
  implements  SurfaceHolder.Callback
{
    public GLSurfaceView(Context context) {
        super(context);
        init();
    }

    public GLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed
mHolder = getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
    }

    public SurfaceHolder getSurfaceHolder() {
        return mHolder;
    }

    public void setRenderer(Renderer renderer) {
        mGLThread = new GLThread(renderer, mHolder);
        mGLThread.start();
    }

    public void surfaceCreated(SurfaceHolder holder) {
        mGLThread.surfaceCreated();
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface will be destroyed when we return
        mGLThread.surfaceDestroyed();
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int w,
            int h) {
        // Surface size or format has changed. This should not happen in
        // this example.
        mGLThread.onWindowResize(w, h);
    }

    /**
     * Inform the view that the activity is paused.
     */
    public void onPause() {
        mGLThread.onPause();
    }

    /**
     * Inform the view that the activity is resumed.
     */
    public void onResume() {
        mGLThread.onResume();
    }

    /**
     * Inform the view that the window focus has changed.
     */
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        mGLThread.onWindowFocusChanged(hasFocus);
    }
/**
     * Queue an "event" to be run on the GL rendering thread.
     *
     * @param r
     *            the runnable to be run on the GL rendering thread.
     */
    public void queueEvent(Runnable r) {
        mGLThread.queueEvent(r);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mGLThread.requestExitAndWait();
    }

    private SurfaceHolder mHolder;
    private GLThread mGLThread;

}

Other important methods in the surface view include the following:

  • setRenderer(): This method creates the inner thread that does all the work and starts it. The thread keeps a reference to the surface holder available by calling getHolder().

    public void setRenderer(Renderer renderer) {
        mGLThread = new GLThread(renderer, mHolder);
        mGLThread.start();
    }
  • queueEvent(Runnable r): This method sends an event to be run by the inner thread.

  • onDetachedFromWindow(): This method is called when the view is detached from a window. At this point, it no longer has a surface for drawing.

GL Thread

The main loop of the animation is performed by GLThread. When started, this thread performs the following steps:

  1. It creates a semaphore:

    sEglSemaphore.acquire();
    guardedRun();   // Only 1 thread can access this code
    sEglSemaphore.release();
  2. It runs the critical animation loop. Within the loop, the actual drawing is delegated to the CubeRenderer.

  3. When asked to quit, the loops terminates, and the OpenGL resources are released.

Note

A semaphore is an object often used to restrict the number of threads than can access the OpenGL context. When the Android framework launches a second instance of an activity, the new instance's onCreate() method may be called before the first instance returns from onDestroy(). A semaphore ensures that only one instance at a time accesses the GL API. We must do this because OpenGL is a single-threaded API (which means that only one thread can access the GLContext at a time).

Listing 5-4 shows a fragment of the GLThread class taken from the GL cubes sample. When the thread starts, the run() method will be invoked, and a semaphore used to ensure that guardedRun() can be accessed by one thread only. guardedRun() performs other important steps, such as the following:

  • Initialize the Embedded OpenGL (EGL) for a given configuration specification. The configuration specification defines information, such as pixel format and image depth.

  • Create the OpenGL surface and tell the renderer about it.

  • Check if the size of the surface has changed and tell the renderer about it.

  • Queue and get events to be run on the GL rendering thread.

Example 5-4. Rendering Thread for the GL Cubes Sample

package opengl.scenes;

// ...

/**
 * A generic GL Thread. Takes care of initializing EGL and GL.
 * Delegates to a Renderer instance to do the actual drawing.
*/
public class GLThread extends Thread
{
    public GLThread(Renderer renderer, SurfaceHolder holder) {
        super();
        mDone = false;
        mWidth = 0;
        mHeight = 0;
        mRenderer = renderer;
        mHolder = holder;
        setName("GLThread");
    }
@Override
    public void run() {
        try {
            try {
                sEglSemaphore.acquire();
            } catch (InterruptedException e) {
                return;
            }
            guardedRun();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            sEglSemaphore.release();
        }
    }

    private void guardedRun() throws InterruptedException {
        mEglHelper = new EglHelper();

         // Specify a configuration for our OpenGL session
        int[] configSpec = mRenderer.getConfigSpec();
        mEglHelper.start(configSpec);

        GL10 gl = null;
        boolean tellRendererSurfaceCreated = true;
        boolean tellRendererSurfaceChanged = true;

         // This is our main activity thread's loop,
        while (!mDone) {

             // Update the asynchronous state (window size)
            int w, h;
            boolean changed;
            boolean needStart = false;
            synchronized (this) {
                Runnable r;
                while ((r = getEvent()) != null) {
                    r.run();
                }
                if (mPaused) {
                    mEglHelper.finish();
                    needStart = true;
                }
                if (needToWait()) {
                    while (needToWait()) {
                        wait();
                    }
                }
                if (mDone) {
                    break;
                }
                changed = mSizeChanged;
w = mWidth;
                h = mHeight;
                mSizeChanged = false;
            }
            if (needStart) {
                mEglHelper.start(configSpec);
                tellRendererSurfaceCreated = true;
                changed = true;
            }
            if (changed) {
                // Create the surface
                gl = (GL10) mEglHelper.createSurface(mHolder);
                tellRendererSurfaceChanged = true;
            }
            if (tellRendererSurfaceCreated) {
                mRenderer.surfaceCreated(gl);
                tellRendererSurfaceCreated = false;
            }
            if (tellRendererSurfaceChanged) {
                mRenderer.sizeChanged(gl, w, h);
                tellRendererSurfaceChanged = false;
            }
            if ((w > 0) && (h > 0)) {
                /* draw a frame here */
                mRenderer.drawFrame(gl);

                 // Call swapBuffers() to instruct the system to display
                mEglHelper.swap();
            }
        }

         // Clean up...
        mEglHelper.finish();
    }

    // ...
    private static final Semaphore sEglSemaphore = new Semaphore(1);
    private EglHelper mEglHelper;
}

Cube Renderer

CubeRenderer is the class that renders the pair of tumbling cubes (see Listing 5-5). It implements the Renderer interface and does some very interesting things.

The void drawFrame(GL10 gl) method does the actual drawing and gets called many times per second. The method starts by setting the matrix mode to GL_MODELVIEW. This essentially says to render things in a 3D perspective (model view). Next, it clears all screen buffers by calling glLoadIdentity().

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();

Next, the perspective is translated in the z axis by three units toward the eye viewpoint (also known as the camera):

gl.glTranslatef(0, 0, −3.0f);

The next two instructions tell the pipeline to rotate the perspective in the y and x axes by an angle given in radians (0-6.28, 0 meaning zero degrees, and 6.28, meaning 360 degrees).

gl.glRotatef(mAngle, 0, 1, 0);
gl.glRotatef(mAngle * 0.25f, 1, 0, 0);

Next, it requests that vertices and colors be rendered. These are defined within the Cube class:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

Then the cube is drawn:

mCube.draw(gl);

The perspective is rotated again in the y and z axes, and translated half a unit away from the eye:

gl.glRotatef(mAngle * 2.0f, 0, 1, 1);
gl.glTranslatef(0.5f, 0.5f, 0.5f);

The second cube is drawn, and the angle of rotation is increased for the next iteration.

mCube.draw(gl);
mAngle += 1.2f;

The int[] getConfigSpec() method initializes the pixel format and the depth of the display. The pixel format describes the size of the ARGB values used to describe a pixel. The depth indicates the maximum number of colors used. For example, the following integer array requests 32 bits per pixel (ARGB 32bpp) with a depth of 16 (2^16 colors).

int[] configSpec = {
EGL10.EGL_RED_SIZE,      8,
EGL10.EGL_GREEN_SIZE,    8,
EGL10.EGL_BLUE_SIZE,     8,
EGL10.EGL_ALPHA_SIZE,    8,
EGL10.EGL_DEPTH_SIZE,   16,
EGL10.EGL_NONE
};

The following are two other interesting methods in the cube renderer:

  • void sizeChanged(GL10 gl, int width, int height): This method fires when the size of the viewport changes. It scales the cubes by setting the ratio of the projection matrix and resizing the viewport.

  • void surfaceCreated(GL10 gl): This method fires when the surface is created. Here, some initialization is performed, such as setting a translucent background (if requested) and miscellaneous OpenGL renderer tweaking.

When the code in drawFrame() is executed many times per second, the result is two tumbling cubes (see Figure 5-4).

Example 5-5. Cube Renderer for the Pair of Tumbling Cubes

package opengl.scenes.cubes;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.opengles.GL10;

import opengl.jni.Natives;
import opengl.scenes.Renderer;

/**
 * Render a pair of tumbling cubes.
 */
public class CubeRenderer implements Renderer {

    public CubeRenderer(boolean useTranslucentBackground) {
        mTranslucentBackground = useTranslucentBackground;
        mNativeDraw = nativeDraw;
        mCube = new Cube();
    }

    public void drawFrame(GL10 gl) {
        /*
         * Usually, the first thing one might want to do is to clear
         * the screen. The most efficient way of doing this is
         * to use glClear().
         */
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

        /*
         * Now we're ready to draw some 3D objects
         */
        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();
        gl.glTranslatef(0, 0, −3.0f);
        gl.glRotatef(mAngle, 0, 1, 0);
        gl.glRotatef(mAngle * 0.25f, 1, 0, 0);

        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        mCube.draw(gl);

        gl.glRotatef(mAngle * 2.0f, 0, 1, 1);
gl.glTranslatef(0.5f, 0.5f, 0.5f);

        mCube.draw(gl);

        mAngle += 1.2f;
    }

    public int[] getConfigSpec() {
        if (mTranslucentBackground) {
            // We want a depth buffer and an alpha buffer
            int[] configSpec = { EGL10.EGL_RED_SIZE, 8,
                    EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8,
                    EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_DEPTH_SIZE, 16,
                    EGL10.EGL_NONE };
            return configSpec;
        } else {
            // We want a depth buffer, don't care about the
            // details of the color buffer.
            int[] configSpec = { EGL10.EGL_DEPTH_SIZE, 16,
                    EGL10.EGL_NONE };
            return configSpec;
        }
    }

    public void sizeChanged(GL10 gl, int width, int height) {
        gl.glViewport(0, 0, width, height);

        /*
         * Set our projection matrix. This doesn't have to be done each time we
         * draw, but usually a new projection needs to be set when the viewport
         * is resized.
         */
        float ratio = (float) width / height;
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glFrustumf(-ratio, ratio, −1, 1, 1, 10);
    }

    public void surfaceCreated(GL10 gl) {
        /*
         * By default, OpenGL enables features that improve quality but reduce
         * performance. One might want to tweak that especially on software
         * renderer.
         */
        gl.glDisable(GL10.GL_DITHER);

        /*
         * Some one-time OpenGL initialization can be made here probably based
         * on features of this particular context
         */
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT
           , GL10.GL_FASTEST);
if (mTranslucentBackground) {
            gl.glClearColor(0, 0, 0, 0.5f);
        } else {
            gl.glClearColor(1, 1, 1, 0.5f);
        }

        gl.glEnable(GL10.GL_CULL_FACE);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glEnable(GL10.GL_DEPTH_TEST);
    }

    private boolean mTranslucentBackground;
    private Cube mCube;
    private float mAngle;
}

Cube Class

CubeRenderer delegates drawing to the Cube class (see Listing 5-6). This class defines a 12-sided cube with 8 vertices (8 * x,y,z coordinates), 32 colors (8 vertices * 4 ARGB values), and 36 indices for the x,y,z coordinates of each side. The class consists of two methods:

  • Cube(): This is the class constructor. It initializes arrays for the vertices, colors, and indices required to draw. It then uses direct Java buffers to place the data on the native heap, where the garbage collector cannot move them. This is required by the gl*Pointer() API functions that do the actual drawing.

  • draw(): To draw the cube, we simply set the vertices and colors, and issue a call to glDrawElements using triangles (GL_TRIANGLES). Note that a cube has 6 faces, 8, vertices, and 12 sides:

    gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
    gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
    gl.glDrawElements(GL10.GL_TRIANGLES, 36
         , GL10.GL_UNSIGNED_BYTE,  mIndexBuffer);

Example 5-6. Cube Class for the GL Cubes Sample

package opengl.scenes.cubes;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import javax.microedition.khronos.opengles.GL10;

/**
 * A vertex shaded cube.
 */
public class Cube {
    public Cube() {
        int one = 0x10000;
// 8 vertices each with 3 xyz coordinates
        int vertices[] = { -one, -one, -one
                , one, -one, -one
                , one, one,  -one
                , -one, one, -one
                , -one, -one, one
                , one, -one, one
                , one, one, one
                , -one, one, one };

        // 8 colors each with  4 RGBA values
        int colors[] = { 0, 0, 0, one
                , one, 0, 0, one
                , one, one, 0, one
                , 0, one, 0, one
                , 0, 0, one, one
                , one, 0, one, one
                , one, one, one, one
                , 0, one, one, one};
        // 12 indices each with 3 xyz coordinates
        byte indices[] = { 0, 4, 5, 0, 5, 1, 1, 5, 6, 1, 6, 2, 2, 6, 7,
                2, 7, 3, 3, 7, 4, 3, 4, 0, 4, 7, 6, 4, 6, 5, 3, 0, 1,
                3, 1, 2 };


        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
        vbb.order(ByteOrder.nativeOrder());
        mVertexBuffer = vbb.asIntBuffer();
        mVertexBuffer.put(vertices);
        mVertexBuffer.position(0);

        ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
        cbb.order(ByteOrder.nativeOrder());
        mColorBuffer = cbb.asIntBuffer();
        mColorBuffer.put(colors);
        mColorBuffer.position(0);

        mIndexBuffer = ByteBuffer.allocateDirect(indices.length);
        mIndexBuffer.put(indices);
        mIndexBuffer.position(0);
    }

    public void draw(GL10 gl) {
        gl.glFrontFace(GL10.GL_CW);
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);
        gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);
        gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE,
                mIndexBuffer);
    }

    private IntBuffer mVertexBuffer;
    private IntBuffer mColorBuffer;
private ByteBuffer mIndexBuffer;
}

Figure 5-4 shows the sample in action. In the next section, you'll see how portions of this code can be implemented natively.

Tumbling cubes from the Java sample

Figure 5-4. Tumbling cubes from the Java sample

OpenGL the Native Way

In the previous section, you saw how a pure Java OpenGL application works from the ground up. This applies if you write an application from scratch in Java. However, if you already have a C OpenGL renderer and wish to interface with Android, you probably don't want to rewrite your application (especially if it has thousands of lines of code). This would consume significant time and resources, and more than likely, give you terrible headache. To understand how you can maximize the return on your investment, let's look at the general steps used to create an OpenGL application.

Any OpenGL application can be divided into the following major steps:

  1. Initialization: OpenGL is a single-threaded system that requires a GLContext to be initialized. Only one thread can access this context at a time. In EGL, this step is subdivided as follows:

    1. Get an EGL instance. In Android, this can be done using the EGLContext class:

      mEgl = EGLContext.getEGL();
    2. Get a default display. The display is required for the rendering process. In Android, use this call:

      mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    3. Initialize the display, as follows:

      int[] version = new int[2];
      mEgl.eglInitialize(mEglDisplay, version);
    4. You must also specify the pixel format and image depth you wish to use. The following requests a 32bpp pixel format with an image depth of 16:

      EGLConfig[] configs = new EGLConfig[1];
      int[] num_config = new int[1];
      
      int[] configSpec = {
        EGL10.EGL_RED_SIZE,      8,
        EGL10.EGL_GREEN_SIZE,    8,
        EGL10.EGL_BLUE_SIZE,     8,
        EGL10.EGL_ALPHA_SIZE,    8,
        EGL10.EGL_DEPTH_SIZE,   16,
        EGL10.EGL_NONE
      };
      mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);
  2. Main loop: This is usually a user-defined thread that performs or delegates drawing operations.

  3. Drawing: In the drawing process, a set of GL operations is performed for each iteration of the loop. At the end of each iteration, buffers must be swapped to display the rendered surface on the screen.

  4. Cleanup: In this step, the GLContext is destroyed and resources released back to the system.

All these steps can be performed in Java. So it happened that one day I wanted to port an OpenGL-based game to Android written in C, and wondered if some steps could be done in Java and some in C. I was very happy to discover that this is indeed possible. For example, the following steps can be performed in Java within an Android activity:

  • Initialization: Get the EGL instance, initialize the default display, and set the pixel format and image depth.

  • Main loop: The main loop can be a combination of a Java thread that calls a native game loop. Here is where things get interesting.

    Note

    OpenGL operations can be performed natively after the GLContext is initialized by an Android activity if, and only if, the native code is loaded by the activity as a shared library through JNI.

  • Swap buffers: This step can be performed in Java, provided that the native library issues a callback after all GL operations have been completed. This is simply using JNI callbacks and will result in a rendered surface on the screen.

This is great news. You don't need to rewrite large portions of an OpenGL game. You simply need to initialize the GLContext within your Java activity, load the shared library, do all the rendering operations natively, and issue a swap buffers callback to Java on each iteration of the game loop.

Let's apply this concept by rewriting portions of the GL cubes Java sample in C. The portion that will be rewritten is the rendering of the cubes. The rest—initialization, main loop, and swap buffers—will remain in Java. To accomplish this, you must make some simple changes to the sample classes and add a new native activity.

Main Activity

You must create a new activity (with its own launcher) to load the native code (see Listing 5-7). This activity is almost identical to its Java counterpart, except for the following:

  • A native library is loaded using System.load("/data/libgltest_jni.so").

  • The Renderer constructor has been modified to accept a second Boolean argument (use native rendering): mGLSurfaceView.setRenderer(new CubeRenderer(true, true)). This tells the cube renderer to use a translucent background and native rendering.

Example 5-7. Native Cubes Activity

package opengl.test;

import opengl.scenes.GLSurfaceView;
import opengl.scenes.cubes.CubeRenderer;
import android.app.Activity;
import android.os.Bundle;

public class NativeGLActivity extends Activity {
    private GLSurfaceView mGLSurfaceView;

    {
        final String LIB_PATH = "/data/libgltest_jni.so";

        System.out
.println("Loading JNI lib using abs path:" + LIB_PATH);
        System.load(LIB_PATH);
    }

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGLSurfaceView = new GLSurfaceView(this);

        try {
            mGLSurfaceView.setRenderer(new CubeRenderer(true, true));
            setContentView(mGLSurfaceView);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        // Ideally a game should implement onResume() and onPause()
        // to take appropriate action when the activity loses focus
        super.onResume();
        mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause() {
        // Ideally a game should implement onResume() and onPause()
        // to take appropriate action when the activity loses focus
        super.onPause();
        mGLSurfaceView.onPause();
    }
}

The following new files will be added to the project (see Figure 5-5):

  • Native activity: This is the main entry point to the application. It can be run from its own launcher on the device.

  • Native interface class: This is a new Java class that contains the native methods to be invoked within the renderer thread.

  • Native cube renderer (cuberenderer.c): This is the C equivalent of CubeRenderer.java. It initializes the scene and draws a frame. It also contains all the JNI callbacks.

  • Native cube (cube.c): This file is equivalent to Cube.java; it draws the cube.

Three files will be updated to accommodate the native calls: CubeRenderer, GLSurfaceView, and GLThread.

GL native cubes sample file layout

Figure 5-5. GL native cubes sample file layout

Native Interface Class

The native interface class defines native methods to be invoked within the application thread (see Listing 5-8). It includes one native method and two callbacks:

  • static native int NativeRender(): This is the actual native method that will render the cube. It is implemented natively in C and executed through JNI.

  • static void OnMessage(String text): This is a callback invoked within the native layer to display a message back to the application.

  • static void GLSwapBuffers(): This is a callback invoked within the native layer to request a buffer swap (render it). For this sample, this method will not be actually invoked (as the loop is defined in Java), but it could be useful in other situations (when the main loop is implemented natively).

Tip

As you may know, using JNI, you can invoke C functions from Java. You may not know that you can also load classes and invoke Java methods within C.

Example 5-8. Native Interface for the GL Cubes Sample

package opengl.jni;

public class Natives {

    private static EventListener listener;

    public static interface EventListener {
        void OnMessage(String text);
        void GLSwapBuffers();
    }

    public static void setListener(EventListener l) {
        listener = l;
    }

    /**
     * Native Render test
     *
     * @return
     */
    public static native int NativeRender();

    @SuppressWarnings("unused")
    private static void OnMessage(String text) {
        if (listener != null)
            listener.OnMessage(text);
    }

    @SuppressWarnings("unused")
    private static void GLSwapBuffers() {
        if (listener != null)
            listener.GLSwapBuffers();
    }
}

This class needs a way to notify components (the activity, for example) that some message has been received from the native layer. You do this by creating the interface EventListener. In this way, a class that wants to receive messages must implement EventListener and issue a call to Natives.setListener(this).

Before we jump to the C code, let's take a look at the Java changes required to the classes CubeRenderer, GLSurfaceView, and GLThread for the sample.

Changes to the Original Sample

The class CubeRenderer has been modified to accept a Boolean argument in its constructor to request a native draw (see Listing 5-9).

Example 5-9. Changes for CubeRenderer Class

public class CubeRenderer implements Renderer
{
    private boolean mNativeDraw = false;

    public CubeRenderer(boolean useTranslucentBackground,
            boolean nativeDraw)
    {
        mTranslucentBackground = useTranslucentBackground;
        mNativeDraw = nativeDraw;
        mCube = new Cube();
    }

    public void drawFrame(GL10 gl) {
        if (mNativeDraw)
            doNativeDraw();
        else
            doJavaDraw(gl);
    }

    private void doJavaDraw(GL10 gl) {
        // Same as before
        // ...
    }

    public void doNativeDraw() {
        Natives.NativeRender();
    }

    // ...
}

When drawFrame() is invoked and mNativeDraw is true, the cube will be rendered from C (by calling Natives.NativeRender()). Otherwise, the Java implementation will be used.

When the surface is created, and a renderer is set for that surface using GLSurfaceView.setRenderer(Renderer renderer), you must tell the native interface class (Natives.java) that you wish to listen for messages by sending a reference to the loop thread:

public void setRenderer(Renderer renderer) {
    mGLThread = new GLThread(renderer, mHolder);
    mGLThread.start();
    Natives.setListener(mGLThread);
}

Note that GLThread must implement Natives.EventListener for this to work.

Finally, the last class to be updated is GLThread (see Listing 5-10), which contains the main loop.

Example 5-10. Changes for GLThread.java

public class GLThread extends Thread implements EventListener
{
    // ...
    @Override
    public void GLSwapBuffers() {
        if ( mEglHelper != null ) {
            mEglHelper.swap();
        }
    }

    @Override
    public void OnMessage(String text) {
        System.out.println("GLThread::OnMessage " + text);
    }
}

GLThread implements EventListener. This allows the C code to send text messages if something is wrong. The method GLSwapBuffers() will be invoked when the C code requests a buffer swap.

This takes care of the Java portion of the sample. Now let's look at the C files: cuberenderer.c and cube.c.

Native Cube Renderer

The native cube renderer (cuberenderer.c) is similar to the Java class CubeRenderer. This file performs the following tasks (see Listings 5-11 through 5-14):

  • It initializes the scene. This function is almost identical to CubeRenderer.surfaceCreated().

  • It draws a frame using the drawFrame() function. This function is similar in nature to CubeRenderer.drawFrame().

  • It contains the native implementation of the native interface class opengl.jni.Natives.NativeRender (mapped in C as Java_opengl_jni_Natives_NativeRender). This function will be invoked every time a frame is rendered from the GLThread Java class.

  • It contains the Java callbacks (functions that will invoke Java methods):

    • jni_printf(char *format, ...) sends a text message back.

    • jni_gl_swap_buffers () requests a buffer swap within Java.

Warning

Before the cuberenderer.c file can be compiled, the header file opengl_jni_Natives.h must be created. This header contains the prototypes for the native methods in opengl.jni.Natives.java. To do this, use the javah command: javah -cp [CLASS_PATH] -d include opengl.jni.Natives, where CLASS_PATH points to the project bin folder.

Scene Initialization

Scene initialization is performed by the init_scene() function (see Listing 5-11). Its job is to perform trivial GL initialization calls, such as setting a perspective correction hint, background color, and shade model, and in this case, enabling face culling and depth tests.

init_scene() is meant to mirror the Java method CubeRenderer.surfaceCreated, which initializes the scene after the surface is created. Note that Java lines such as gl.glDisable(GL10.GL_DITHER) become glDisable(GL_DITHER). Because the context is already initialized in Java, you can simply make the GL commands you need in the equivalent C function.

Example 5-11. Scene Initialization from cuberenderer.c

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <math.h>

#include <EGL/egl.h>
#include <GLES/gl.h>
#include <GLES/glext.h>

#include "include/opengl_jni_Natives.h"

#define ONE  1.0f
#define FIXED_ONE 0x10000

// Prototypes
void jni_printf(char *format, ...);
void jni_gl_swap_buffers ();

// Rotation Angle
static float mAngle = 0.0;

extern void Cube_draw();

static void init_scene(void)
{
        glDisable(GL_DITHER);

        /*
         * Some one-time OpenGL initialization can be made here
* probably based on features of this particular context
         */
        glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_FASTEST);

        glClearColor(.5f, .5f, .5f, 1);

        glEnable(GL_CULL_FACE);
        glShadeModel(GL_SMOOTH);
        glEnable(GL_DEPTH_TEST);
}

Drawing Frames

Drawing the actual frames is performed by the drawFrame() function. This function performs the following steps:

  • It clears the screen via glClear().

  • It sets the framework to draw 3D objects via the glMatrixMode(GL_MODELVIEW) system call.

  • It performs an initial translation—a rotation to be applied to the first cube.

  • It draws the first cube by calling Cube_draw(). Note that vertices and colors must be enabled via glEnableClientState().

  • It performs a second rotation/translation and draws a second cube by calling Cube_draw() again.

  • It increases the angle for the next interaction.

drawFrame() is meant to mirror the Java method CubeRenderer.drawFrame(), which includes the code in the next fragment:

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslatef(0, 0, −3.0f);
gl.glRotatef(mAngle,        0, 1, 0);
gl.glRotatef(mAngle*0.25f,  1, 0, 0);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

mCube.draw(gl);

gl.glRotatef(mAngle*2.0f, 0, 1, 1);
gl.glTranslatef(0.5f, 0.5f, 0.5f);

mCube.draw(gl);

mAngle += 1.2f;

In C, the preceding code simply becomes the following:

glDisable(GL_DITHER);

glTexEnvx(GL_TEXTURE_ENV,
      GL_TEXTURE_ENV_MODE,GL_MODULATE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glTranslatef(0, 0, −3.0f);
glRotatef(mAngle,        0, 0, 1.0f);
glRotatef(mAngle*0.25f,  1, 0, 0);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

Cube_draw();

glRotatef(mAngle*2.0f, 0, 1, 1);
glTranslatef(0.5f, 0.5f, 0.5f);

Cube_draw();

mAngle += 1.2f;

Note that drawFrame() is defined as static, which tells the compiler that this function will be visible only by functions within cuberenderer.c (a bit similar to the private keyword in Java). Furthermore, the function Cube_draw() is implemented in cube.c.

Example 5-12. Drawing Frames from cuberenderer.c

static void drawFrame()
{
        /*
         * By default, OpenGL enables features that improve quality
         * but reduce performance. One might want to tweak that
         * especially on software renderer.
         */
        glDisable(GL_DITHER);
        glTexEnvx(GL_TEXTURE_ENV,
              GL_TEXTURE_ENV_MODE,GL_MODULATE);

        /*
         * Usually, the first thing one might want to do is to clear
         * the screen. The most efficient way of doing this is to use
         * glClear().
         */
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/*
         * Now we're ready to draw some 3D objects
         */
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        glTranslatef(0, 0, −3.0f);
        glRotatef(mAngle,        0, 0, 1.0f);
        glRotatef(mAngle*0.25f,  1, 0, 0);

        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_COLOR_ARRAY);

        Cube_draw();

        glRotatef(mAngle*2.0f, 0, 1, 1);
        glTranslatef(0.5f, 0.5f, 0.5f);

        Cube_draw();

        mAngle += 1.2f;
}

Java Callbacks

The Java callbacks are used to send messages from the native layer to the Java layer (see Listing 5-13). The cube renderer implements two callbacks:

  • jni_send_str(const char * text): This callback sends a string message to Java (mostly for debugging purposes). It does so by attaching to the current thread. This step must be done if you call JNI in a C function outside a JNI native implementation. The callback then loads the Java class opengl.jni.Natives.java. Finally, it calls the Java method opengl.jni.Natives.OnMessage(String).

  • jni_gl_swap_buffers (): This is the most important callback. It tells Java that it is time to swap the OpenGL buffers. In OpenGL lingo, that means render the graphics. This step must be performed at the end of each frame of the rendering loop. The callback implementation is similar to the previous one. The main difference is that it invokes the Java method opengl.jni.Natives.GLSwapBuffers ().

Example 5-13. Java Callbacks from cuberenderer.c

/**
 * Send a string back to Java
 */
static jmethodID mSendStr;
static jclass jNativesCls;
static JavaVM *g_VM;
static void jni_send_str( const char * text)
{
    JNIEnv *env;

    if ( !g_VM) {
        return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);


    if ( !jNativesCls ) {
        jNativesCls = (*env)->FindClass(env, "opengl/jni/Natives");

    }
    if ( jNativesCls == 0 ) {
            return;
    }

    // Call opengl.jni.Natives.OnMessage(String)
    if (! mSendStr ) {
        mSendStr = (*env)->GetStaticMethodID(env, jNativesCls
            , "OnMessage"
            , "(Ljava/lang/String;)V");
    }
    if (mSendStr) {
        (*env)->CallStaticVoidMethod(env, jNativesCls
                , mSendStr
                , (*env)->NewStringUTF(env, text) );
    }
}

void jni_gl_swap_buffers () {
    JNIEnv *env;

    if ( !g_VM) {
        return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);


    if ( !jNativesCls ) {
        jNativesCls = (*env)->FindClass(env, "opengl/jni/Natives");

    }
    if ( jNativesCls == 0 ) {
            return;
    }

    // Call opengl.jni.Natives.GLSwapBuffers ()
    jmethodID mid = (*env)->GetStaticMethodID(env, jNativesCls
, "GLSwapBuffers"
            , "()V");

    if (mid) {
        (*env)->CallStaticVoidMethod(env, jNativesCls
                , mid
                );
    }
}


/**
 * Printf into the java layer
 * does a varargs printf into a temp buffer
 * and calls jni_sebd_str
 */

void jni_printf(char *format, ...)
{
    va_list         argptr;
    static char             string[1024];

    va_start (argptr, format);
    vsprintf (string, format,argptr);
    va_end (argptr);

    jni_send_str (string);
}

Let's take a closer look at the anatomy of a JNI Java callback. To start using JNI, a C program must include the system header:

#include <jni.h>

Now, if your function is called from a different place than the one that started Java_opengl_jni_Natives_NativeRender, you must attach to the current thread with the following:

(*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);

This is required if, for example, your program implements its own game loop, and then sends messages back to Java through JNI. This isn't the case in our example, but I've included it so the function can be invoked either way. g_VM is a global reference to the JVM, which must be saved within the very first call to Java_opengl_jni_Natives_NativeRender. Next, to load a Java class opengl.jni.Natives within C, you use the following:

jclass jNativesCls = (*env)->FindClass(env, "opengl/jni/Natives");

Here, env is a reference to the JNI environment obtained from then previous call. Note that the class name must be separated using /, not ..

Now, with a reference to the natives class, you can call the static void method OnMessage:

jmethod mSendStr = (*env)->GetStaticMethodID(env, jNativesCls, "OnMessage"
, "(Ljava/lang/String;)V");
(*env)->CallStaticVoidMethod(env, jNativesCls, mSendStr
            , (*env)->NewStringUTF(env, text) );

Note that to call this method, you need to obtain its JNI method ID using its name (OnMessage) and its signature (Ljava/lang/String;)V. The signature describes the method's arguments (a string in this case) and the return type (void). With this information, you call the static void method sending the corresponding arguments.

Note

C strings must be converted into Java strings before invoking Java methods, using (*env)-> NewStringUTF(env, MyCString).

Native Interface Call

The native interface function (see Listing 5-14) is the C implementation of the Java native method opengl.jni.Natives.NativeRender(). This function performs the following tasks:

  • It saves a reference to the Java VM, required by the Java callbacks of the previous section.

  • It initializes the scene.

  • It renders one frame. This function is meant to be called multiple times within the rendering thread (implemented by GLThread.java).

Example 5-14. Native Interface Function from cuberenderer.c

/*
 * Class:     opengl_jni_Natives
 * Method:    RenderTest
 * Signature: ()V
 */
JNIEXPORT jint JNICALL Java_opengl_jni_Natives_NativeRender
  (JNIEnv * env, jclass cls)
{

    (*env)->GetJavaVM(env, &g_VM);
    static int initialized = 0;

    if ( ! initialized ) {
        jni_printf("Native:RenderTest initscene");
        init_scene();

        initialized = 1;

    }
drawFrame();
    return 1;
}

Native Cube

Native cube (cube.c) is the last file in the lot (see Listing 5-15). This file is a carbon copy of Cube.java. It defines the vertices, colors, and indices of the cube, and draws it in the same way as its Java counterpart.

Example 5-15. Native Implementation of Cube.java

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <GLES/gl.h>

#define FIXED_ONE 0x10000
#define one 1.0f

typedef unsigned char byte;

extern void jni_printf(char *format, ...);

// Cube vertices
static GLfloat vertices[24] = {
        -one, -one, -one,
        one, -one, -one,
        one,  one, -one,
        -one,  one, -one,
        -one, -one,  one,
        one, -one,  one,
        one,  one,  one,
        -one,  one,  one,
};

// Colors
static GLfloat colors[] = {
        0,    0,    0,  one,
        one,    0,    0,  one,
        one,  one,    0,  one,
        0,  one,    0,  one,
        0,    0,  one,  one,
        one,    0,  one,  one,
        one,  one,  one,  one,
        0,  one,  one,  one,
};

static byte indices[] = {
        0, 4, 5,    0, 5, 1,
        1, 5, 6,    1, 6, 2,
2, 6, 7,    2, 7, 3,
        3, 7, 4,    3, 4, 0,
        4, 7, 6,    4, 6, 5,
        3, 0, 1,    3, 1, 2
};


void Cube_draw()
{
    glFrontFace(GL_CW);

    glVertexPointer(3, GL_FLOAT, 0, vertices);
    glColorPointer(4, GL_FLOAT, 0 , colors);

    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, indices);
}

Compiling and Running the Sample

You have the code in place, and now you must compile it. To do so, use the helper scripts agcc and ald described in Chapter 1, and the Makefile shown in Listing 5-16.

Example 5-16. Makefile for This Chapter's Example

#############################################
# Android Makefile for CH05
#############################################

# Android source: Contains GL C headers
SYS_DEV            = /home/user/mydroid

# Compiler script
CC            = agcc

# Linker script
LINKER             = ald

# C files
MAIN_OBJS       = cuberenderer.o cube.o

# Library name, dynamic executable
LIB            = libgltest_jni.so
DYN            = gl-dyn

# Compilation flags & libs
INCLUDES       = -I$(SYS_DEV)/frameworks/base/opengl/include
CFLAGS      = -DNORMALUNIX -DLINUX -DANDROID
LIBS            = -lGLES_CM -lui

# Compile the main library (ibgltest_jni.so)
lib:  $(MAIN_OBJS) # jni
      @echo
      $(LINKER) -shared -o $(LIB) $(MAIN_OBJS) $(LIBS)
      @echo Done. Out file is $(LIB)

# Create JNI headers
jni:
      @echo "Creating JNI C headers..."
      javah -jni -classpath ../bin -d include opengl.jni.Natives


# Test dynamic exe
dyn:
      $(CC) -c test.c $(INCLUDES)
      $(LINKER) -o $(DYN) test.o  -lgltest_jni -L.
      @echo
      @echo Done. Out file is $(DYN)
      @echo

.c.o:
      @echo
      $(CC) -Wall -O2 -fpic -c $(CFLAGS) $(INCLUDES) $<


# Deploy lib to device /data folder
deploy-test:
      @echo "Deploying $(LIB) to /data"
      adb push $(LIB) /data

clean:
      rm -f *.o

First, generate the JNI headers for opengl.jni.Natives.java with the following:

$ make jni

Next, compile the library with the following:

user@ubuntu:~/workspace/ch05.OpenGL/native$ make
agcc -Wall -O2 -fpic -c -DNORMALUNIX -DLINUX -DANDROID -
I/home/user/mydroid/frameworks/base/opengl/include cuberenderer.c

agcc -Wall -O2 -fpic -c -DNORMALUNIX -DLINUX -DANDROID -
I/home/user/mydroid/frameworks/base/opengl/include cube.c

ald -shared -o libgltest_jni.so cuberenderer.o cube.o   -lGLES_CM -lui
arm-none-linux-gnueabi-ld: warning: library search path "/usr/lib/jvm/java-6-
sun/jre/lib/i386" is unsafe for cross-compilation
Done. Out file is libgltest_jni.so

Check for missing symbols with this code:

user@ubuntu:~/workspace/ch05.OpenGL/native$ make dyn
agcc -c test.c -I/home/user/mydroid/frameworks/base/opengl/include
ald -o gl-dyn test.o  -lgltest_jni -L.
arm-none-linux-gnueabi-ld: warning: library search path "/usr/lib/jvm/java-6-
sun/jre/lib/i386" is unsafe for cross-compilation
arm-none-linux-gnueabi-ld: warning: cannot find entry symbol _start; defaulting to 000082e8

Done. Out file is gl-dyn

Note

The compiler will complain about the local system JNI headers: "library search path "/usr/lib/jvm/java-6-sun/jre/lib/i386" is unsafe for cross-compilation." This message can be ignored.

Finally, the library must be deployed to the device /data folder so the activity can load it:

user@ubuntu:~ ch05.OpenGL/native$ make deploy-test
Deploying libgltest_jni.so to /data
adb push libgltest_jni.so /data
87 KB/s (7389 bytes in 0.082s)

As shown in Figure 5-6, when the project is started in the device, two launchers will be placed in the device desktop: OpenGL Java and OpenGL Native.

Device launchers for the GL cubes sample

Figure 5-6. Device launchers for the GL cubes sample

Run both launchers and look at the device log (see Listing 5-17). On the native side, you should see the following messages:

DEBUG/dalvikvm(512): Trying to load lib /data/libgltest_jni.so 0x433a7258
DEBUG/dalvikvm(512): Added shared lib /data/libgltest_jni.so

This indicates that the native library has been successfully loaded by JNI. Now stop the activity using the Devices view (see Figure 5-7), and then try the other activity.

Stopping an Android activity using the Devices view

Figure 5-7. Stopping an Android activity using the Devices view

Example 5-17. Device Logs for the Java and Native Implementations of GL Cubes

// Java Device Log
07-28 19:46:04.568: INFO/ActivityManager(52): Start proc opengl.test for activity
opengl.test/.JavaGLActivity: pid=505 uid=10021 gids={}
07-28 19:46:04.857: INFO/jdwp(505): received file descriptor 10 from ADB
07-28 19:46:05.677: INFO/System.out(505): GLSurfaceView::setRenderer setting
07-28 19:46:06.347: INFO/System.out(505): Vendor:Google Inc.
07-28 19:46:06.376: INFO/System.out(505): Renderer:Android PixelFlinger 1.0
07-28 19:46:06.376: INFO/System.out(505): Version:OpenGL ES-CM 1.0
07-28 19:46:06.416: INFO/System.out(505): Vendor:Google Inc.
07-28 19:46:06.436: INFO/System.out(505): Renderer:Android PixelFlinger 1.0
07-28 19:46:06.476: INFO/System.out(505): Version:OpenGL ES-CM 1.0
07-28 19:46:06.546: INFO/ARMAssembler(505): generated 07-28 19:46:06.638:
INFO/ActivityManager(52): Displayed activity opengl.test/.JavaGLActivity: 2202 ms

// Native Log
07-28 19:56:57.167: INFO/ActivityManager(52): Start proc opengl.test for activity
opengl.test/.NativeGLActivity: pid=512 uid=10021 gids={}
07-28 19:56:57.357: INFO/jdwp(512): received file descriptor 10 from ADB
07-28 19:56:58.247: INFO/System.out(512): Loading JNI lib using abs
path:/data/libgltest_jni.so
07-28 19:56:58.267: DEBUG/dalvikvm(512): Trying to load lib /data/libgltest_jni.so
0x433a7258
07-28 19:56:58.376: DEBUG/dalvikvm(512): Added shared lib /data/libgltest_jni.so 0x433a7258
07-28 19:56:58.387: DEBUG/dalvikvm(512): No JNI_OnLoad found in /data/libgltest_jni.so
0x433a7258
07-28 19:56:58.548: INFO/System.out(512): GLSurfaceView::setRenderer setting natives
listener
07-28 19:56:59.777: INFO/System.out(512): Vendor:Google Inc.
07-28 19:56:59.816: INFO/System.out(512): Renderer:Android PixelFlinger 1.0
07-28 19:56:59.916: INFO/System.out(512): Version:OpenGL ES-CM 1.0
07-28 19:57:00.056: INFO/System.out(512): Vendor:Google Inc.
07-28 19:57:00.158: INFO/System.out(512): Renderer:Android PixelFlinger 1.0
07-28 19:57:00.187: INFO/System.out(512): Version:OpenGL ES-CM 1.0
07-28 19:57:00.187: INFO/System.out(512): GLThread::OnMessage Native:RenderTest initscene
07-28 19:57:00.796: INFO/ActivityManager(52): Displayed activity
opengl.test/.NativeGLActivity: 3971 ms

Figure 5-7 shows the native renderer running in the emulator.

GL cubes native renderer

Figure 5-8. GL cubes native renderer

When it comes to advanced OpenGL games written for embedded devices, there are some caveats that you should be aware of before starting you porting work, which we'll look at in the next section.

Caveats of Porting OpenGL Games to Android

Today's smart phones have become pretty powerful. They feature a GPU capable of advanced graphics. Nevertheless, when it comes to writing advanced 3D games for embedded devices using OpenGL, several issues should be considered.

Consider a game like Quake, which has been ported to multiple smart phones. This game uses immediate mode drawing for specifying geometry. For example, consider the following snippet to render an arbitrary polygon and corresponding texture:

// Bind some texture
glBegin (GL_POLYGON);
glTexCoord2f (...);
glVertex3fv (...);
...
glEnd ();

This code is typical of a desktop application; however, it is not valid in Android (which implements OpenGL ES). This is because OpenGL ES does not support immediate mode (glBegin/glEnd) for simple geometry. Porting this code can consume significant resources (especially for a game like Quake, which has approximately 100,000 lines of source).

In OpenGL ES, geometry must be specified using vertex arrays, so the preceding code becomes something like this:

const GLbyte Vertices []= { ...};
const GLbyte TexCoords []= { ...};
...
glEnableClientState (GL_VERTEX_ARRAY);
glEnableClientState (GL_TEXTCOORD_ARRAY);

glVertexPointer (..., GL_BYTE , 0, Vertices);
glTexCoordPointer (..., GL_BYTE , 0, TexCoords);
glDrawArrays (GL_TRIANGLES, 0, ...);

You also must consider floating-point issues. OpenGL ES defines functions that use fixed-point values, as many devices do not have a floating-point unit (FPU). Fixed-point math is a technique to encode floating-point numbers using only integers. OpenGL ES uses 16 bits to represent the integer part, and another 16 bits to represent the fractional part. Here is an example of using a fixed-point translation function:

glTranslatex (10 << 16, 0, 0, 2 << 16); // glTranslatef (10.0f, 0.0f, 0.0f, 2.0f);

The following are other differences worth mentioning:

  • OpenGL ES does not render polygons as wireframe or points (only solid).

  • There is no GLU (OpenGL Utility Library). However, it is possible to find implementations of GLU functions on the Internet.

  • The GL_QUADS, GL_QUAD_STRIP, and GL_POLYGON primitives are not supported.

These are some of the things to watch for when you decide to port your OpenGL game to an embedded device.

The Veil Has Been Lifted

The veil has been lifted to reveal a new frontier of 3D development for Android. The techniques demonstrated in this chapter can help you to bring a large number of 3D PC games to the platform, at an enormous savings in development costs.

In this chapter, you have learned a trick to mix OpenGL code in Java and C to enable the reuse of large portions of C code along with Java code. We started by looking at the OpenGL tumbling cubes sample provided by Google, and how sections of the rendering process can be implemented in C. You saw that the sample's rendering process included EGL initialization, the main loop, drawing, buffer swap, and cleanup. Then you saw how to reimplement the cube rendering invoked within the main loop. You created the new components:

  • The native activity used to launch the application from the device

  • The native interface class used to define the native methods and C callbacks

  • The cube renderer and cube class used to render the cubes

Finally, we looked at the limitations of OpenGL ES when it comes to advanced 3D games.

I hope this chapter will help you get your own 3D games for Android with minimal effort and maximum code reuse. This is a prelude to the next chapters, where we will look at two of the greatest 3D shooters for the PC, Wolfenstein 3D and Doom, and the minimal changes required to get them running on your phone.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.156.22