Calling OpenGL from C on Android, Using the NDK

For this first post in the Developing a Simple Game of Air Hockey Using C++ and OpenGL ES 2 for Android, iOS, and the Web series, we’ll create a simple Android program that initializes OpenGL, then renders simple frames from native code.

Prerequisites

  • The Android SDK & NDK installed, along with a suitable IDE.
  • An emulator or a device supporting OpenGL ES 2.0.

We’ll be using Eclipse in this lesson.

To prepare and test the code for this article, I used revision 22.0.1 of the ADT plugin and SDK tools, and revision 17 of the platform and build tools, along with revision 8e of the NDK and Eclipse Juno Service Pack 2.

Getting started

The first thing to do is create a new Android project in Eclipse, with support for the NDK. You can follow along all of the code at the GitHub project.

Before creating the new project, create a new folder called airhockey, and then create a new Git repository in that folder. Git is a source version control system that will help you keep track of changes to the source and to roll back changes if anything goes wrong. To learn more about how to use Git, see the Git documentation.

To create a new project, select File->New->Android Application Project, and then create a new project called ‘AirHockey’, with the application name set to ‘Air Hockey’ and the package name set to ‘com.learnopengles.airhockey’. Leaving the rest as defaults or filling out as you prefer, save this new project in a new folder called android, inside of the airhockey folder that we created in the previous step.

Once the project has been created, right-click on the project in the Package Explorer, select Android Tools from the drop-down menu, then select Add Native Support…. When asked for the Library Name, enter ‘game’ and hit Finish, so that the library will be called libgame.so. This will create a new folder called jni in the project tree.

Initializing OpenGL

With our project created, we can now edit the default activity and configure it to initialize OpenGL. We’ll first add two member variables to the top of our activity class:

	private GLSurfaceView glSurfaceView;
	private boolean rendererSet;

Now we can set the body of onCreate() as follows:

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		ActivityManager activityManager
			= (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
		ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();

		final boolean supportsEs2 =
			configurationInfo.reqGlEsVersion >= 0x20000 || isProbablyEmulator();

		if (supportsEs2) {
			glSurfaceView = new GLSurfaceView(this);

			if (isProbablyEmulator()) {
				// Avoids crashes on startup with some emulator images.
				glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
			}

			glSurfaceView.setEGLContextClientVersion(2);
			glSurfaceView.setRenderer(new RendererWrapper());
			rendererSet = true;
			setContentView(glSurfaceView);
		} else {
			// Should never be seen in production, since the manifest filters
			// unsupported devices.
			Toast.makeText(this, "This device does not support OpenGL ES 2.0.",
					Toast.LENGTH_LONG).show();
			return;
		}
	}

First we check if the device supports OpenGL ES 2.0, and then if it does, we initialize a new GLSurfaceView and configure it to use OpenGL ES 2.0.

The check for configurationInfo.reqGlEsVersion >= 0x20000 doesn’t work on the emulator, so we also call isProbablyEmulator() to see if we’re running on an emulator. Let’s define that method as follows:

	private boolean isProbablyEmulator() {
		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
				&& (Build.FINGERPRINT.startsWith("generic")
						|| Build.FINGERPRINT.startsWith("unknown")
						|| Build.MODEL.contains("google_sdk")
						|| Build.MODEL.contains("Emulator")
						|| Build.MODEL.contains("Android SDK built for x86"));
	}

OpenGL ES 2.0 will only work in the emulator if it’s been configured to use the host GPU. For more info, read Android Emulator Now Supports Native OpenGL ES2.0!

Let’s complete the activity by adding the following methods:

	@Override
	protected void onPause() {
		super.onPause();

		if (rendererSet) {
			glSurfaceView.onPause();
		}
	}

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

		if (rendererSet) {
			glSurfaceView.onResume();
		}
	}

We need to handle the Android lifecycle, so we also pause & resume the GLSurfaceView as needed. We only do this if we’ve also called glSurfaceView.setRenderer(); otherwise, calling these methods will cause the application to crash.

For a more detailed introduction to OpenGL ES 2, see Android Lesson One: Getting Started or OpenGL ES 2 for Android: A Quick-Start Guide.

Adding a default renderer

Create a new class called RendererWrapper, and add the following code:

public class RendererWrapper implements Renderer {
	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		glClearColor(0.0f, 0.0f, 1.0f, 0.0f);
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		// No-op
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		glClear(GL_COLOR_BUFFER_BIT);
	}
}

This simple renderer will set the clear color to blue and clear the screen on every frame. Later on, we’ll change these methods to call into native code. To call methods like glClearColor() without prefixing them with GLES20, add import static android.opengl.GLES20.*; to the top of the class file, then select Source->Organize Imports.

If you have any issues in getting the code to compile, ensure that you’ve organized all imports, and that you’ve included the following imports in RendererWrapper:

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

import android.opengl.GLSurfaceView.Renderer;

Updating the manifest to exclude unsupported devices

We should also update the manifest to make sure that we exclude devices that don’t support OpenGL ES 2.0. Add the following somewhere inside AndroidManifest.xml:

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

Since OpenGL ES 2.0 is only fully supported from Android Gingerbread 2.3.3 (API 10), replace any existing <uses-sdk /> tag with the following:

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="17" />

If we run the app now, we should see a blue screen as follows:

First pass
First pass

Adding native code

We’ve verified that things work from Java, but what we really want to do is to be using OpenGL from native code! In the next few steps, we’ll move the OpenGL code to a set of C files and setup an NDK build for these files.

We’ll be sharing this native code with our future projects for iOS and the web, so let’s create a folder called common located one level above the Android project. What this means is that in your airhockey folder, you should have one folder called android, containing the Android project, and a second folder called common which will contain the common code.

Linking a relative folder that lies outside of the project’s base folder is unfortunately not the easiest thing to do in Eclipse. To accomplish this, we’ll have to follow these steps:

  1. Right-click the project and select Properties. In the window that appears, select Resource->Linked Resources and click New….
  2. Enter ‘COMMON_SRC_LOC’ as the name, and ‘${PROJECT_LOC}\..\common’ as the location. Once that’s done, click OK until the Properties window is closed.
  3. Right-click the project again and select Build Path->Link Source…, select Variables…, select COMMON_SRC_LOC, and select OK. Enter ‘common’ as the folder name and select Finish, then close the Properties window.

You should now see a new folder in your project called common, linked to the folder that we created.

Let’s create two new files in the common folder, game.c and game.h. You can create these files by right-clicking on the folder and selecting New->File. Add the following to game.h:

void on_surface_created();
void on_surface_changed();
void on_draw_frame();

In C, a .h file is known as a header file, and can be considered as an interface for a given .c source file. This header file defines three functions that we’ll be calling from Java.

Let’s add the following implementation to game.c:

#include "game.h"
#include "glwrapper.h"

void on_surface_created() {
	glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
}

void on_surface_changed() {
	// No-op
}

void on_draw_frame() {
	glClear(GL_COLOR_BUFFER_BIT);
}

This code will set the clear color to red, and will clear the screen every time on_draw_frame() is called. We’ll use a special header file called glwrapper.h to wrap the platform-specific OpenGL libraries, as they are often located at a different place for each platform.

Adding platform-specific code and JNI code

To use this code, we still need to add two things: a definition for glwrapper.h, and some JNI glue code so that we can call our C code from Java. JNI stands for Java Native Interface, and it’s how C and Java can talk to each other on Android.

Inside your project, create a new file called glwrapper.h in the jni folder, with the following contents:

#include <GLES2/gl2.h>

That wraps Android’s OpenGL headers. To create the JNI glue, we’ll first need to create a Java class that exposes the native interface that we want. To do this, let’s create a new class called GameLibJNIWrapper, with the following code:

public class GameLibJNIWrapper {
	static {
		System.loadLibrary("game");
	}

	public static native void on_surface_created();

	public static native void on_surface_changed(int width, int height);

	public static native void on_draw_frame();
}

This class will load the native library called libgame.so, which is what we’ll be calling our native library later on when we create the build scripts for it. To create the matching C file for this class, build the project, open up a command prompt, change to the bin/classes folder of your project, and run the following command:

javah -o ../../jni/jni.c com.learnopengles.airhockey.GameLibJNIWrapper

The javah command should be located in your JDKs bin directory. This command will create a jni.c file that will look very messy, with a bunch of stuff that we don’t need. Let’s simplify the file and replace it with the following contents:

#include "../../common/game.h"
#include <jni.h>

JNIEXPORT void JNICALL Java_com_learnopengles_airhockey_GameLibJNIWrapper_on_1surface_1created
	(JNIEnv * env, jclass cls) {
	on_surface_created();
}

JNIEXPORT void JNICALL Java_com_learnopengles_airhockey_GameLibJNIWrapper_on_1surface_1changed
	(JNIEnv * env, jclass cls, jint width, jint height) {
	on_surface_changed();
}

JNIEXPORT void JNICALL Java_com_learnopengles_airhockey_GameLibJNIWrapper_on_1draw_1frame
	(JNIEnv * env, jclass cls) {
	on_draw_frame();
}

We’ve simplified the file greatly, and we’ve also added a reference to game.h so that we can call our game methods. Here’s how it works:

  1. GameLibJNIWrapper defines the native C functions that we want to be able to call from Java.
  2. To be able to call these C functions from Java, they have to be named in a very specific way, and each function also has to have at least two parameters, with a pointer to a JNIEnv as the first parameter, and a jclass as the second parameter. To make life easier, we can use javah to create the appropriate function signatures for us in a file called jni.c.
  3. From jni.c, we call the functions that we declared in game.h and defined in game.c. That completes the connections and allows us to call our native functions from Java.

Compiling the native code

To compile and run the native code, we need to describe our native sources to the NDK build system. We’ll do this with two files that should go in the jni folder: Android.mk and Application.mk. When we added native support to our project, a file called game.cpp was automatically created in the jni folder. We won’t be needing this file, so you can go ahead and delete it.

Let’s set Android.mk to the following contents:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := game
LOCAL_CFLAGS    := -Wall -Wextra
LOCAL_SRC_FILES := ../../common/game.c jni.c
LOCAL_LDLIBS := -lGLESv2

include $(BUILD_SHARED_LIBRARY)

This file describes our sources, and tells the NDK that it should compile game.c and jni.c and build them into a shared library called libgame.so. This shared library will be dynamically linked with libGLESv2.so at runtime.

When specifying this file, be careful not to leave any trailing spaces after any of the commands, as this may cause the build to fail.

The next file, Application.mk, should have the following contents:

APP_PLATFORM := android-10
APP_ABI := armeabi-v7a

This tells the NDK build system to build for Android API 10, so that it doesn’t complain about us using unsupported features not present in earlier versions of Android, and it also tells the build system to generate a library for the ARMv7-A architecture, which supports hardware floating point and which most newer Android devices use.

Updating RendererWrapper

Before we can see our new changes, we have to update RendererWrapper to call into our native code. We can do that by updating it as follows:

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		GameLibJNIWrapper.on_surface_created();
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		GameLibJNIWrapper.on_surface_changed(width, height);
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		GameLibJNIWrapper.on_draw_frame();
	}

The renderer now calls our GameLibJNIWrapper class, which calls the native functions in jni.c, which calls our game functions defined in game.c.

Building and running the application

You should now be able to build and run the application. When you build the application, a new shared library called libgame.so should be created in your project’s /libs/armeabi-v7a/ folder. When you run the application, it should look as follows:

Second pass
Second pass

We know that our native code is being called with the color changing from blue to red.

Exploring further

The full source code for this lesson can be found at the GitHub project. For a more detailed introduction to OpenGL ES 2, see Android Lesson One: Getting Started or OpenGL ES 2 for Android: A Quick-Start Guide.

In the next part of this series, we’ll create an iOS project and we’ll see how easy it is to reuse our code from the common folder and wrap it up in Objective-C. Please let me know if you have any questions or feedback!

About the book

Android is booming like never before, with millions of devices shipping every day. In OpenGL ES 2 for Android: A Quick-Start Guide, you’ll learn all about shaders and the OpenGL pipeline, and discover the power of OpenGL ES 2.0, which is much more feature-rich than its predecessor.

It’s never been a better time to learn how to create your own 3D games and live wallpapers. If you can program in Java and you have a creative vision that you’d like to share with the world, then this is the book for you.

Share

Author: Admin

Kevin is the author of OpenGL ES 2 for Android: A Quick-Start Guide. He also has extensive experience in Android development.

23 thoughts on “Calling OpenGL from C on Android, Using the NDK”

  1. I’m curious why you chose not to use NativeActivity and EGL, rather than JNI and GLSurfaceView.
    .

    1. I can see the advantage of NativeActivity when bringing over an existing framework that has certain assumptions about the way that the system works. For new code I don’t see the advantage — the Android APIs are really meant to be used from Java, and if you want to use 3rd party libraries, you’re going to have to learn to use JNI sooner or later. 😛

      On iOS it’s the same thing with Objective-C, only that calling C from Objective-C is much simpler than calling C from Java, as there’s no need for something like a JNI wrapper.

      1. The thing is true but when you want to have one library/ engine you don’t want to have code here and there…you just want to have one library. so basically the implementation will be mostly different and you will end up creating a wrapper anyway.

        1. Please try deleting the R import and restart the IDE, then try to add the import again. When I get this sort of error it’s usually something like that.

  2. First off, thanks for a very informative example. I learned a lot. Here’s one troubling problem I have that I hope you can help me with – I haven’t had luck on the boards. It’s frustrating because I feel I should be able to work it out, but I can’t!

    As I worked through the example, Eclipse reported that GL_COLOR_BUFFER_BIT used in the glClear() call “could not be resolved” (see the on_draw_frame() function in game.c). It’s odd because I can’t eliminate that error even if I explicitly place #include at the top of the file. In order to get it to compile I looked up the value defined in gl2.h used it instead (0x00004000). That got everything to work, but now I want to render a bitmap.

    I have this at the top of game.c:

    #include
    #include

    Oddly enough, Eclipse doesn’t report any problems for things defined bitmap.h like AndroidBitmap_getInfo() and ANDROID_BITMAP_FORMAT_RGB_565 but things from gl2.h such as glTexImage2D() or the parameters passed to it like GL_TEXTURE_2D remain a problem, so now it doesn’t build.

    How can it be that bitmap.h resolves but gl2.h doesn’t? I’m declaring the includes exactly the same way!

    The C/C++>General>Paths and Symbols properties for the project has this path:
    C:\Android\android-ndk-r8e-windows-x86\android-ndk-r8e\platforms\android-9\arch-arm\usr\include

    I’ve confirmed that android & GLES2 folders exist at that path location and each contain the appropriate .h file (bitmap.h and gl2.h respectively).

    My setup:
    OS: Windows 7
    SDK: adt-bundle-windows-x86-20130717
    NDK: android-ndk-r8e
    Eclipse ADT: Build: v22.0.4-741630

    Thanks,
    Howard

    1. Hi Howard,

      Apologies for the delay, the site has been bombarded by spam and I recently sifted through it all to pull out the legitimate comments. I have the same problems with Eclipse and I ended up “working” around it by closing the C files in the editor and manually deleting the errors — after all, ndk_build has no issues. This is annoying but the only work-around I’ve found that actually works, to date. 🙁

  3. I did exactly as mentionned in this post but having an exception when trying to run it on my android tablet:

    04-19 15:54:06.231: E/AndroidRuntime(3998): FATAL EXCEPTION: GLThread 1935
    04-19 15:54:06.231: E/AndroidRuntime(3998): Process: com.prototype.androidnativeprototype, PID: 3998
    04-19 15:54:06.231: E/AndroidRuntime(3998): java.lang.UnsatisfiedLinkError: Native method not found: com.prototype.androidnativeprototype.GameLibJNIWrapper.on_surface_created:()V
    04-19 15:54:06.231: E/AndroidRuntime(3998): at com.prototype.androidnativeprototype.GameLibJNIWrapper.on_surface_created(Native Method)
    04-19 15:54:06.231: E/AndroidRuntime(3998): at com.prototype.androidnativeprototype.RendererWrapper.onSurfaceCreated(RendererWrapper.java:10)
    04-19 15:54:06.231: E/AndroidRuntime(3998): at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1501)
    04-19 15:54:06.231: E/AndroidRuntime(3998): at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1240)

    my GameLibJNIWrapper.java is exactly the same as above and is placed in com.prototype.androidnativeprototype. My jni.c goes as follow and is placed in the jni folder

    #include "../../common/game.h"
    #include

    JNIEXPORT void JNICALL Java_com_prototype_androidnativeprototype_GameLibJNIWrapper_on_1surface_1created
    (JNIEnv * env, jclass cls) {
    on_surface_created();
    }

    JNIEXPORT void JNICALL Java_com_prototype_androidnativeprototype_GameLibJNIWrapper_on_1surface_1changed
    (JNIEnv * env, jclass cls, jint width, jint height) {
    on_surface_changed();
    }

    JNIEXPORT void JNICALL Java_com_prototype_androidnativeprototype_GameLibJNIWrapper_on_1draw_1frame
    (JNIEnv * env, jclass cls) {
    on_draw_frame();
    }

    Really can’t figure what is wrong! Thanks!

    1. Hi Yanick,

      If you are compiling as C++, please enclose your function declarations and definitions with extern “C” { …}. If it’s not that, then I don’t know. :S Maybe you could upload the project somewhere?

    2. it would be great to add android “command line” build script… it’s really annoying to have to install eclipse only to build manually this part instead of for example a top makefile/script that allow to build the entire project… I don’t know about the IOS part, but for me I just want to install the android sdk, the android ndk, emscripten and native part and build all in a single shot…
      I know that android is now forcing to use gradle and android studio, but I don’t like the fact to be bound an IDE to do one application…. so maybe a generic build.gradle for the android part would be awsome ! ? I don’t know for the ndk part, gradle seems to have “experimental” support for ndk but I found no-where if it still use the android.mk or not
      I’m really new to android/IOS world but have lot of C/C++/Linux knowledge and I prefer to keep my “android” knowledge/binding as low as possible
      thanks and regards

  4. it would be great to add android “command line” build script… it’s really annoying to have to install eclipse only to build manually this part instead of for example a top makefile/script that allow to build the entire project… I don’t know about the IOS part, but for me I just want to install the android sdk, the android ndk, emscripten and native part and build all in a single shot…
    I know that android is now forcing to use gradle and android studio, but I don’t like the fact to be bound an IDE to do one application…. so maybe a generic build.gradle for the android part would be awsome ! 🙂 I don’t know for the ndk part, gradle seems to have “experimental” support for ndk but I found no-where if it still use the android.mk or not
    I’m really new to android/IOS world but have lot of C/C++/Linux knowledge and I prefer to keep my “android” knowledge/binding as low as possible
    thanks and regards
    JLM

  5. This seems to be exactly the sort of thing I’m looking for right now. I need to leverage existing C libraries and need to be able to make OpenGL calls from native code. I’m new to developing on Android Studio (and android, in general) and I’m wondering what I’ll have to do to adapt this to my needs.
    Will it be dramatically different?

  6. About Android Studio and related comments: I’ll think up of a new demo along the similar lines of this one, but with the latest build systems & IDEs. 🙂 Once that’s published (might still be a while from this comment) I’ll go ahead and link it from here.

Leave a Reply to Admin Cancel reply

Your email address will not be published. Required fields are marked *