ソースを共用しながら、AndroidとWindowsの両方で動かせるようにしてみました。C++を主に使うのでNDKも入れておきます。
Android StudioでassetsフォルダやC++のソースを階層の浅い場所に移動
Androidのassetsフォルダにテクスチャやシェーダーを配置します。ただ、eclipse時代と違ってAndroid Studioはデフォルトで app/src/main/assetsとすごく深い場所にあります。これでは不便なので、build.gradle に以下のように書いて使いやすい場所を指定します。(同時にC++のソースも浅い場所に移動しています)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
android { | |
sourceSets { | |
main.jni.srcDirs = ['../../cpp'] | |
main.assets.srcDirs = ['../../assets'] | |
} | |
} |
main.assets.srcDirsはassetsを置く場所、main.jni.srcDirsはC++のファイルを置く場所です。'../../assets'のようにAndroid Studioのプロジェクトファイルより上位のフォルダも指定できることがわかりました。
Visual Studioからも同じソースやファイルを使うので、この任意のフォルダ位置を指定する機能は重宝します。
assetsフォルダをC++から使う
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Helper { | |
private static Context context; | |
public static void setContext(Context c) { context = c; } | |
public static byte[] loadIntoBytes(String fileName) { | |
AssetManager assetManager = context.getAssets(); | |
try { | |
InputStream is = assetManager.open(fileName); | |
byte buf[] = new byte[is.available()]; | |
is.read(buf); | |
return buf; | |
} catch (IOException e) { | |
} | |
return null; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const char* boundJavaClass = "common/pinotnoir/Helper"; | |
void *LoadFile(const char *fileName, int* size) | |
{ | |
jclass myview = jniEnv->FindClass(boundJavaClass); | |
jmethodID method = jniEnv->GetStaticMethodID(myview, "loadIntoBytes", "(Ljava/lang/String;)[B"); | |
if (method == 0) { | |
return nullptr; | |
} | |
jobject arrayAsJObject = jniEnv->CallStaticObjectMethod(myview, method, jniEnv->NewStringUTF(fileName)); | |
jbyteArray array = (jbyteArray)arrayAsJObject; | |
jbyte* byteArray = jniEnv->GetByteArrayElements(array, NULL); | |
jsize arrayLen = jniEnv->GetArrayLength(array); | |
void* ptr = calloc(arrayLen + 1, 1); | |
memcpy(ptr, byteArray, arrayLen); | |
if (size) { | |
*size = arrayLen; | |
} | |
jniEnv->ReleaseByteArrayElements(array, byteArray, 0); | |
return ptr; | |
} |
テクスチャをJava経由でC++から生成
Android機はGPUによって対応している圧縮テクスチャフォーマットが違うので面倒です。この対応は後で考える事とし、とりあえず手始めにjpgやpngなどをまず使えるようにしておきます。
AndroidではJavaから直接テクスチャを生成すると簡単です。assetsフォルダからBitmapFactory.decodeStreamでBitmapを生成し、更にBitmapからGLUtils.texImage2Dでテクスチャを生成します。JavaからC++に渡すのはGLuint型(Javaではint型)の生成済みのテクスチャを表すtexture nameだけなのでシンプルに実装できます。
それぞれAndroid版と同名のローダを作ります。
LoadFileはassetsフォルダのファイルをfopenでファイルを読み込むだけです。
LoadTextureViaOSではGDI+を使うことで、アルファチャンネル付きの32bitのrawデータが得られます。ここからテクスチャを生成しています。
AndroidではJavaから直接テクスチャを生成すると簡単です。assetsフォルダからBitmapFactory.decodeStreamでBitmapを生成し、更にBitmapからGLUtils.texImage2Dでテクスチャを生成します。JavaからC++に渡すのはGLuint型(Javaではint型)の生成済みのテクスチャを表すtexture nameだけなのでシンプルに実装できます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Helper { | |
... | |
public static int loadTexture(String s){ | |
Bitmap img; | |
try { | |
img = BitmapFactory.decodeStream(context.getAssets().open(s)); | |
} catch (IOException e) { | |
return -1; | |
} | |
int tex[] = new int[1]; | |
GLES20.glGenTextures(1, tex, 0); | |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tex[0]); | |
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, img, 0); | |
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); | |
GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_LINEAR); | |
GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); | |
GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); | |
GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); | |
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); | |
img.recycle(); | |
return tex[0]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
static GLuint LoadTextureViaOS(const char* name) | |
{ | |
jclass myview = jniEnv->FindClass(boundJavaClass); | |
jmethodID method = method = jniEnv->GetStaticMethodID(myview, "loadTexture", "(Ljava/lang/String;)I"); | |
if (method == 0) { | |
return 0; | |
} | |
return jniEnv->CallStaticIntMethod(myview, method, jniEnv->NewStringUTF(name)); | |
} |
Windows版の対応
それぞれAndroid版と同名のローダを作ります。
LoadFileはassetsフォルダのファイルをfopenでファイルを読み込むだけです。
LoadTextureViaOSではGDI+を使うことで、アルファチャンネル付きの32bitのrawデータが得られます。ここからテクスチャを生成しています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Gdiplus { | |
using std::min; | |
using std::max; | |
} | |
#include <gdiplus.h> | |
#pragma comment(lib, "gdiplus.lib") | |
static GLuint LoadTextureViaOS(const char* name) | |
{ | |
Gdiplus::GdiplusStartupInput gdiplusStartupInput; | |
ULONG_PTR gdiplusToken; | |
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr); | |
WCHAR wc[MAX_PATH]; | |
MultiByteToWideChar(CP_ACP, 0, name, -1, wc, dimof(wc)); | |
Gdiplus::Bitmap* image = new Gdiplus::Bitmap(wc); | |
int w = (int)image->GetWidth(); | |
int h = (int)image->GetHeight(); | |
Gdiplus::Rect rc(0, 0, w, h); | |
Gdiplus::BitmapData* bitmapData = new Gdiplus::BitmapData; | |
image->LockBits(&rc, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, bitmapData); | |
std::vector<uint32_t> col; | |
col.resize(w * h); | |
for (int y = 0; y < h; y++) { | |
memcpy(&col[y * w], (char*)bitmapData->Scan0 + bitmapData->Stride * y, w * 4); | |
for (int x = 0; x < w; x++) { | |
uint32_t& c = col[y * w + x]; | |
c = (c & 0xff00ff00) | ((c & 0xff) << 16) | ((c & 0xff0000) >> 16); | |
} | |
} | |
image->UnlockBits(bitmapData); | |
delete bitmapData; | |
delete image; | |
Gdiplus::GdiplusShutdown(gdiplusToken); | |
GLuint texture; | |
glGenTextures(1, &texture); | |
glBindTexture(GL_TEXTURE_2D, texture); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); | |
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, &col[0]); | |
glGenerateMipmap(GL_TEXTURE_2D); | |
glBindTexture(GL_TEXTURE_2D, 0); | |
return texture; | |
} |
OpenGLは以前作ったWGLGrabberを使って関数ポインタを取得しています。
ネイティブクラスの設計
基本的に出来る限りC++で処理すると決めたので、onTouchEventを使ったタッチ等のイベントは全部JavaからC++に渡します。また、GLSurfaceViewを使うので、onDrawFrameからC++のレンダラーを呼び出すことになります。
注意すべきはJavaのスレッドが2つあることです。onTouchEventはUIスレッド、onDrawFrameはレンダースレッドです。
今回は簡単にするため、タッチの座標だけ保存しておいてonDrawFrameの中からタッチイベントもC++に渡します。こうすることでC++は常にレンダースレッドで実行されることが保証されます。
将来的にはマルチスレッドの同期取りはC++側に移動したほうが最適化しやすいかもしれません。
まとめ
OpenGLを使った開発はOpenGL関連のコードが大多数を占めるので、このようにプラットフォーム依存する場所を開発の始めに括っておくと以後のマルチプラットフォーム化が楽になります。
No comments:
Post a Comment