Windows では、(OpenGL 等を使用しない場合)BMP 画像ならいったんメモリに読み込んで HBITMAP のような「ビットマップハンドル」を取得し、
BitBlt 関数等で画面(Window DC)に描画します。
画像の 1 ドットは基本的にウィンドウ(画面)の
Android などのモバイル端末では、デバイスの種類が多様で、 画面解像度も画面サイズ(ピクセル数)も想定できません。 また、通常の場合は全画面でアプリを実行するため、 直接座標指定はしない、画像と画面も 1 ピクセルに対して 1 ドットに対応させません。
また、描画タイミングでは毎回、全画面再描画が基本のようであり、
ここでは、BMP 画像(正確には作成済みのテクスチャ)を画面に描画できるように、詳細を検討しています。 OpenGL ES は 3D の描画にも対応しているようですが、ここでは 2D のみを扱っています。
このページ、および開発関連ページは、PC 向けデザインとなっております。 画面サイズの小さいスマホでは、快適な表示が得られませんので、ご了承ください。
ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
また、本サイトが初めての方は、まずこのページの注意事項をご覧ください。
画面に 2D テクスチャを描画するための手順を、まずまとめておきます。
描画自体は main.cpp のメインループから呼び出される engine_draw_frame 関数が行います。 どのような仕組みで呼び出されているかは、下記トピックで書いていますが、今すぐ必要な知識というほどではありません。
android_main 関数(メインループ)
作成されたままの android_main 関数を確認し、
どのような処理が行われているかを検証しています。
自動生成されたコードの engine_draw_frame 関数を見ると、 バッファに書き込んだ画像データを実際に画面に転送する eglSwapBuffers 関数を呼び出す前に、 画面クリア色を設定する glClearColor 関数と 画面クリアを実行する glClear 関数の呼び出しのみがあります。 この部分を自分のコードに置き換えます。
// 色で画面を塗りつぶします。 glClearColor(((float)engine->state.x) / engine->width, engine->state.angle, ((float)engine->state.y) / engine->height, 1); glClear(GL_COLOR_BUFFER_BIT);
実際のコーディング時には main.cpp の書き換えを最小限にしたいので、 アプリ用のクラスを用意して struct android_app* を渡して描画を行う関数を呼び出すようにしています。 今は描画までの手順を明らかにする目的ですので、だらだらとコードを並べます。 なお、関数化したときに渡す struct android_app* は、 engine_draw_frame 関数が引数で受け取っている struct engine* engine の app メンバー(engine->app)です。
eglSwapBuffers 関数呼び出しは、描画したものを実際に画面に転送する関数なので、ここに残します。
すると、engine_draw_frame 関数は、以下のようになります。
まずは画面全体を背景色でクリアしています。 今回は全画面に、縦横比に関係なくイメージを描画しますので意味はありませんが、 縦横比を決めて描画する場合は画面に余りがでるので、ちゃんと何か描画しておくべきなのでしょう。
glClearColor(1.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT);
glClearColor の引数は R、G、B、A の順に 0.0 〜 1.0 指定ですので、 この指定だと完全に不透明な赤、という指定です。 glClear 関数呼び出しで、画面全体をクリアです。 この時点では画面は更新されず、最後の eglSwapBuffers 関数呼び出しで実際の画面に反映されます。
まず、glEnable 関数に GL_TEXTURE_2D を渡して、2次元テクスチャを有効にします。 つまり、これから使うという宣言です。
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnableClientState 関数に GL_TEXTURE_COORD_ARRAY を渡し、 テクスチャ画像のどの部分を使用するかを指定するための座標系を選択しているようです。 テクスチャとして作成した画像は、全部を一度に使用する必要はなく、 そのうちのこの領域だけを使う、という指定ができるようになっています。 "COORD" は coordinate の意味で、「座標」を表しています。
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState 関数に GL_VERTEX_ARRAY を渡すと、 ポリゴン(描画面)の 座標指定方法を選択しているようです。
glEnableClientState(GL_VERTEX_ARRAY);
以上は、描画前に毎回の手続きのようですから、1関数にまとめておきます。 決まった手続きしかしないので、グローバルな関数にしてもいいのではないかと思います。 あるいは描画クラスの関数にしてもいいのかも知れません。
いよいよテクスチャを選択して描画します。 複数のテクスチャを使用する場合は、選択して描画を繰り返すことになります。
glBindTexture(GL_TEXTURE_2D, m_textureID);
テクスチャを選択することを「バインド」と呼んでいるようです。 m_textureID は、テクスチャ登録時に glGenTextures 関数で返された値です。
バインドしたテクスチャのどの部分を使用するかを、glTexCoordPointer 関数で設定します。 今はテクスチャ全体を指定したいので、次のように渡します。
画像の座標は左下が原点 (0.0, 0.0) で、右端が 1.0、上端が 1.0 となっています。 算数や数学のグラフと同じイメージです。 指定する値はピクセル単位ではなく割合で、また、y 座標が Windows の指定と逆になっています。 なのでいったん left、top、right、bottom に Windows 風の値を設定し、 それを計算して正しい値としています。
GLfloat texuv[8]; // テクスチャ画像の使用範囲 GLfloat left = 0.0f, top = 0.0f, right = 1.0f, bottom = 1.0f; texuv[0] = left; // 左下:画像原点(画像に対して左端・下端が原点) texuv[1] = 1.0f - bottom; texuv[2] = right; // 右下 texuv[3] = 1.0f - bottom; texuv[4] = left; // 左上 texuv[5] = 1.0f - top; texuv[6] = right; // 右上 texuv[7] = 1.0f - top; glTexCoordPointer(2, GL_FLOAT, 0, texuv);
2次元なので、(x,y) のセットが4つです。
描画位置指定です。 画面のどの部分に描画を行うかを、glVertexPointer 関数で指定しています。 今は、テクスチャ画像の縦横比に関係なく、画面全体を指定するようにしています。
画面の座標は中央が原点 (0.0, 0.0) で、 x 座標は左端が -1.0、右端が 1.0 であり、 y 座標は下端が -1.0、上端が 1.0 となっているようです。 本来は Windows のような指定にしたいところですが、 今はこの座標系で指定できるまでにしています。
GLfloat vtxz[12]; GLfloat xf = -1.0f, wf = 1.0f, yf = 1.0f, hf = -1.0f; vtxz[0] = xf; // 左下 vtxz[1] = hf; vtxz[2] = 0.0f; vtxz[3] = wf; // 右下 vtxz[4] = hf; vtxz[5] = 0.0f; vtxz[6] = xf; // 左上 vtxz[7] = yf; vtxz[8] = 0.0f; vtxz[9] = wf; // 右上 vtxz[10] = yf; vtxz[11] = 0.0f; glVertexPointer(3, GL_FLOAT, 0, vtxz);
座標指定自体は3次元なので、(x,y,z) のセットが4つです。 2次元で描画するため、z 座標をすべて 0.0f にしています。 なお、この順序などの指定については奥が深そうですので、とりあえず動作したものを追及していません。
指定は長方形である必要はないと思われますが、まずは長方形としていますので、 対応関係がどうなっているかを明示するため、いったん変数に値をいれてから設定しています。
ここで、必要なら、glTranslatef 関数で移動や、glRotatef 関数で回転を指定することができるようですが、今は省略します。
指定が完了したら、「テクスチャを描画」します。 おそらく言葉としては正しくありませんが、そういうイメージでいます。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
描画の後処理です。
glEnable 系の関数を呼び出したときは、最後に glDisable 系の関数を呼び出して、設定を戻す必要があるようです。
// 頂点配列の有効化を解除します。 glDisableClientState(GL_VERTEX_ARRAY); // テクスチャマッピング座標の有効化を解除します。 glDisableClientState(GL_TEXTURE_COORD_ARRAY); // アルファブレンド設定を解除します。 glDisable(GL_BLEND);
これで描画コードは終わりですので、 もとからある eglSwapBuffers 関数が呼び出されて、画面に表示されます。
eglSwapBuffers(engine->display, engine->surface);
複数のテクスチャを別の設定で描画したいとき、 glPushMatrix 呼び出しと glPopMatrix 呼び出しで囲うと、 一時的に設定を変更しても、簡単に元に戻せるようです。
次のセクションは、テクスチャの登録についてです。 上記コードの m_textureID を得るまでです。
engine_draw_frame 関数で描画を行うために、 テクスチャを作成しておかなくてはなりません。
本来は「行儀よく」そのテクスチャを使う直前に読み込み、使い終わったら解放する、 あるいはよく使うテクスチャ画像はまとめて1枚にして切り出して使う、 のようにすべきなのでしょう。
engine_draw_frame 関数でテクスチャを作成しようとすると、 すでに読み込んであるテクスチャを描画ごとに再読み込みするなどの不都合がありますので、 うまく制御しなくてはいけません。
今はテスト目的ですので、最初に1回読み込んで、あとはずっとそれを使います。
アプリ起動時には、android_main 関数からスタートして、 メインループに入ると、順次メッセージが処理されます。 最初に来るメッセージが APP_CMD_INIT_WINDOW であり、APP_CMD_GAINED_FOCUS のあと、 engine_draw_frame 関数が繰り返し呼び出されているようです。
少なくとも、テクスチャの作成が行えるのは APP_CMD_INIT_WINDOW メッセージが到着し、 engine_init_display 関数が呼び出されて OpenGL ES が初期化されてからですので、 もっとも簡単なテクスチャ作成位置は engine_init_display 関数の最後になります。
なお、engine_init_display 関数はアプリ起動時にしか呼び出されませんので、 いったんホーム画面に戻ったり、別アプリを使ったりして、タスク一覧から戻ってくると、 テクスチャが破棄されていて画面表示されないようです。 APP_CMD_GAINED_FOCUS が最適かもしれないです。
OpenGL ES で 2D テクスチャを作成する
OpenGL ES で 2D テクスチャを作成するためのコードについて、検討しています。
「OpenGL ES で 2D テクスチャを作成する」の 「テクスチャ管理クラス」にあるような、 CaxTexture テクスチャ管理クラス m_tex があるとします。
if (m_tex.generateID() < 1) { log.add(engine->app, "テクスチャ ID の生成に失敗"); }
実装した generateID 関数を呼び出し、 利用可能なテクスチャ ID をシステムから取得し、 メンバー変数 m_textureID に設定します。
log によるログ出力は、独自のログ保存用のクラス・関数です。
C/C++ によるログ出力
Android の LogCat が非常に使いにくいと感じたので、
独自のログファイルに、自由にログを出力できるコードを用意しました。
GLuint CaxTexture::generateID(void) { if (m_textureID < 1) { // 未設定の時のみ glGenTextures(1, &m_textureID); // 空きテクスチャIDを取得 if (m_textureID < 1) { // 空きがないとき return 0; // 失敗します } } return m_textureID; }
有効なテクスチャ ID は、1 以上です。 m_textureID が 0 で初期化されているとして、 m_textureID が有効でない場合にのみ、テクスチャ ID の要求を行います。 すでに m_textureID に 1 以上の値がある場合は、その値を返すのみです。
glGenTextures 関数に、欲しいテクスチャ ID の数と、 それを格納するポインタを渡します。 今は1つでいいので、こんな呼び出しです。 1 以上の値が返れば成功なので、その値を返します。 0 が返れば失敗で、空きテクスチャ ID がないとか、OpenGL 未初期化とか、そういう意味になります。
空きテクスチャ ID を取得できた場合のみ、以下の処理を行います。 取得できなければ描画もできませんから、アプリとしては「致命的エラー」で続行不能です。
if (!m_tex.isImageLoaded()) { m_tex.bind(); m_tex.setParamsRGBA(); AAssetManager* mgr = engine->app->activity->assetManager; m_tex.loadBmpRGBA(mgr, "bg.bmp"); if (m_tex.createImage2D()) { log.add(engine->app, "テクスチャの読み込み成功"); } else{ log.add(engine->app, "テクスチャの読み込み失敗"); } }
詳しくは「OpenGL ES で 2D テクスチャを作成する」に書いていますので、 ここでは流れ中心です。
クラスに実装した isImageLoaded 関数は、イメージが読み込まれたかどうかを返します。 テクスチャ ID を取得したあと、イメージを登録しないとテクスチャとしては使えませんので、 最初は false が返ることになります。
次の bind 関数は、m_textureID が有効な場合のみ、 glBindTexture(GL_TEXTURE_2D, m_textureID); を実行するための関数です。 これからこのテクスチャ ID に対して操作する、という宣言です。
setParamsRGBA 関数は、 「テクスチャ描画方法の指定」です。 データ形式や拡大・縮小時の処理方法などを設定しています。
画像データは .Packaing の assets フォルダに入れていますので、 アセットマネージャー AAssetManager を使用して、メモリに読み込みます。 loadBmpRGBA 関数として、assets にあるビットマップファイルを RGBA 形式でメモリに読み込むため、定義しています。
読み込まれた RGBA ビットデータを、createImage2D 関数でシステムに登録します。 「createImage2D 関数」 に実体がありますが、 テクスチャ ID が正常で、ビットデータが読み込まれているとき、 それを glTexImage2D 関数に渡して、テクスチャを作成する関数です。
私はテクスチャやポリゴンについて、ほんの少しだけ知識がありましたが、ちゃんと利用したことはありませんでした。
テクスチャの作成は、以下の流れでよさそうです。
1.glGenTextures 関数で、システムから、空いているテクスチャ ID を取得する。
2.glBindTexture 関数で、取得したテクスチャ ID を選択する。
3.描画モードを指定する。
glPixelStorei 関数で、RGBA フォーマットで1ピクセル4バイトを指定する。
glTexParameteri 関数で、拡大・縮小時の処理方法を指定する。
同じく glTexParameteri 関数で、画像不足時の処理方法を指定する。
glTexEnvi 関数で、透過処理方法を指定する。
4.アセットマネージャーを使用して、メモリに画像を読み込む。
5.glTexImage2D 関数で、2D テクスチャを作成する。
一部の指定は余剰かもしれませんが、大きな遅延にはつながらないと思っています。
よく使うテクスチャは解放せず保持したほうが速度的に有利と思いますので、いちいち解放しない代わりに、 同じテクスチャを何度も読み込まないように、管理する必要があります。
また、特定の画面のみで使用するテクスチャは、その都度解放するほうが、
透過が必要な画像は PNG にするつもりですが、簡単に作成できますが、簡単にはデコードできません。 32 ビット BMP でもいいのですが、これは簡単に作成できないのではないかと思います。
透過が不要な背景画像などは BMP で扱うよりも、圧縮されている PNG や JPG にしたほうが、
ダウンロードサイズ(.apk のサイズ;小さいほど
Android 開発に関する記事をまとめた Android 開発トップ もご覧ください。