Android ネイティブアプリで画面を構成するには、それが 3D 表示でなくても、 「テクスチャ」を使用しなくてはなりません。 今は 2D しか扱いませんが、それでも「面を作成して、それにテクスチャ(表面画像)を貼り付ける」という形をとる必要があります。
まずは背景画像など、透過が不要な画像を描画するために、ビットマップ画像を読み込んでテクスチャとしたいのですが、 特に簡単なクラスなどは用意されていないようである上、Windows のように自由にファイルにアクセスできるわけでもないようです。
ここでは、パッケージの assets フォルダに置いた 24 ビットカラーのビットマップファイルから、 縦横サイズとビットデータを取り出し、メモリに確保する部分までを扱っています。 すなわち、
void loadBmpRGBA(AAssetManager* AssetManager, const char* filename);
として、
【 2021 年 3 月 6 日】
VS2019 による再スタートしたら、当時一気に欲張って実装したために、個別の機能がわかりにくくなってしまっていました。
順を追って見直し、それぞれのページの書き換えを進めています。
このページ、および開発関連ページは、PC向けデザインとなっております。 画面サイズの小さいスマホでは、快適な表示が得られませんので、ご了承ください。
ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
また、本サイトが初めての方は、まずこのページの注意事項をご覧ください。
ここでは、ファイル構成が最も単純な、圧縮なしの 24 ビット BMP ファイルを対象とします。
ファイルの先頭から、
ビットマップ・ファイル・ヘッダー(BITMAPFILEHEADER)、
ビットマップ・インフォ・ヘッダー(BITMAPINFOHEADER)
と続きます。
カラーパレット情報はなく、このあとすぐに 1 ピクセルが 3 バイトのデータが始まります。
構造体は自分で定義しないといけないようですので、アプリに依存しない共通の定義として、 axCommon.h と名前を付けた axCommonDef.h と名前を付けたファイルに定義しました。
ファイル名の先頭の ax は Android 開発用に作成したファイルであることを意味することにしています。 以前は axCommon.h としていましたが、新しく axCommonDef.h に変更しました。 ここにはこのような定義のみを入れることとして、他のコードからの独立性を保つ予定です。
ファイル名は、もちろん何でも構いません。 BMP ファイルを解析する部分からだけ参照できれば、問題ありません。
// ビットマップ・ファイル・ヘッダー(14バイト) // 【注意】4バイトアラインメントにより、bfTypeのあと、bfSizeがオフセット4に配置されます。 typedef struct tagBITMAPFILEHEADER { uint16_t bfType; // [00] 2-byte(WORD) 識別子("BM" であること) uint32_t bfSize; // [02] 4-byte(DWORD) ビットマップファイルのサイズ(アラインメントの影響を受ける) uint16_t bfReserved1; // [06] 2-byte(WORD) 予約済み(0であること) uint16_t bfReserved2; // [08] 2-byte(WORD) 予約済み(0であること) uint32_t bfOffBits; // [10] 4-byte(DWORD) このヘッダの最初からビットデータへのオフセット } BITMAPFILEHEADER, *PBITMAPFILEHEADER;
念のためですが、uint16_t は符号なし 16 ビット整数、uint32_t は符号なし 32 ビット整数です。 バイト数を正しく設定しないと、取得される値が正しくなくなりますので、型指定には注意が必要です。
// ビットマップ情報ヘッダ(40バイト) typedef struct tagBITMAPINFOHEADER { uint32_t biSize; // [00] 4-byte(DWORD) このデータのバイト数 int32_t biWidth; // [04] 4-byte(LONG) ビットマップの幅 int32_t biHeight; // [08] 4-byte(LONG) ビットマップの高さ(負のとき上から下へのDIB) uint16_t biPlanes; // [12] 2-byte(WORD) プレーン数(1であること) uint16_t biBitCount; // 2-byte(WORD) bits-per-pixel uint32_t biCompression; // [16] 4-byte(DWORD) BI_RGB またはその他 uint32_t biSizeImage; // [20] 4-byte(DWORD) イメージのバイト数(BI_RGBのとき0も可) int32_t biXPelsPerMeter; // [24] 4-byte(LONG) 水平解像度(pixels-per-meter) int32_t biYPelsPerMeter; // [28] 4-byte(LONG) 垂直解像度(pixels-per-meter) uint32_t biClrUsed; // [32] 4-byte(DWORD) カラーテーブルのインデックス数 uint32_t biClrImportant; // [36] 4-byte(DWORD) 使用しているカラーテーブル数(0のときすべて) } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
int32_t は符号付き 32 ビット整数です。
このあとにビットデータが続くわけですが、あとにまわします。
Java を使用した Android アプリだと、画像データは、解像度別に .Package の res フォルダ内にある drawable や drawable-hdpi に入れておくと簡単にアクセスできるか、 デバイスの解像度などを気にせず(自動的に)アクセスできると思いますが、 ネイティブアプリでは自由度が高い代償に(?)、自分でやる必要があります。
とりあえず、(イメージ)データファイルは assets フォルダを作成して、置くことにします。 .Packaging の中身は実際のフォルダと同等なので、さらにサブフォルダを作成して置き、アクセスすることもできます。
なお、アセットとは「資産」のことで、フォルダ名は assets と複数形にしておかないと、アクセスできないかも知れません。
assets は、ソリューションの <プロジェクト名>.Package にあるべきフォルダですが、 VS2015 でも VS2019 でも、自動で作成されたコードにはありませんので、手動で作成します。 <プロジェクト名>.Package を右クリックし、 追加、新しいフォルダーと選択して、作成します。
作成した assets を右クリックし、 追加、既存の項目と選んでファイルを取り込むと、 元の場所からコピーされたファイルが、プロジェクトの .Packaging にある assets フォルダに単純にコピーされるようです。 そしてそのまま、.apk ファイルに取り込まれるようです。
取り込まれる様子を確認してみます。
Debug でビルドし、エクスプローラーでソリューションのフォルダを確認すると、 プロジェクトの <プロジェクト名>.Packaging 内に、ARM や ARM64 などプラットフォーム別のフォルダができて、 そこに Debug サブフォルダが作成され、 .apk ファイルが作成されます。
.apk ファイルは拡張子を .zip にすると中身を確認することができます。
単なる zip ファイルですので、ビットマップイメージは高圧縮されていることがわかります。
よって、.apk ファイルサイズを小さくするために、
(透過ビットがない画像を)JPEG や PNG で保存して展開しても、.apk ダウンロードサイズのメリットはほぼなく、
assets フォルダからのファイルの読み込みは、 何をするにも(データファイルを参照するとか、サウンドデータを読み込むとか)基本となるようですので、 共通で利用する関数として用意しました。 コード自体は単純で、下記のようになりました。
AAsset や AAssetManager で始まる関数は、アセット系の関数で、 Android Depelopers の Asset を参照しています。 ちなみに、先頭の A が、"Android" を意味しているようです。ミスタイプでダブっているわけではありません。
/** * 【アセット】asset にあるファイルを読み込んで、メモリに格納します。 */ void* assetLoad(AAssetManager* AssetManager, const char* name, uint32_t* size) { AAsset* _asset = AAssetManager_open(AssetManager, name, AASSET_MODE_BUFFER); assert(_asset); size_t _size = AAsset_getLength(_asset); // ファイルサイズを取得します void* _buf = malloc(_size); // データバッファを用意します AAsset_read(_asset, _buf, _size); // データを読み込みます AAsset_close(_asset); // アセットをクローズします // パラメータでサイズを返す指定となっていたら、値を設定します。 if (size) { *size = (uint32_t)_size; } // 確保したメモリへのポインタを返します。 return _buf; }
引数 AssetManager は、
AAssetManager* AssetManager =
のようにして取得できる、アセットマネージャーと呼ばれるものです。
AAssetManager_open 関数で、 引数 name で指定した名前のファイルをオープンします。 返される AAsset* は、読み込み用のアクセスを指すもののようですので、FILE* と同じ感覚でしょう。
AAsset_getLength 関数でファイルサイズを取得し、
malloc 関数でメモリを確保しています。
本当はメモリの確保が成功したかどうかを調べる必要があるとは思いますが、
これで、assets フォルダにあるファイルの内容が、メモリに取り込まれたことになります。
引数で uint32_t* size が指定されている(NULL ではない)場合、 それにファイルサイズを設定し、確保したメモリへのポインタを返しています。
この関数を呼び出した側で、確保したメモリを解放する必要があることに注意が必要です。
free(_buf);
のような呼び出しで良さそうです。
この読み込み処理は、画像を使用する前に1回だけ実行すればよいので、 今はとりあえず、main.cpp のメインループに入る前に実行します。
uint32_t sz; void* buf = sa::saGX::assetLoad(state->activity->assetManager, "bgFinish.bmp", &sz); /* ここで buf を使用して BMP をメモリに展開 */ free(buf);
assetLoad 関数は、sa::saGX ネームスペースに定義されているとしています。
作成したコードでは、
namespace saAX { class CaxTexture { public: // constructor CaxTexture(); // destructor ~CaxTexture(); // assets から指定のBMP画像を読み込み、RGBAデータとしてメモリに格納します。 void loadBmpRGBA(AAssetManager* AssetManager, const char* filename); protected: // 画像データビット GLubyte* m_bits; }; }
axTexture.h に置いた定義は、こんなイメージです。 saAX ネームスペース内に、CaxTexture クラスを定義しています。
AAssetManager* mgr = app->activity->assetManager; texture.loadBmpRGBA(mgr, "bgFinish.bmp");
以降、loadBmpRGBA 関数の定義と読み込み部分です。
関数を分割して記載している都合で、変数の定義位置などは変えてあります。 また、assetLoad 関数は、実際には外部関数として定義していますが、記載の都合上、クラス内にあるようになっています。
void CaxTexture::loadBmpRGBA(AAssetManager* AssetManager, const char* filename) { uint32_t size; // アセットから画像を読み込みます。 char* data = (char*)assetLoad(AssetManager, filename, &size);
data に、指定したファイルデータがまるまる読み込まれた、malloc 関数で確保されたメモリへのポインタが返されます。 正式には、読み込みエラーもあり得るので、data を検証する必要があります。
BMP ファイルのヘッダーを解析し、m_bits に必要なメモリを確保するまでが、次の部分です。
// ヘッダ情報から、画像サイズを取得します。 BITMAPFILEHEADER bfh; BITMAPINFOHEADER bih; bmpGetHeaders(&bfh, &bih, data); // dataからヘッダ情報を取り出します m_width = bih.biWidth; // 横サイズ m_height = bih.biHeight; // 縦サイズ // 必要なサイズの RGBA メモリ領域を用意します。 deleteBits(); // m_bits がすでに確保されていれば削除 m_bits = new GLubyte[(m_width * 4) * m_height]; // 画像データ領域 if (!m_bits) { return; // メモリ不足 }
bmpGetHeaders 関数は、BMP ファイルのヘッダーを bfh と bih に取得する関数です。 あとで触れます。 deleteBits 関数は、m_bits にすでにメモリが確保されていたら解放する関数です。
なお、bmpGetHeaders 関数も、実際には外部関数として定義していますが、記載の都合上、クラス内にあるようになっています。
m_bits にビットデータをコピーして、完了となります。 セットする値は、1ピクセル4バイトの RGBA データです。 GLubyte 型は、符号なし1バイトです。
unsigned int x, y; int n, r, base; // 画像データを設定します。 base = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); // BMPファイルのヘッダ部(54バイト)をスキップ for (y = 0; y < m_height; y++) { n = y * (m_width * 4); // 書き込み対象ラインの先頭のオフセット for (x = 0; x < m_width; x++) { r = n + x * 4; m_bits[r + 2] = data[base]; // B m_bits[r + 1] = data[base + 1]; // G m_bits[r + 0] = data[base + 2]; // R m_bits[r + 3] = 255; // A(255=不透明) // 画像データのインデックスを進めます。 // 24ビットカラー固定であれば、3進めます。 base += 3; } } // assetLoad で確保したメモリは、呼び出し側で解放する必要があります。 free(data); }
【メモ】画像の幅にパディングがある(4の倍数でない)と、うまくいかなそうです。行ごとに base を再計算すれば良さそうです。
BMP ファイルのヘッダーを解析する bmpGetHeaders 関数です。 BMP ファイルをまるまる読み込んだメモリから、 BITMAPFILEHEADER 構造体と、 BITMAPINFOHEADER 構造体を取得します。
bool bmpGetHeaders(PBITMAPFILEHEADER bfh, PBITMAPINFOHEADER bih, char* data) { char* off; // 引数のチェックを行います。 if (!bfh || !bih || !data) { return false; } // BITMAPFILEHEADER にデータを設定します。 bfh->bfType = valDataUint16(data); // [00] 2-byte(WORD) 識別子("BM" であること) bfh->bfSize = valDataUint32(data + 2); // [02] 4-byte(DWORD) ビットマップファイルのサイズ(アラインメントの影響を受ける) bfh->bfReserved1 = valDataUint16(data + 6); // [06] 2-byte(WORD) 予約済み(0であること) bfh->bfReserved2 = valDataUint16(data + 8); // [08] 2-byte(WORD) 予約済み(0であること) bfh->bfOffBits = valDataUint32(data + 10); // [10] 4-byte(DWORD) このヘッダの最初からビットデータへのオフセット // BITMAPFILEHEADER のサイズだけ進めます。 off = data + 14; // BITMAPINFOHEADER にデータを設定します。 bih->biSize = valDataUint32(off); // [00] 4-byte(DWORD) このデータのバイト数 bih->biWidth = valDataInt32(off + 4); // [04] 4-byte(LONG) ビットマップの幅 bih->biHeight = valDataInt32(off + 8); // [08] 4-byte(LONG) ビットマップの高さ(負のとき上から下へのDIB) bih->biPlanes = valDataUint16(off + 12); // [12] 2-byte(WORD) プレーン数(1であること) bih->biBitCount = valDataUint16(off + 14); // [14] 2-byte(WORD) bits-per-pixel bih->biCompression = valDataUint32(off + 16); // [16] 4-byte(DWORD) BI_RGB またはその他 bih->biSizeImage = valDataUint32(off + 20); // [20] 4-byte(DWORD) イメージのバイト数(BI_RGBのとき0も可) bih->biXPelsPerMeter = valDataInt32(off + 24); // [24] 4-byte(LONG) 水平解像度(pixels-per-meter) bih->biYPelsPerMeter = valDataInt32(off + 28); // [28] 4-byte(LONG) 垂直解像度(pixels-per-meter) bih->biClrUsed = valDataUint32(off + 32); // [32] 4-byte(DWORD) カラーテーブルのインデックス数 bih->biClrImportant = valDataUint32(off + 36); // [36] 4-byte(DWORD) 使用しているカラーテーブル数(0のときすべて) return true; }
valDataUint16 関数は、引数で渡したバイトデータから符号なし 16 ビット整数を取り出す関数です。 同様に、valDataUint32 関数は、符号なし 32 ビット整数を、 valDataInt32 関数は、符号あり 32 ビット整数を取り出す関数です。
いずれもグローバルに定義しています(実際には、ネームスペースを作成して定義しています)。
uint16_t valDataUint16(char* data) { return *((uint16_t*)data); } int32_t valDataInt32(char* data) { return *((int32_t*)data); } uint32_t valDataUint32(char* data) { return *((uint32_t*)data); }
ここまでで、パッケージの assets フォルダに置いた BMP 画像を読み込み、 m_bits にビットデータをセット、 m_width と m_height に画像サイズが設定されました。
これで、2D テクスチャを作成できるようになります。
OpenGL ES で 2D テクスチャを作成する
テクスチャ管理クラス CaxTexture を拡張し、
読み込み済みのイメージデータから 2D テクスチャを作成する手順について、書いています。
Android 開発に関する記事をまとめた Android 開発トップ もご覧ください。