Android で OpenGL ES 2.0 事始め JNI 編 〜android/bitmap.h を使ってみる〜
先程のサンプルにちょっと手お加えて今度は、ビットマップを JNI 側から読み込むように変更してみます。(この方式は Java 側からピクセルデータを渡す方式と比べて JNI 側で画像形式のハンドリングができる特徴があります)
今回は前回のサンプルからの差分のみを紹介します。変更が発生するソースは
- SimpleRenderer
- JniBridge
- Android.mk
- Application.mk
- main.cpp
です。
SimpleRenderer
onSurfaceCreated を変更しています。今回は JNI でビットマップを読み込むので onSurfaceCreated でビットマップからピクセルデータを読み込まず Bitmap オブジェクトを渡すように変更しています。
/* * 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 { : 略 : @Override public void onSurfaceCreated(final GL10 gl, final EGLConfig config) { final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample); JniBridge.nativeOnSurfaceCreated(bitmap); bitmap.recycle(); } : 略 : }
JniBridge
nativeOnSurfaceCreated を変更しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import android.graphics.Bitmap; /** * JNI への橋渡しをするブリッジクラスを提供します。 * * @author 杉澤 浩二 */ public final class JniBridge { : 略 : public static native void nativeOnSurfaceCreated(Bitmap bitmap); : 略 : }
Android.mk
JNI で Bitmap から読み込むので、jnigraphics ライブラリを追加しています。
# # 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 ライブラリを追加します。 LOCAL_LDLIBS += -ljnigraphics # Java ビットマップオブジェクトへアクセスするので jnigraphics ライブラリを追加します。(ソースコード上で使用するには <android/bitmap.h> ヘッダファイルをインクルードする必要があります) include $(BUILD_SHARED_LIBRARY)
Application.mk
jnigraphics ライブラリは API レベル 8 から使用可能なので、APP_PLATFORM で明確化しています。
#APP_STL := gnustl_static #APP_CPPFLAGS := -frtti -fexceptions APP_ABI := all #APP_ABI := armeabi armeabi-v7a APP_PLATFORM := android-8
main.cpp
JNI で Bitmap から読み込むので、
- インクルードに android/bitmap.h を追加しています。
- nativeOnSurfaceCreated のパラメータと実装を変更しています。
- nativeOnDrawFrame での背景ブレンド方法を GL_ONE, GL_ONE_MINUS_SRC_ALPHA へ変更しています。
尚、ピクセルデータ使用時と異なり ARGB ⇒ RGBA 変換をしていないことにも注意して下さい。
/* * 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/bitmap.h> #include <android/log.h> ////////////////////////////////////////////////////////////////////////////// : 略 : ////////////////////////////////////////////////////////////////////////////// #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceCreated(JNIEnv* env, jobject thiz, jobject bitmap); 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 ////////////////////////////////////////////////////////////////////////////// : 略 : JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnSurfaceCreated(JNIEnv* env, jobject thiz, jobject bitmap) { 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"); // テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります) int ret; AndroidBitmapInfo info; // ビットマップオブジェクトに関する情報を取得します。 if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) { LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret); return; } // ビットマップの画像データをロックして画像データを操作できるようにします。 void* pixels; if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels)) < 0) { LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret); } // テクスチャオブジェクトを作成して画像を与えます。 glGenTextures(1, textures); glBindTexture(GL_TEXTURE_2D, textures[0]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, info.width, info.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); // ビットマップの画像データをアンロックします。 AndroidBitmap_unlockPixels(env, bitmap); // テクスチャを拡大/縮小する方法を設定します。 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 縮小するときピクセルの中心に最も近いテクスチャ要素で補完 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 拡大するときピクセルの中心付近の線形で補完 } : 略 : JNIEXPORT void JNICALL Java_com_orangesignal_android_example_gles20_gl_JniBridge_nativeOnDrawFrame(JNIEnv* env, jobject thiz) { : 略 : // 背景とのブレンド方法を設定します。 glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // see http://ga29.blog.fc2.com/blog-entry-37.html : 略 : } : 略 :
Android で OpenGL ES 2.0 事始め 〜JNI 編〜
数回にわたり Android で OpenGL 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 とあまり変わらないかもです。
Windows + Eclipse + CDT で JNI 開発環境を構築する 〜Android NDK 快適エンジニアリング〜
Windows 環境で Eclipse を使っている場合の JNI 開発環境について、Cygwin を導入した例が圧倒的に多いのですが、Cygwin という Eclipse と非統合な別環境を導入することで開発容易性が損なわれる。またポピュラーで JNI な OSS プロダクトのいくつかは Cygwin の導入を推奨していないものがある。など Cygwin を導入することで嬉しいことが見つけられないので、Cygwin を使わない環境構築手順を紹介してみたいと思います。
尚、以下の環境を前提に説明して参ります。
- Windows7
- Eclipse 最新版 ... 執筆時点での最新版は 4.2 (Juno)
- JDK6 の最新版 ... JDK7 は Android 開発時は面倒事が増えるので JDK6 をマストで使って下さい。
- ADT (Android Development Tools) プラグインの最新版 ... 執筆時点での最新版は 20.0.2
Windows 環境へ NDK を導入/設定する
- まず何はともあれ Android NDK のサイト から最新版の NDK をダウンロード・解凍して、空白を含まない名前の場所へ配置します。
- 次に NDK を配置した場所を Windows の環境変数として設定してます。
Eclipse へ CDT (C/C++ Development Tooling) を導入する
- サブタイトルのまんまですが Eclipse のインストールダイアログを起動して CDT (C/C++ Development Tooling) をインストールします。CDT を有効にするために Eclipse の再起動をたずねられるので再起動します。
ここまでで Windows + Eclipse 環境での下準備は完了です。続いて Eclipse 上の Android プロジェクトへコンパイル設定を紹介して参ります。
Android プロジェクトへ JNI 設定を追加する下準備
- Eclipse 上で JNI 開発したい Android プロジェクトを選択して右クリックします。
- コンテキストメニューから [新規]-[その他] を選択して [新規]ウィザードダイアログを起動します。
Android プロジェクトへ JNI 設定を追加する
- 先程選択した Android プロジェクト直下へ "jni" という名称のフォルダを作成します。(C/C++ ソースや make ファイルなど一式ここに置くことになります)
- 再度 Android プロジェクトを右クリックして今度はプロジェクトのプロパティダイアログを表示させます。
- "C/C++ ビルド" の "ビルダー設定" を行います。デフォルト・ビルド・コマンドのチェックをオフにしてビルド・コマンドへ "${ANDROID_NDK_HOME}/ndk-build.cmd" を入力します。(ANDROID_NDK_HOME は最初の手順で Windows へ設定した環境変数です。変数ボタンを押下すると Eclipse が認識している環境変数の一覧が参照できます)
- ビルド・ロケーションのビルド・ディレクトリーは JNI 開発する対象のプロジェクトを指していることを確認して下さい。多くの方は、Eclipse ワークスペース上にプロジェクトを配置していると思いますのでその場合は、"${workspace_loc:/Eclipse上でのプロジェクト名}" となっていることを確認します。
- 次に "振る舞い" を設定します。クリーンの挙動が怪しいとの報告も巷でありますが、最近は問題なくなっているようです。インクリメンタルビルドの欄はこの例では -B でリビルドされるようにしています(ブランクでも構いません)。またファイルを保存する度にビルドが走るのがうざい場合は "リソース保管時のビルド (自動ビルド)" のチェックをオフにして下さい。
※蛇足ですが、プロジェクト名の変更をした場合にリフレッシュポリシーの内容は反映されないので、SVN などでチーム開発している場合やプロジェクト名をしばしば変更する場合はリフレッシュポリシーの設定を削るか修正する必要があります。でないとコンパイル時に謎なエラーで悩まされることでしょう。なので、僕的にはこの時点で設定を削除することを推奨。
- 続いて "C/C++ 一般" の "パスおよびシンボル" を選択します。
- インクルードタブの[追加]をクリックして NDK のインクルードパスを指定します。
- gnu-libstdc++ も使用する場合は "${ANDROID_NDK_HOME}/sources/cxx-stl/gnu-libstdc++/4.6/include" もしくは "${ANDROID_NDK_HOME}/sources/cxx-stl/gnu-libstdc++/4.4.3/include" をインクルードパスへ追加して下さい。
- 最後に "ソースロケーション" タブを選択して、先程作成した jni フォルダをソースロケーションへ加え、デフォルトのプロジェクト直下のソースロケーションを削除して[OK]します。
これで jni フォルダへ make ファイルや C/C++ ソースコードを配置することでプロジェクト実行時に自動的にビルドされ実行することが可能となりました。
それでは快適な JNI エンジニアリングライフを!
Android で OpenGL ES 2.0 事始め その弐 〜画面キャプチャ〜
今回は前回の超入門に画面キャプチャ機能を追加してみたいと思います。
GLSurfaceView.draw(canvas) で簡単に画面キャプチャできるかと思っていたのですが、そうは問屋がおろさなかったです。
今回のソースは、
- AndroidManifest.xml
- Activity
- GLSurfaceView
- GLSurfaceView.Renderer
- OpenGL ES 2.0 簡易ユーティリティ
- フォトギャラリーへ書き込むためのユーティリティ
で構成しています。
リファクタリングしながら作っているので一部クラス名に変更があります。
それではご覧ください。
AndroidManifest.xml
今回のサンプルでは画面をキャプチャしてストレージへ保存するので、uses-permission に WRITE_EXTERNAL_STORAGE と READ_EXTERNAL_STORAGE を加えています。
<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" /> <!-- 画像を保存するのでストレージへアクセスすることを宣言します。 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 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
今回のサンプルでは、メニューから "Capture" を選択した場合にキャプチャの要求をします。
Activity.onCreate 中の mPhotoView.setOnCaptureListener でキャプチャ用のリスナーを設定しています。
そして Activity に PhotoView.OnCaptureListener を実装して onCapture(Bitmap) でキャプチャ後の処理を行っています。
尚 UI からの入力があることから GLSurfaceView のサブクラス (PhotoView) を作成/使用しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.ui; import android.app.Activity; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import com.orangesignal.android.example.gles20.gl.PhotoView; import com.orangesignal.android.media.ImageMediaUtils; /** * このアプリケーションの主たるアクティビティを提供します。 * * @author 杉澤 浩二 */ public final class MainActivity extends Activity implements PhotoView.OnCaptureListener { private PhotoView mPhotoView; ////////////////////////////////////////////////////////////////////////// // ライフサイクル @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPhotoView = new PhotoView(this); mPhotoView.setOnCaptureListener(this); setContentView(mPhotoView); } @Override public void onResume() { super.onResume(); mPhotoView.onResume(); } @Override public void onPause() { super.onPause(); mPhotoView.onPause(); } ////////////////////////////////////////////////////////////////////////// // メニュー private static final int MENU_CAPTURE = 0; @Override public boolean onCreateOptionsMenu(final Menu menu) { menu.add(Menu.NONE, MENU_CAPTURE, Menu.NONE, "Capture"); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case MENU_CAPTURE: mPhotoView.requestCapture(); return true; default: return super.onOptionsItemSelected(item); } } ////////////////////////////////////////////////////////////////////////// // PhotoView.OnCaptureListener @Override public void onCapture(final Bitmap bitmap) { // MediaStore.Images.Media.insertImage で画像を保存すると期待通りの形式や画質にならないので自前で保存します。 final Uri uri = ImageMediaUtils.savePngImage(getApplicationContext(), bitmap); bitmap.recycle(); final StringBuffer sb = new StringBuffer(); if (uri == null) { sb.append("failure"); } else { sb.append("success [").append(uri).append(']'); } Toast.makeText(getApplicationContext(), sb, Toast.LENGTH_LONG).show(); } }
GLSurfaceView
UI イベントを処理して、必要があれば Renderer へ処理を要求するための GLSurfaceView クラスです。
このクラスでは基本的には OnCaptureListener を操作しているだけですが、
別スレッドで動作している Renderer への処理要求に queueEvent を使用していることに注意して下さい。この辺も公式サイト記載のままですね。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import android.content.Context; import android.graphics.Bitmap; import android.opengl.GLSurfaceView; import android.util.AttributeSet; /** * UI からの入力を受け付けて {@link GLSurfaceView.Renderer} へ処理を依頼するための {@link GLSurfaceView} のサブクラスを提供します。 * * @author 杉澤 浩二 */ public final class PhotoView extends GLSurfaceView { /** * 画像データをキャプチャするためのコールバックインタフェースを提供します。 */ public interface OnCaptureListener { /** * 画像を取得した場合に呼び出されます。 * * @param bitmap 画像データの {@link Bitmap} */ void onCapture(Bitmap bitmap); } ////////////////////////////////////////////////////////////////////////// /* package */ PhotoRenderer mRenderer; /** * 画像データをキャプチャするためのコールバックインタフェースを保持します。 */ /* package */ OnCaptureListener mOnCaptureListener; ////////////////////////////////////////////////////////////////////////// // Constructors /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can access the current theme, resources, etc. */ public PhotoView(final Context context) { super(context); initialize(context); } /** * Constructor that is called when inflating a view from XML. * This is called when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. * * @param context The Context the view is running in, through which it can access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. */ public PhotoView(final Context context, final AttributeSet attrs) { super(context, attrs); initialize(context); } private void initialize(final Context context) { setEGLContextClientVersion(2); // OpenGL ES 2.0 を使用するように構成します。 mRenderer = new PhotoRenderer(context); setRenderer(mRenderer); } ////////////////////////////////////////////////////////////////////////// // public method /** * 画像データのキャプチャを要求します。 * * @see {@link #setOnCaptureListener(OnCaptureListener)} */ public void requestCapture() { // GLSurfaceView.Renderer は別スレッドで動作しているので Renderer への操作は GLSurfaceView.queueEvent を使用して行います。 queueEvent(new Runnable() { @Override public void run() { mRenderer.capture(mOnCaptureListener); } }); } ////////////////////////////////////////////////////////////////////////// // setter / getter /** * 画像データをキャプチャするためのコールバックインタフェースを設定します。 * * @param l 画像データをキャプチャするためのコールバックインタフェース * @see {@link #requestCapture()} */ public void setOnCaptureListener(final OnCaptureListener l) { mOnCaptureListener = l; } }
GLSurfaceView.Renderer
前回のソースからの変更は、まずクラス名を SimpleRenderer ⇒ PhotoRenderer へ変更しました。
そしてキャプチャ用のメソッドや変数、UI スレッドへコールバックするための Handler などを追加しています。
変更はクラス名のみであとは追加です。
肝心のキャプチャ処理は glReadPixels でピクセルデータを取得しています。
但しそのまま Android の Bitmap へ変換すると、赤色と青色が逆だったり、上下が逆だったりするので修正しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import java.nio.FloatBuffer; import java.nio.IntBuffer; 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.GLES20; import android.opengl.GLSurfaceView; import android.os.Handler; import com.orangesignal.android.example.gles20.R; import com.orangesignal.android.example.gles20.gl.PhotoView.OnCaptureListener; import com.orangesignal.android.opengl.GLES20Utils; /** * ビットマップをテクスチャとして読み込んで {@link GLSurfaceView} の描画領域いっぱいに描画するだけのシンプルな {@link GLSurfaceView.Renderer} を提供します。 * * @author 杉澤 浩二 */ public final class PhotoRenderer implements GLSurfaceView.Renderer { /** * コンテキストを保持します。 */ private final Context mContext; /** * UI スレッドへコールバックを返すための {@link Handler} オブジェクトを保持します。 */ private Handler mHandler = new Handler(); private int mWidth; private int mHeight; /** * 頂点データです。 */ private static final float 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 マッピング) データです。 */ private static final float TEXCOORDS[] = { 0.0f, 0.0f, // 左上 0.0f, 1.0f, // 左下 1.0f, 0.0f, // 右上 1.0f, 1.0f // 右下 }; /** * 頂点バッファを保持します。 */ private final FloatBuffer mVertexBuffer = GLES20Utils.createBuffer(VERTEXS); /** * テクスチャ (UV マッピング) バッファを保持します。 */ private final FloatBuffer mTexcoordBuffer = GLES20Utils.createBuffer(TEXCOORDS); private int mProgram; private int mPosition; private int mTexcoord; private int mTexture; private int mTextureId; ////////////////////////////////////////////////////////////////////////// // コンストラクタ /** * コンストラクタです。 * * @param context コンテキスト */ public PhotoRenderer(final Context context) { mContext = context; } ////////////////////////////////////////////////////////////////////////// // オーバーライド メソッド /** * ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード */ private static final String VERTEX_SHADER = "attribute vec4 position;" + "attribute vec2 texcoord;" + "varying vec2 texcoordVarying;" + "void main() {" + "gl_Position = position;" + "texcoordVarying = texcoord;" + "}"; /** * 色描画用のピクセル/フラグメントシェーダのソースコード */ private static final String FRAGMENT_SHADER = "precision mediump float;" + "varying vec2 texcoordVarying;" + "uniform sampler2D texture;" + "void main() {" + "gl_FragColor = texture2D(texture, texcoordVarying);" + "}"; @Override public void onSurfaceCreated(final GL10 gl, final EGLConfig config) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // プログラムを生成して使用可能にします。 mProgram = GLES20Utils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (mProgram == 0) { throw new IllegalStateException(); } GLES20.glUseProgram(mProgram); GLES20Utils.checkGlError("glUseProgram"); // シェーダで使用する変数のハンドルを取得し使用可能にします。 mPosition = GLES20.glGetAttribLocation(mProgram, "position"); GLES20Utils.checkGlError("glGetAttribLocation position"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for position"); } GLES20.glEnableVertexAttribArray(mPosition); mTexcoord = GLES20.glGetAttribLocation(mProgram, "texcoord"); GLES20Utils.checkGlError("glGetAttribLocation texcoord"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for texcoord"); } GLES20.glEnableVertexAttribArray(mTexcoord); mTexture = GLES20.glGetUniformLocation(mProgram, "texture"); GLES20Utils.checkGlError("glGetUniformLocation texture"); if (mTexture == -1) { throw new IllegalStateException("Could not get uniform location for texture"); } // テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります) final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample); mTextureId = GLES20Utils.loadTexture(bitmap); bitmap.recycle(); } @Override public void onSurfaceChanged(final GL10 gl, final int width, final int height) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 mWidth = width; mHeight = height; // ビューポートを設定します。 GLES20.glViewport(0, 0, width, height); GLES20Utils.checkGlError("glViewport"); } @Override public void onDrawFrame(final GL10 gl) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // XXX - このサンプルではテクスチャの簡単な描画だけなので深さ関連の有効/無効や指定は一切していません。 // 背景色を指定して背景を描画します。 GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 背景とのブレンド方法を設定します。 GLES20.glEnable(GLES20.GL_TEXTURE_2D); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); // 単純なアルファブレンド // テクスチャの指定 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); GLES20.glUniform1i(mTexture, 0); GLES20.glVertexAttribPointer(mTexcoord, 2, GLES20.GL_FLOAT, false, 0, mTexcoordBuffer); GLES20.glVertexAttribPointer(mPosition, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glDisable(GLES20.GL_TEXTURE_2D); } ////////////////////////////////////////////////////////////////////////// /* package */ void capture(final OnCaptureListener callback) throws NullPointerException { if (callback == null) { throw new NullPointerException("OnCaptureListener must not be null."); } final Bitmap bitmap = capture(); // コールバックします。 mHandler.post(new Runnable() { @Override public void run() { callback.onCapture(bitmap); } }); } private Bitmap capture() { final int width = mWidth; final int height = mHeight; final int pixels[] = new int[width * height]; final IntBuffer buffer = IntBuffer.wrap(pixels); buffer.position(0); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); return GLES20Utils.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); } }
OpenGL ES 2.0 簡易ユーティリティ
glReadPixels で取得したピクセルデータを処理して Bitmap を作る createBitmap メソッドを追加しています。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.opengl.GLES20; import android.opengl.GLException; import android.opengl.GLUtils; import android.util.Log; /** * OpenGL ES 2.0 に関するユーティリティを提供します。 * * @author 杉澤 浩二 */ public final class GLES20Utils { /** * ログ出力用のタグです。 */ private static String TAG = "GLES20Utils"; /** * オブジェクトが無効であることを表します。<p> * * @see {@link #createProgram(String, String)} * @see {@link #loadShader(int, String)} */ public static final int INVALID = 0; /** * インスタンス化できない事を強制します。 */ private GLES20Utils() {} /** * 最初の要素の位置です。 */ private static final int FIRST_INDEX = 0; private static final int DEFAULT_OFFSET = 0; private static final int FLOAT_SIZE_BYTES = 4; /** * 指定されたプリミティブ型配列のデータを {@link FloatBuffer} へ変換して返します。 * * @param array バッファデータ * @return 変換されたバッファデータ * @see {@link GLES20#glVertexAttribPointer(int, int, int, boolean, int, java.nio.Buffer)} */ public static FloatBuffer createBuffer(float[] array) { final FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); buffer.put(array).position(FIRST_INDEX); return buffer; } /** * 指定されたバーテックスシェーダとフラグメントシェーダを使用してプログラムを生成します。 * * @param vertexSource ポリゴン描画用バーテックスシェーダのソースコード * @param fragmentSource 色描画用のフラグメントシェーダのソースコード * @return プログラムハンドラまたは {@link #INVALID} * @throws GLException OpenGL API の操作に失敗した場合 */ public static int createProgram(final String vertexSource, final String fragmentSource) throws GLException { // バーテックスシェーダをコンパイルします。 final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == INVALID) { return INVALID; } // フラグメントシェーダをコンパイルします。 final int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == INVALID) { return INVALID; } // プログラムを生成して、プログラムへバーテックスシェーダとフラグメントシェーダを関連付けます。 int program = GLES20.glCreateProgram(); if (program != INVALID) { // プログラムへバーテックスシェーダを関連付けます。 GLES20.glAttachShader(program, vertexShader); checkGlError("glAttachShader"); // プログラムへフラグメントシェーダを関連付けます。 GLES20.glAttachShader(program, pixelShader); checkGlError("glAttachShader"); GLES20.glLinkProgram(program); final int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, DEFAULT_OFFSET); if (linkStatus[FIRST_INDEX] != GLES20.GL_TRUE) { Log.e(TAG, "Could not link program: "); Log.e(TAG, GLES20.glGetProgramInfoLog(program)); GLES20.glDeleteProgram(program); program = INVALID; } } return program; } /** * 指定されたシェーダのソースコードをコンパイルします。 * * @param shaderType シェーダの種類 * @param source シェーダのソースコード * @return シェーダハンドラまたは {@link #INVALID} * @see {@link GLES20#GL_VERTEX_SHADER} * @see {@link GLES20.GL_FRAGMENT_SHADER} */ public static int loadShader(final int shaderType, final String source) { int shader = GLES20.glCreateShader(shaderType); if (shader != INVALID) { GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); final int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, DEFAULT_OFFSET); if (compiled[FIRST_INDEX] == INVALID) { Log.e(TAG, "Could not compile shader " + shaderType + ":"); Log.e(TAG, GLES20.glGetShaderInfoLog(shader)); GLES20.glDeleteShader(shader); shader = INVALID; } } return shader; } /** * 指定された直前の OpenGL API 操作についてエラーが発生しているかどうか検証します。 * * @param op 検証する直前に操作した OpenGL API 名 * @throws GLException 直前の OpenGL API 操作でエラーが発生している場合 */ public static void checkGlError(final String op) throws GLException { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e(TAG, op + ": glError " + error); throw new GLException(error, op + ": glError " + error); } } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @return テクスチャ ID */ public static int loadTexture(final Bitmap bitmap) { return loadTexture(bitmap, GLES20.GL_NEAREST, GLES20.GL_LINEAR); } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。<p> * この実装は簡易なテクスチャの初期化のみで繰り返しの指定をサポートしません。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @param min テクスチャを縮小するときの補完方法 * @param mag テクスチャを拡大するときの補完方法 * @return テクスチャ ID * @see {@link GLES20#GL_TEXTURE_MIN_FILTER} * @see {@link GLES20#GL_TEXTURE_MAG_FILTER} */ public static int loadTexture(final Bitmap bitmap, final int min, final int mag) { final int[] textures = new int[1]; GLES20.glGenTextures(1, textures, DEFAULT_OFFSET); final int texture = textures[FIRST_INDEX]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); // テクスチャを拡大/縮小する方法を設定します。 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, min); GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, mag); return texture; } /* * @see http://www.anddev.org/how_to_get_opengl_screenshot__useful_programing_hint-t829.html */ public static Bitmap createBitmap(final int[] pixels, final int width, final int height, final Bitmap.Config config) { // 取得したピクセルデータは R (赤) と B (青) が逆になっています。 // また垂直方向も逆になっているので以下のように ColorMatrix と Matrix を使用して修正します。 /* * カラーチャネルを交換するために ColorMatrix と ColorMatrixFilter を使用します。 * * 5x4 のマトリックス: [ * a, b, c, d, e, * f, g, h, i, j, * k, l, m, n, o, * p, q, r, s, t * ] * * RGBA カラーへ適用する場合、以下のように計算します: * * R' = a * R + b * G + c * B + d * A + e; * G' = f * R + g * G + h * B + i * A + j; * B' = k * R + l * G + m * B + n * A + o; * A' = p * R + q * G + r * B + s * A + t; * * R (赤) と B (青) を交換したいので以下の様になります。 * * R' = B => 0, 0, 1, 0, 0 * G' = G => 0, 1, 0, 0, 0 * B' = R => 1, 0, 0, 0, 0 * A' = A => 0, 0, 0, 1, 0 */ final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); // R (赤) と B (青) が逆なので交換します。 paint.setColorFilter(new ColorMatrixColorFilter(new ColorMatrix(new float[] { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0 }))); final Bitmap bitmap = Bitmap.createBitmap(width, height, config); final Canvas canvas = new Canvas(bitmap); // 上下が逆さまなので垂直方向に反転させます。 final Matrix matrix = new Matrix(); matrix.postScale(1.0f, -1.0f); matrix.postTranslate(0, height); canvas.concat(matrix); // 描画します。 canvas.drawBitmap(pixels, 0, width, 0, 0, width, height, false, paint); return bitmap; } }
フォトギャラリーへ書き込むためのユーティリティ
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.media; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.provider.MediaStore; import android.provider.MediaStore.Images; /** * * @author 杉澤 浩二 */ public final class ImageMediaUtils { /** * インスタンス化できない事を強制します。 */ private ImageMediaUtils() {} /** * 指定された {@link Bitmap} を {@link MediaStore.Images.Media.EXTERNAL_CONTENT_URI} のコンテンツとして PNG 形式で保存します。 * * @param context コンテキスト * @param bitmap {@link Bitmap} * @return コンテンツの {@link Uri} または <code>null</code> */ public static Uri savePngImage(final Context context, final Bitmap bitmap) { final ContentResolver recolver = context.getContentResolver(); final ContentValues values = new ContentValues(); values.put(Images.Media.MIME_TYPE, "image/png"); // XXX - 外部ストレージがマウントされていない場合に例外が発生するのは呼び出す側の責務で良いか final Uri uri = recolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri != null) { boolean success = false; final Cursor cursor = recolver.query(uri, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { final String filename = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)); try { final FileOutputStream out = new FileOutputStream(filename); try { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // PNG なので quality は無視されるのでとりあえず 100 を設定しています。 success = true; } finally { try { out.close(); } catch (final IOException e) {} // 無視する } } catch (final FileNotFoundException e) {} // 無視する break; } } if (!success) { recolver.delete(uri, null, null); return null; } } return uri; } }
Android で OpenGL ES 2.0 事始め 〜Beginning〜
夏休みでちょっと時間もできたし、せっかくなので Android で OpenGL ES 2.0 にトライしてみました。
以下の Beginning サンプルでは超入門として ApiDemos よりシンプルなサンプルを目指してみました。
なので、ビットマップを読み込んでテクスチャとしてそのまま表示するだけの物となっています。
ソースは
だけです。
エミュレータ(Android OS 4.0.3 以上)で実行する場合は、デバイスのハードウェアプロパティで GPU emulation を yes にして下さい。
AndroidManifest.xml
Android Developers の OpenGL に記載されてるままですが、OpenGL ES 2.0 を使用するので最低の API レベル に 8 を指定しています。そして uses-feature で OpenGL ES 2.0 を使用可能な端末に制限します。
<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
ApiDemos より短いです。ApiDemos の GLES20Activity#detectOpenGLES20 はエミュレータでは正しい値が帰ってこないので、OpenGL ES 2.0 しかサポートしない場合は、setEGLContextClientVersion(2) として例外をキャッチしてごにょごにょする方が良いのではないかと。
/* * 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
このサンプルの肝心なところですね。ソースにいっぱいコメント書いてみたのでご覧ください。あとしつこいくらいにエラーチェックも入れてみました。
サンプル中の R.drawable.sample は res/drawable-nodpi に何か適当な表示させたい画像を置いて下さい。
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.example.gles20.gl; import java.nio.FloatBuffer; 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.GLES20; import android.opengl.GLSurfaceView; import com.orangesignal.android.example.gles20.R; import com.orangesignal.android.opengl.GLES20Utils; /** * ビットマップをテクスチャとして読み込んで {@link GLSurfaceView} の描画領域いっぱいに描画するだけのシンプルな {@link GLSurfaceView.Renderer} を提供します。 * * @author 杉澤 浩二 */ public final class SimpleRenderer implements GLSurfaceView.Renderer { /** * コンテキストを保持します。 */ private final Context mContext; /** * 頂点データです。 */ private static final float 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 マッピング) データです。 */ private static final float TEXCOORDS[] = { 0.0f, 0.0f, // 左上 0.0f, 1.0f, // 左下 1.0f, 0.0f, // 右上 1.0f, 1.0f // 右下 }; /** * 頂点バッファを保持します。 */ private final FloatBuffer mVertexBuffer = GLES20Utils.createBuffer(VERTEXS); /** * テクスチャ (UV マッピング) バッファを保持します。 */ private final FloatBuffer mTexcoordBuffer = GLES20Utils.createBuffer(TEXCOORDS); private int mProgram; private int mPosition; private int mTexcoord; private int mTexture; private int mTextureId; ////////////////////////////////////////////////////////////////////////// // コンストラクタ /** * コンストラクタです。 * * @param context コンテキスト */ public SimpleRenderer(final Context context) { mContext = context; } ////////////////////////////////////////////////////////////////////////// // オーバーライド メソッド /** * ポリゴン描画用のバーテックスシェーダ (頂点シェーダ) のソースコード */ private static final String VERTEX_SHADER = "attribute vec4 position;" + "attribute vec2 texcoord;" + "varying vec2 texcoordVarying;" + "void main() {" + "gl_Position = position;" + "texcoordVarying = texcoord;" + "}"; /** * 色描画用のピクセル/フラグメントシェーダのソースコード */ private static final String FRAGMENT_SHADER = "precision mediump float;" + "varying vec2 texcoordVarying;" + "uniform sampler2D texture;" + "void main() {" + "gl_FragColor = texture2D(texture, texcoordVarying);" + "}"; @Override public void onSurfaceCreated(final GL10 gl, final EGLConfig config) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // プログラムを生成して使用可能にします。 mProgram = GLES20Utils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (mProgram == 0) { throw new IllegalStateException(); } GLES20.glUseProgram(mProgram); GLES20Utils.checkGlError("glUseProgram"); // シェーダで使用する変数のハンドルを取得し使用可能にします。 mPosition = GLES20.glGetAttribLocation(mProgram, "position"); GLES20Utils.checkGlError("glGetAttribLocation position"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for position"); } GLES20.glEnableVertexAttribArray(mPosition); mTexcoord = GLES20.glGetAttribLocation(mProgram, "texcoord"); GLES20Utils.checkGlError("glGetAttribLocation texcoord"); if (mPosition == -1) { throw new IllegalStateException("Could not get attrib location for texcoord"); } GLES20.glEnableVertexAttribArray(mTexcoord); mTexture = GLES20.glGetUniformLocation(mProgram, "texture"); GLES20Utils.checkGlError("glGetUniformLocation texture"); if (mTexture == -1) { throw new IllegalStateException("Could not get uniform location for texture"); } // テクスチャを作成します。(サーフェスが作成される度にこれを行う必要があります) final Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.sample); mTextureId = GLES20Utils.loadTexture(bitmap); bitmap.recycle(); } @Override public void onSurfaceChanged(final GL10 gl, final int width, final int height) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // ビューポートを設定します。 GLES20.glViewport(0, 0, width, height); GLES20Utils.checkGlError("glViewport"); } @Override public void onDrawFrame(final GL10 gl) { // OpenGL ES 2.0 を使用するので、パラメータで渡された GL10 インターフェースを無視して、代わりに GLES20 クラスの静的メソッドを使用します。 // XXX - このサンプルではテクスチャの簡単な描画だけなので深さ関連の有効/無効や指定は一切していません。 // 背景色を指定して背景を描画します。 GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 背景とのブレンド方法を設定します。 GLES20.glEnable(GLES20.GL_TEXTURE_2D); GLES20.glEnable(GLES20.GL_BLEND); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); // 単純なアルファブレンド // テクスチャの指定 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId); GLES20.glUniform1i(mTexture, 0); GLES20.glVertexAttribPointer(mTexcoord, 2, GLES20.GL_FLOAT, false, 0, mTexcoordBuffer); GLES20.glVertexAttribPointer(mPosition, 3, GLES20.GL_FLOAT, false, 0, mVertexBuffer); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glDisable(GLES20.GL_TEXTURE_2D); } }
OpenGL ES 2.0 簡易ユーティリティ
/* * Copyright (c) 2012 OrangeSignal.com All Rights Reserved. */ package com.orangesignal.android.opengl; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import android.graphics.Bitmap; import android.opengl.GLES20; import android.opengl.GLException; import android.opengl.GLUtils; import android.util.Log; /** * OpenGL ES 2.0 に関するユーティリティを提供します。 * * @author 杉澤 浩二 */ public final class GLES20Utils { /** * ログ出力用のタグです。 */ private static String TAG = "GLES20Utils"; /** * オブジェクトが無効であることを表します。<p> * * @see {@link #createProgram(String, String)} * @see {@link #loadShader(int, String)} */ public static final int INVALID = 0; /** * インスタンス化できない事を強制します。 */ private GLES20Utils() {} /** * 最初の要素の位置です。 */ private static final int FIRST_INDEX = 0; private static final int DEFAULT_OFFSET = 0; private static final int FLOAT_SIZE_BYTES = 4; /** * 指定されたプリミティブ型配列のデータを {@link FloatBuffer} へ変換して返します。 * * @param array バッファデータ * @return 変換されたバッファデータ * @see {@link GLES20#glVertexAttribPointer(int, int, int, boolean, int, java.nio.Buffer)} */ public static FloatBuffer createBuffer(float[] array) { final FloatBuffer buffer = ByteBuffer.allocateDirect(array.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); buffer.put(array).position(FIRST_INDEX); return buffer; } /** * 指定されたバーテックスシェーダとフラグメントシェーダを使用してプログラムを生成します。 * * @param vertexSource ポリゴン描画用バーテックスシェーダのソースコード * @param fragmentSource 色描画用のフラグメントシェーダのソースコード * @return プログラムハンドラまたは {@link #INVALID} * @throws GLException OpenGL API の操作に失敗した場合 */ public static int createProgram(final String vertexSource, final String fragmentSource) throws GLException { // バーテックスシェーダをコンパイルします。 final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == INVALID) { return INVALID; } // フラグメントシェーダをコンパイルします。 final int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == INVALID) { return INVALID; } // プログラムを生成して、プログラムへバーテックスシェーダとフラグメントシェーダを関連付けます。 int program = GLES20.glCreateProgram(); if (program != INVALID) { // プログラムへバーテックスシェーダを関連付けます。 GLES20.glAttachShader(program, vertexShader); checkGlError("glAttachShader"); // プログラムへフラグメントシェーダを関連付けます。 GLES20.glAttachShader(program, pixelShader); checkGlError("glAttachShader"); GLES20.glLinkProgram(program); final int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, DEFAULT_OFFSET); if (linkStatus[FIRST_INDEX] != GLES20.GL_TRUE) { Log.e(TAG, "Could not link program: "); Log.e(TAG, GLES20.glGetProgramInfoLog(program)); GLES20.glDeleteProgram(program); program = INVALID; } } return program; } /** * 指定されたシェーダのソースコードをコンパイルします。 * * @param shaderType シェーダの種類 * @param source シェーダのソースコード * @return シェーダハンドラまたは {@link #INVALID} * @see {@link GLES20#GL_VERTEX_SHADER} * @see {@link GLES20.GL_FRAGMENT_SHADER} */ public static int loadShader(final int shaderType, final String source) { int shader = GLES20.glCreateShader(shaderType); if (shader != INVALID) { GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); final int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, DEFAULT_OFFSET); if (compiled[FIRST_INDEX] == INVALID) { Log.e(TAG, "Could not compile shader " + shaderType + ":"); Log.e(TAG, GLES20.glGetShaderInfoLog(shader)); GLES20.glDeleteShader(shader); shader = INVALID; } } return shader; } /** * 指定された直前の OpenGL API 操作についてエラーが発生しているかどうか検証します。 * * @param op 検証する直前に操作した OpenGL API 名 * @throws GLException 直前の OpenGL API 操作でエラーが発生している場合 */ public static void checkGlError(final String op) throws GLException { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e(TAG, op + ": glError " + error); throw new GLException(error, op + ": glError " + error); } } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @return テクスチャ ID */ public static int loadTexture(final Bitmap bitmap) { return loadTexture(bitmap, GLES20.GL_NEAREST, GLES20.GL_LINEAR); } /** * 指定された {@link Bitmap} 情報をテクスチャへ紐付けます。<p> * この実装は簡易なテクスチャの初期化のみで繰り返しの指定をサポートしません。 * * @param bitmap テクスチャへ紐付ける {@link Bitmap} 情報 * @param min テクスチャを縮小するときの補完方法 * @param mag テクスチャを拡大するときの補完方法 * @return テクスチャ ID * @see {@link GLES20#GL_TEXTURE_MIN_FILTER} * @see {@link GLES20#GL_TEXTURE_MAG_FILTER} */ public static int loadTexture(final Bitmap bitmap, final int min, final int mag) { final int[] textures = new int[1]; GLES20.glGenTextures(1, textures, DEFAULT_OFFSET); final int texture = textures[FIRST_INDEX]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); // テクスチャを拡大/縮小する方法を設定します。 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, min); GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, mag); return texture; } }