Android で OpenGL ES 2.0 事始め 〜JNI 編〜

数回にわたり AndroidOpenGL ES 2.0 を使用した超絶簡単なサンプルを紹介して参りましたが、OpenGL を使用しているのだから、やはりネイティブな C/C++ でもっと速度を向上させたいと思うのが実践的です。 前回の記事では Android NDK での開発環境の構築手順について紹介しました。そこで今回は Beginning サンプルを JNI 化したいと思います。

今回のソースは、

  • AndroidManifest.xml
  • Activity
  • GLSurfaceView.Renderer
  • JNI ブリッジクラス
  • Android.mk
  • Application.mk
  • C/C++ ソースファイル

です。
それではご覧ください。

AndroidManifest.xml

Beginning サンプルのままです。変更はありません。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.orangesignal.android.example.gles20"
	android:versionCode="1"
	android:versionName="1.0"
	android:installLocation="auto">

	<!-- OpenGL ES 2.0 を使用するので API レベル 8 以上とします。 -->
	<uses-sdk android:minSdkVersion="8" />

	<!-- Google プレイ (旧 Android マーケット) からダウンロード可能な端末を OpenGL ES 2.0 を使用可能な端末に制限します。 -->
	<uses-feature android:glEsVersion="0x00020000" />

	<application
		android:label="@string/app_name"
		android:icon="@drawable/ic_launcher">

		<activity android:name=".ui.MainActivity">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>

	</application>

</manifest>

Activity

このクラスも Beginning サンプルから変更はありません。

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.example.gles20.ui;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;

import com.orangesignal.android.example.gles20.gl.SimpleRenderer;

/**
 * このアプリケーションの主たるアクティビティを提供します。
 * 
 * @author 杉澤 浩二
 */
public final class MainActivity extends Activity {

	private GLSurfaceView mGLSurfaceView;

	//////////////////////////////////////////////////////////////////////////
	// ライフサイクル

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

		mGLSurfaceView = new GLSurfaceView(this);
		mGLSurfaceView.setEGLContextClientVersion(2);	// OpenGL ES 2.0 を使用するように構成します。
		mGLSurfaceView.setRenderer(new SimpleRenderer(getApplicationContext()));
		setContentView(mGLSurfaceView);
	}

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

	@Override
	public void onPause() {
		super.onPause();
		mGLSurfaceView.onPause();
	}

}

GLSurfaceView.Renderer

今回は OpenGL の処理を JNI で行うため、onSurfaceCreated、onSurfaceChanged、onDrawFrame それぞれで JNI のネイティブメソッドを呼び出しているだけとなります。

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.example.gles20.gl;

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

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLSurfaceView;

import com.orangesignal.android.example.gles20.R;

/**
 * ビットマップをテクスチャとして読み込んで {@link GLSurfaceView} の描画領域いっぱいに描画するだけのシンプルな {@link GLSurfaceView.Renderer} を提供します。
 * 
 * @author 杉澤 浩二
 */
public final class SimpleRenderer implements GLSurfaceView.Renderer {

	/**
	 * コンテキストを保持します。
	 */
	private final Context mContext;

	//////////////////////////////////////////////////////////////////////////
	// コンストラクタ

	/**
	 * コンストラクタです。
	 * 
	 * @param context コンテキスト
	 */
	public JniBridge(final Context context) {
		mContext = context;
	}

	//////////////////////////////////////////////////////////////////////////
	// オーバーライド メソッド

	@Override
	public void onSurfaceCreated(final GL10 gl, final EGLConfig config) {
		// テクスチャで使用するビットマップを読み込んでピクセルデータを取得します。
		final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample);
		final int width = bitmap.getWidth();
		final int height = bitmap.getHeight();
		final int[] pixels = new int[width * height];
		bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
		bitmap.recycle();

		JniBridge.nativeOnSurfaceCreated(pixels, width, height);
	}

	@Override
	public void onSurfaceChanged(final GL10 gl, final int width, final int height) {
		JniBridge.nativeOnSurfaceChanged(width, height);
	}

	@Override
	public void onDrawFrame(final GL10 gl) {
		JniBridge.nativeOnDrawFrame();
	}

}

JNI ブリッジクラス

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

package com.orangesignal.android.example.gles20.gl;

/**
 * JNI への橋渡しをするブリッジクラスを提供します。
 * 
 * @author 杉澤 浩二
 */
public final class JniBridge {

	/**
	 * JNI のライブラリ名です。
	 */
	private static final String LIBRARY_NAME = "example";

	static {
		// JNI のライブラリ (モジュール) をロードします。
		System.loadLibrary(LIBRARY_NAME);
	}

	public static native void nativeOnSurfaceCreated(int[] pixels, int width, int height);
	public static native void nativeOnSurfaceChanged(int width, int height);
	public static native void nativeOnDrawFrame();

}

Android.mk

いわゆる make ファイルです。モジュール名やソースファイル群の指定、使用するライブラリの指定などを行います。LOCAL_MODULE で指定した名前と Java 側の System.loadLibrary で指定している名前が一致していることを確認して下さい。

#
# Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
#

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := example
LOCAL_SRC_FILES := main.cpp

# see ${ANDROID_NDK_HOME}/docs/STABLE-APIS.html
LOCAL_LDLIBS := -llog			# ログ機能を使用するので Android-specific Log Support を追加します。
LOCAL_LDLIBS += -lGLESv2		# OpenGL ES 2.0 を使用するので OpenGL ES 2.0 ライブラリを追加します。

include $(BUILD_SHARED_LIBRARY)

Application.mk

Application.mk はマストではないですが、対応プラットフォームや対応アーキテクチャなどを指定するため、実際の開発時には実質的にマストとなってくるかと思います。このサンプルでは APP_ABI にとりあえず all を指定しています。実際のアプリ開発では、対象端末に合わせて APP_ABI や APP_PLATFORM などを最適化することになります。

#APP_STL := gnustl_static
#APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := all
#APP_ABI := armeabi armeabi-v7a
#APP_PLATFORM := android-8

main.cpp

今回の肝となる C/C++ のソースファイルです。OpenGL ES 2.0 の処理を C 側へ持ってきているだけなので Java の時と大筋違いはないですがエラーチェックを若干盛っています。テクスチャの読み込みのところでは Android の Bitmap が ARGB なのに対して OpenGL が扱える RGBA へ変換しています。また glVertexAttribPointer では Java の場合と異なり GLfloat により直接パラメータの指定を行っています。

/*
 * Copyright (c) 2012 OrangeSignal.com All Rights Reserved.
 */

#include <jni.h>

#include <stdio.h>
#include <stdlib.h>

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

#include <android/log.h>

//////////////////////////////////////////////////////////////////////////////

#define LOG_TAG    "example-jni"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,  LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,  LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

//////////////////////////////////////////////////////////////////////////////

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceCreated(JNIEnv* env, jobject thiz, jintArray pixels, jint width, jint height);
JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceChanged(JNIEnv* env, jobject thiz, jint width, jint height);
JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnDrawFrame(JNIEnv* env, jobject thiz);

#ifdef __cplusplus
}
#endif

//////////////////////////////////////////////////////////////////////////////

static void printGLString(const char *name, GLenum s) {
	const char *v = (const char *) glGetString(s);
	LOGI("GL %s = %s\n", name, v);
}

/**
 * 指定された直前の OpenGL API 操作についてエラーが発生しているかどうか検証します。
 * 
 * @param op 検証する直前に操作した OpenGL API 名
 */
static void checkGlError(const char* op) {
	for (GLint error = glGetError(); error; error = glGetError()) {
		LOGE("after %s() glError (0x%x)\n", op, error);
	}
}

/**
 * 指定されたシェーダのソースコードをコンパイルします。
 * 
 * @param shaderType シェーダの種類
 * @param shaderCode シェーダのソースコード
 * @return シェーダハンドルまたは <code>0</code>
 */
static GLuint loadShader(GLenum shaderType, const char *shaderCode){
	GLuint shader = glCreateShader(shaderType);
	if (shader != 0) {
		glShaderSource(shader, 1, &shaderCode, NULL);
		glCompileShader(shader);
		GLint compiled = 0;
		glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
		if (!compiled) {
			GLint infoLen = 0;
			glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
			if (infoLen) {
				char* buf = (char*) malloc(infoLen);
				if (buf) {
					glGetShaderInfoLog(shader, infoLen, NULL, buf);
					LOGE("Could not compile shader %d:\n%s\n",shaderType, buf);
					free(buf);
				}
			}
			glDeleteShader(shader);
		}
	}
	return shader;
}

static GLuint createProgram(const char* vertexCode, const char* fragmentCode) {
	// バーテックスシェーダをコンパイルします。
	GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexCode);
	if (!vertexShader) {
		LOGE("vertex missed!");
		return 0;
	}

	// フラグメントシェーダをコンパイルします。
	GLuint pixelShader = loadShader(GL_FRAGMENT_SHADER, fragmentCode);
	if (!pixelShader) {
		LOGE("fragment missed!");
		return 0;
	}

	// プログラムを生成して、プログラムへバーテックスシェーダとフラグメントシェーダを関連付けます。
	GLuint program = glCreateProgram();
	if (program) {
		// プログラムへバーテックスシェーダを関連付けます。
		glAttachShader(program, vertexShader);
		checkGlError("glAttachShader");
		// プログラムへフラグメントシェーダを関連付けます。
		glAttachShader(program, pixelShader);
		checkGlError("glAttachShader");

		glLinkProgram(program);
		GLint linkStatus = GL_FALSE;
		glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
		if (linkStatus != GL_TRUE) {
			GLint bufLength = 0;
			glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
			if (bufLength) {
				char* buf = (char*) malloc(bufLength);
				if (buf) {
					glGetProgramInfoLog(program, bufLength, NULL, buf);
					LOGE("Could not link program:\n%s\n", buf);
					free(buf);
				}
			}
			LOGE("program missed!");
			glDeleteProgram(program);
			program = 0;
		}
	}
	return program;
}

//////////////////////////////////////////////////////////////////////////////

/**
 * ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード
 */
static const char VERTEX_SHADER[] =
	"attribute vec4 position;"
	"attribute vec2 texcoord;"
	"varying vec2 texcoordVarying;"
	"void main() {"
		"gl_Position = position;"
		"texcoordVarying = texcoord;"
	"}";

/**
 * 色描画用のピクセル/フラグメントシェーダのソースコード
 */
static const char FRAGMENT_SHADER[] =
	"precision mediump float;"
	"varying vec2 texcoordVarying;"
	"uniform sampler2D texture;"
	"void main() {"
		"gl_FragColor = texture2D(texture, texcoordVarying);"
	"}";


static GLuint program;
static GLuint position;
static GLuint texcoord;

static GLuint textures[1];

JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceCreated(JNIEnv* env, jobject thiz, jintArray pixels, jint width, jint height) {
	LOGI("nativeOnSurfaceCreated");

	printGLString("Version",    GL_VERSION);
	printGLString("Vendor",     GL_VENDOR);
	printGLString("Renderer",   GL_RENDERER);
	printGLString("Extensions", GL_EXTENSIONS);

	// プログラムを生成して使用可能にします。
	program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
	if (!program) {
		LOGE("Could not create program.");
	}
	glUseProgram(program);
	checkGlError("glUseProgram");

	// シェーダで使用する変数のハンドルを取得し使用可能にします。

	position = glGetAttribLocation(program, "position");
	checkGlError("glGetAttribLocation position");
	glEnableVertexAttribArray(position);

	texcoord = glGetAttribLocation(program, "texcoord");
	checkGlError("glGetAttribLocation texcoord");
	glEnableVertexAttribArray(texcoord);

	textures[0] = glGetUniformLocation(program, "texture");
	checkGlError("glGetUniformLocation texture");

	// テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります)

	unsigned int*  _pixels = (unsigned int*) env->GetPrimitiveArrayCritical(pixels, 0);

	// ARGB ⇒ RGBA へ変換します。
	const int size = width * height;
	for (int i = 0; i < size; i++) {
		unsigned int px = _pixels[i];
		_pixels[i] = (
				((px      ) & 0xFF000000) | // A
				((px << 16) & 0x00FF0000) | // R
				((px      ) & 0x0000FF00) | // G
				((px >> 16) & 0x000000FF)	// B
			);
	}

	// テクスチャオブジェクトを作成して画像を与えます。
	glGenTextures(1, textures);
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, _pixels);

	// テクスチャを拡大/縮小する方法を設定します。
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);		// 縮小するときピクセルの中心に最も近いテクスチャ要素で補完
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);		// 拡大するときピクセルの中心付近の線形で補完

	env->ReleasePrimitiveArrayCritical(pixels, _pixels, JNI_ABORT);
}

JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceChanged(JNIEnv* env, jobject thiz, jint width, jint height) {
	// ビューポートを設定します。
	glViewport(0, 0, width, height);
	checkGlError("glViewport");
}

/**
 * 頂点データです。
 */
static const GLfloat VERTEXS[] = {
	-1.0f,  1.0f, 0.0f,	// 左上
	-1.0f, -1.0f, 0.0f,	// 左下
	 1.0f,  1.0f, 0.0f,	// 右上
	 1.0f, -1.0f, 0.0f	// 右下
};

/**
 * テクスチャ (UV マッピング) データです。
 */
static const GLfloat TEXCOORDS[] = {
	0.0f, 0.0f,	// 左上
	0.0f, 1.0f,	// 左下
	1.0f, 0.0f,	// 右上
	1.0f, 1.0f	// 右下
};

JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnDrawFrame(JNIEnv* env, jobject thiz) {
	// XXX - このサンプルではテクスチャの簡単な描画だけなので深さ関連の有効/無効や指定は一切していません。

	// 背景色を指定して背景を描画します。
	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	// 背景とのブレンド方法を設定します。
	glEnable(GL_TEXTURE_2D);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);	// 単純なアルファブレンド

	// テクスチャの指定
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	glUniform1i(textures[0], 0);
	glVertexAttribPointer(texcoord, 2, GL_FLOAT, false, 0, TEXCOORDS);
	glVertexAttribPointer(position, 3, GL_FLOAT, false, 0, VERTEXS);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

	glDisable(GL_BLEND);
	glDisable(GL_TEXTURE_2D);
}

作成して思ったのですが、NDK の hello-gl2 とあまり変わらないかもです。